รู้จักกับ Drizzle ORM ผ่าน Next.js

/ 14 min read

Share on social media

drizzle-nextjs สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

Drizzle ORM คืออะไร

https://orm.drizzle.team/docs/overview

Drizzle ORM คือเครื่องมือ (Object-Relational Mapping: ORM) ที่ใช้ในการเชื่อมต่อและจัดการฐานข้อมูล SQL ผ่านภาษา Programming โดยเฉพาะในระบบที่ใช้ JavaScript หรือ TypeScript โดยมันช่วยให้การเขียน code ที่เชื่อมกับ database ง่ายขึ้น โดยที่นักพัฒนาไม่จำเป็นต้องเขียน SQL แบบดิบ ๆ เอง (ตาม concept ของ ORM นั่นแหละ)

คุณสมบัติที่โดดเด่นของ Drizzle ORM คือ

  1. Typed SQL: รองรับการสร้างและจัดการ query SQL ในแบบที่มีการตรวจสอบ Type เมื่อใช้กับ TypeScript
  2. Schema Generation: สามารถสร้าง schema ของตารางในฐานข้อมูลโดยใช้ JavaScript/TypeScript และตรวจสอบการเปลี่ยนแปลง (migrations) ได้ง่าย
  3. Lightweight: ออกแบบมาให้มีขนาดเล็ก
  4. Compatible with Multiple Databases: รองรับหลายฐานข้อมูล เช่น PostgreSQL, MySQL, SQLite

จุดที่ทำให้หลายๆคนชอบความเป็น Drizzle คือ

  1. คำสั่งของ library มีคล้ายๆกับ SQL สามารถใช้ idea เดียวกันเวลา query map กลับมา ORM ได้ไม่ยาก
  2. แยกส่วนระหว่าง ORM และ migration (แยก library สำหรับทำ migration ไว้) ทำให้ Drizzle มีขนาดที่ไม่ใหญ่มาก (Lightweight)
  3. ใช้ร่วมกับ Typescript ได้ดี

แชร์กันไว้ก่อนว่า ประสบการณ์ส่วนตัวของผมเองนั้น ยังไม่เคยพัฒนา project ขึ้น Production ที่ใช้ Drizzle เลย ดังนั้น ในหัวข้อนี้ เราก็จะมาลองเล่น Drizzle ไปกับทุกๆคนด้วยเช่นกัน (ส่วนสาเหตุที่ทำเพราะ เป็นหนึ่งในหัวข้อที่คนขอกันเยอะพอสมควร) 😆

ลองใช้ Drizzle ORM กับ Next.js

ในหัวข้อนี้เราจะพาทุกคนเล่น Drizzle ผ่าน Next.js 14 กัน ถ้าใครยังไม่ชิน Next.js แนะนำให้ดู Next.js มาก่อนผ่านหัวข้อนี้ได้

https://www.youtube.com/watch?v=e8-WmjKdfRo

โดย project เริ่มต้นด้วย Next.js 14 + Typescript ขึ้นมาก่อน ผ่าน command

Terminal window
npx create-next-app@latest

แล้วเลือกตัวเลือกตามนี้

drizzle-10.webp

เมื่อเรียบร้อย เราก็จะได้ project Next.js ขึ้นมา

สร้าง Database postgresql

(หัวข้อนี้ เราจะไม่ได้ลง detail เรื่อง postgresql มาก หากอยากดู detail เพิ่มเติมของ postgresql สามารถดูได้ผ่านหัวข้อนี้นะครับ https://docs.mikelopster.dev/c/goapi-essential/chapter-4/intro)

เราจะเริ่มจาก สร้าง database ขึ้นมาก่อน เราจะใช้ docker-compose ในการสร้าง database ขึ้นมา (ใครที่ยังไม่เคยสร้าง Database ด้วย Docker ดูเพิ่มเติมจากหัวข้อ Database ORM ตัวอื่นๆก่อนหน้าได้)

version: '3.8'
services:
postgres:
image: postgres:17
container_name: my_postgres
environment:
POSTGRES_DB: mydatabase
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: unless-stopped
volumes:
postgres_data:

จากนั้นเริ่มด้วยสร้าง table users ด้วย SQL กันก่อนเป็นตาราง users มีประกอบด้วย field name, email และ created_at (วันที่สร้าง) ตาม query นี้ (โดยเราจะนำ query นี้ไปลองวางบน Dbeaver กัน)

CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

หลังจาก run query สร้างเรียบร้อย ให้ลอง insert data ผ่าน SQL ดู

INSERT INTO users (name, email)
VALUES
('Jane Smith', '[email protected]'),
('Alice Johnson', '[email protected]'),
('Bob Brown', '[email protected]');

เมื่อเสร็จเรียบร้อย ให้ลอง SELECT query ออกมาเพื่อดูผลลัพธ์การเพิ่ม

drizzle-01.webp

Step ต่อมา เราจะลองเอา Next.js ต่อเข้า database ตัวนี้ผ่าน Drizzle กัน

ลง Drizzle ลง Next.js

เริ่มต้น สิ่งที่เราจะต้องทำคือ ลง Drizzle library เข้าสู่ project สามารถลงได้ด้วย command

Terminal window
npm install drizzle-orm pg
npm i --save-dev @types/pg

โดย library แต่ละตัวนั้น

  • drizzle-orm เป็น library หลักของ Drizzle สำหรับจัดการ ORM
  • pg เป็น PostgreSQL client สำหรับ Node.js ช่วยให้ Next.js สามารถติดต่อและทำการ query กับฐานข้อมูล PostgreSQL ได้โดยตรง
  • @types/pg เป็นชุด type definitions สำหรับ pg ซึ่งเป็นไลบรารีของ TypeScript ช่วยให้คุณได้รับการตรวจสอบข้อมูล (type-checking) ที่ถูกต้องเมื่อใช้งาน pg ในโปรเจคที่ใช้ TypeScript

เมื่อลง library เรียบร้อย step แรกที่เราจะต้องทำกันคือ เพิ่ม config เริ่มต้น + API เริ่มต้นเข้าไป (เพื่อทดสอบว่า config ใช้งานได้จริงหรือไม่) โดยเราจะเพิ่ม file แต่ละจุดไปตามนี้

.
├── README.md
├── app
│ ├── api
│ │ └── users
│ │ └── route.ts
│ └── page.tsx
├── db
│ ├── index.ts
│ └── schema.ts
├── docker-compose.yml
└── .env

สิ่งที่เราเพิ่มมาคือ

  • folder db สำหรับ config เกี่ยวกับ database โดย
    • db/index.ts สำหรับ config connection เข้า postgreSQL (โดย config database เราจะเก็บไว้ใน .env แยกไว้)
    • db/schema.ts สำหรับ schema ที่ประกาศไว้เพื่อเป็นตัวแทน ORM สำหรับคุยกับ database
  • app/api/users/route.ts เป็น API GET users เริ่มต้นโดยจะทำการดึงข้อมูล users ผ่าน ORM ของ Drizzle ออกมา (เป็นการเพิ่ม API + make sure ด้วยว่า API เราต่อถูกต้องแล้วหรือไม่)

เราจะลองมาเพิ่มทีละ file กัน เริ่มจาก db/index.ts ทำการเพิ่ม config connection เริ่มต้นของ Drizzle เข้าไป

// นำเข้า function drizzle จาก drizzle-orm สำหรับ PostgreSQL
import { drizzle } from 'drizzle-orm/node-postgres'
// นำเข้า class Pool จาก pg สำหรับการจัดการการเชื่อมต่อฐานข้อมูล
import { Pool } from 'pg'
// สร้าง pool การเชื่อมต่อฐานข้อมูลใหม่
const pool = new Pool({
// ใช้ connection string จาก environment variable (ที่เก็บใน .env)
connectionString: process.env.DATABASE_URL,
})
// สร้างและส่งออก Object ฐานข้อมูลที่ใช้ drizzle กับ pool ที่สร้างขึ้น
export const db = drizzle(pool)

โดยค่า .env นั้น เรากำหนดเป็น config เดียวกันกับที่เราต่อ Dbeaver ได้เลย

DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase

ต่อมาเพิ่ม db/schema.ts โดยทำการอ้างอิงตาม query ที่ใช้สร้าง table users ขึ้นมา

// นำเข้า function และประเภทข้อมูลที่จำเป็นจาก drizzle-orm
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core'
// กำหนดโครงสร้างตาราง 'users' ในฐานข้อมูล PostgreSQL
export const users = pgTable('users', {
// column id: เป็น primary key, ใช้ serial สำหรับการเพิ่มค่าอัตโนมัติ
id: serial('id').primaryKey(),
// column name: เก็บชื่อผู้ใช้, ความยาวสูงสุด 100 ตัวอักษร, ไม่สามารถเป็นค่าว่างได้
name: varchar('name', { length: 100 }).notNull(),
// column email: เก็บอีเมลผู้ใช้, ความยาวสูงสุด 100 ตัวอักษร, ไม่สามารถเป็นค่าว่างและต้องไม่ซ้ำกัน
email: varchar('email', { length: 100 }).notNull().unique(),
// column createdAt: เก็บเวลาที่สร้างบัญชีผู้ใช้, ค่าเริ่มต้นคือเวลาปัจจุบัน
createdAt: timestamp('created_at').defaultNow(),
})

โดยเมื่อ config ทั้ง schema และ database เรียบร้อยแล้ว เราจะลองเพิ่ม API ใน Next.js ด้วย concept Route Handler กัน โดยลองเพิ่ม API สำหรับ GET ผ่าน app/api/route.ts

// นำเข้า module ฐานข้อมูล
import { db } from '@/db'
// นำเข้า schema มาของตารางผู้ใช้
import { users } from '@/db/schema'
// function GET สำหรับดึงข้อมูลผู้ใช้ทั้งหมด
export async function GET() {
try {
// ดึงข้อมูลผู้ใช้ทั้งหมดจากฐานข้อมูล
const allUsers = await db.select().from(users)
// ส่งข้อมูลผู้ใช้กลับเป็น JSON response
return Response.json(allUsers)
} catch (error) {
console.error('Error fetching users:', error)
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

เมื่อเพิ่ม API เรียบร้อย ให้ลอง check ผลลัพธ์ ผ่าน API GET http://localhost:3000/api/users หากสามารถเรียกได้เรียบร้อยตามภาพ = เรา setup database แล้วเรียบร้อย + schema ถูกต้องเมื่อเทียบกับ database

drizzle-02.webp

Step ต่อมา เราจะลองเพิ่ม API set มาตรฐานเพื่อลองให้ทุกคนเห็นภาพ code สำหรับ ทุกเคสของ CRUD กันว่า code จะออกมาประมาณไหน

CRUD

API ที่เราจะลองเพิ่มเข้ามาจะมีดังนี้

  • POST /api/users สำหรับรับข้อมูล user และสร้าง user ลง table user
  • GET /api/users/:id สำหรับดึงข้อมูล user ราย id
  • PUT /api/users/:id สำหรับแก้ไขข้อมูล user ราย id
  • DELETE /api/users/:id สำหรับลบข้อมูล user ราย id

โดยตาม API ที่ระบุมาก็ต้องเพิ่ม file เข้ามาอีกหนึ่ง file คือ api/users/[id]/route.ts เพื่อเป็นการระบุ path ที่มีการใช้ id เพิ่มเติม

เพื่อเพิ่ม API ให้ครบตามที่ระบุเราจะเริ่มแก้จาก file api/users/route.ts เพื่อเพิ่ม API สำหรับ สร้าง users ขึ้นมา

// นำเข้า module db และ schema ของ users จากโฟลเดอร์ db ที่ตั้งไว้ในโปรเจค
import { db } from '@/db'
import { users } from '@/db/schema'
// สร้าง interface CreateUserRequest เพื่อกำหนดโครงสร้างของข้อมูลที่ต้องการรับเมื่อสร้าง user
interface CreateUserRequest {
name: string
email: string
}
export async function GET() {
try {
const allUsers = await db.select().from(users)
return Response.json(allUsers)
} catch (error) {
console.error('Error fetching users:', error)
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
// function POST สำหรับสร้าง user ใหม่ path POST /api/users
export async function POST(request: Request) {
try {
// รับและแปลงข้อมูล JSON ที่ส่งมาใน request body ให้อยู่ในรูปแบบ CreateUserRequest
const body: CreateUserRequest = await request.json()
const { name, email } = body
// ตรวจสอบว่า name และ email มีค่าหรือไม่
if (!name || !email) {
return Response.json(
{ error: 'Name and email are required' },
{ status: 400 }
)
}
// ถ้ามีค่า name และ email จะทำการ insert ข้อมูลลงตาราง users
const newUser = await db.insert(users).values({ name, email }).returning()
// ส่งข้อมูล user ที่ถูกสร้างขึ้นกลับไปใน response พร้อมสถานะ 201 (created)
return Response.json(newUser[0], { status: 201 })
} catch (error) {
console.error('Error creating user:', error)
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

ต่อมา 3 API ที่เหลือ จะต้องทำใน file ใหม่ที่ /api/users/[id]/route.ts เนื่องจากจะเป็นการเพิ่มที่ path ใหม่ /api/users/:id ออกมา

import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'
// สร้าง interface UpdateUserBody เพื่อกำหนดโครงสร้างของข้อมูลที่ต้องการรับเมื่อ update user
interface UpdateUserBody {
name?: string
email?: string
}
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
// แปลง id จาก string เป็น integer จาก parameter ที่รับเข้ามา
const id = parseInt(params.id)
// ดึงข้อมูล user จากฐานข้อมูล
const [user] = await db
.select()
.from(users)
.where(eq(users.id, id))
.limit(1)
// ถ้าไม่พบ user ให้ส่ง response แจ้ง error
if (!user) {
return Response.json({ error: 'User not found' }, { status: 404 })
}
// ส่ง response กลับไปพร้อมข้อมูล
return Response.json(user)
} catch (error) {
// จัดการกรณีเกิด error
console.error('Error fetching user:', error)
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
try {
// แปลง id จาก string เป็น integer
const id = parseInt(params.id)
// รับข้อมูลที่ต้องการอัพเดทจาก request body
const body: UpdateUserBody = await request.json()
const { name, email } = body
// ตรวจสอบว่ามีข้อมูลที่จะอัพเดทหรือไม่
if (!name && !email) {
return Response.json(
{ error: 'Name or email is required for update' },
{ status: 400 }
)
}
// สร้าง object สำหรับเก็บข้อมูลที่จะอัพเดท
const updateData: { name?: string; email?: string } = {}
if (name) updateData.name = name
if (email) updateData.email = email
// อัพเดทข้อมูล user ในฐานข้อมูล
const [updatedUser] = await db
.update(users)
.set(updateData)
.where(eq(users.id, id))
.returning()
// ถ้าไม่พบ user ให้ส่ง response แจ้ง error
if (!updatedUser) {
return Response.json({ error: 'User not found' }, { status: 404 })
}
// ส่ง response กลับไปพร้อมข้อมูล user ที่อัพเดทแล้ว
return Response.json(updatedUser)
} catch (error) {
// จัดการกรณีเกิด error
console.error('Error updating user:', error)
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
try {
// แปลง id จาก string เป็น integer
const id = parseInt(params.id)
// ลบข้อมูล user จากฐานข้อมูล
const [deletedUser] = await db
.delete(users)
.where(eq(users.id, id))
.returning()
// ถ้าไม่พบ user ให้ส่ง response แจ้ง error
if (!deletedUser) {
return Response.json({ error: 'User not found' }, { status: 404 })
}
// ส่ง response แจ้งว่าลบข้อมูลสำเร็จ
return Response.json({ message: 'User deleted successfully' })
} catch (error) {
// จัดการกรณีเกิด error
console.error('Error deleting user:', error)
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

ผลลัพธ์ก็จะสามารถยิง CRUD ได้ตาม path เหล่านั้นออกมาได้

เพิ่ม Relation

Step ต่อมา เราจะมาลองเล่นอีก 1 feature ของ Drizzle นั่นก็คือ relation กัน

  • การทำ relation ของ database คือการนำ column ใน table มาสร้างความสัมพันธ์กัน และเราสามารถเชื่อมข้อมูลผ่านสัมพันธ์นั้นไปมาหากันได้

โดย table ที่เราจะเพิ่มคือ posts ที่มี relation กับ user คือเป็นเสมือนเจ้าของ post ในแต่ละ item โดยทำการเชื่อมกันผ่าน user_id กำหนดให้เป็น Foreign Key ใน table posts เอาไว้ มีความสัมพันธ์กันประมาณนี้

USERSINTidPKVARCHARnameVARCHARemailTIMESTAMPcreated_atPOSTSINTidPKVARCHARtitleTEXTcontentINTuser_idFKTIMESTAMPcreated_athas many

ทำการ run คำสั่ง SQL สำหรับการสร้าง table posts ขึ้นมา

CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
user_id INT REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

เมื่อสร้างเรียบร้อย เราก็จะได้ table posts เปล่าๆออกมา เราจะมาทำการเพิ่มผ่าน API ของ Next.js กัน โดย step แรกเราจะต้องทำตามเดิมคือ ต้องเพิ่ม schema ใน Drizzle เพื่อให้ ORM รู้จักกับ table users ผ่าน schema.ts

ทำการ update db/schema.ts

// นำเข้า function ต่างๆ จาก Drizzle ORM ที่ใช้สร้าง schema สำหรับ PostgreSQL
import {
pgTable, // function ที่ใช้สร้าง table schema
serial, // ชนิดข้อมูล serial สำหรับ auto-increment (เป็น primary key ได้)
varchar, // ชนิดข้อมูล varchar สำหรับ string ที่กำหนดความยาวสูงสุด
timestamp, // ชนิดข้อมูล timestamp สำหรับเก็บวันเวลา
text, // ชนิดข้อมูล text สำหรับ string ที่ไม่จำกัดความยาว
integer, // ชนิดข้อมูล integer สำหรับตัวเลขจำนวนเต็ม
} from 'drizzle-orm/pg-core'
// users table ถูกสร้างเหมือนเดิม
export const users = /* เหมือนเดิม */
// สร้าง posts table ด้วย function pgTable โดยกำหนดชื่อ table ว่า 'posts'
export const posts = pgTable('posts', {
// column id เป็น serial (auto-increment) และกำหนดให้เป็น primary key
id: serial('id').primaryKey(),
// column title เป็น varchar กำหนดความยาวสูงสุด 200 และห้ามเป็น null
title: varchar('title', { length: 200 }).notNull(),
// column content เป็น text และห้ามเป็น null
content: text('content').notNull(),
// column userId เป็น integer และเป็น foreign key ที่อ้างอิงไปยัง id ของตาราง users
userId: integer('user_id').references(() => users.id),
// column createdAt เป็น timestamp และมีค่า default เป็น CURRENT_TIMESTAMP
createdAt: timestamp('created_at').defaultNow(),
})

เมื่อเพิ่ม schema เรียบร้อย เราจะลองเพิ่ม API สำหรับเพิ่มข้อมูล post เข้ามา โดยเพิ่มผ่า API POST /api/posts

  • ดังนั้นเท่ากับว่า เราจะต้องสร้าง file app/api/posts/route.ts เพื่อทำการสร้าง Path API นี้ขึ้นมา
  • หลังจากสร้างไฟล์มาแล้ว เราจะทำการเพิ่ม code สำหรับการสร้าง post เข้าไปใน Path POST /api/posts โดยวิธีการเพิ่มสามารถใช้วิธีเหมือนกับ users ได้เลย
// นำเข้า db object ที่เชื่อมต่อกับฐานข้อมูล และ schema ของตาราง posts
import { db } from '@/db'
import { posts } from '@/db/schema'
// กำหนด interface CreatePostRequest เพื่ออธิบายรูปแบบข้อมูลที่ใช้ในการสร้าง post
interface CreatePostRequest {
title: string // กำหนดว่า title ต้องเป็น string
content: string // กำหนดว่า content ต้องเป็น string
userId: number // กำหนดว่า userId ต้องเป็น number
}
// function POST สำหรับสร้าง post ใหม่
export async function POST(request: Request) {
try {
// รับข้อมูล JSON จาก request body และแปลงให้อยู่ในรูปแบบของ CreatePostRequest
const body: CreatePostRequest = await request.json()
const { title, content, userId } = body // แยกค่าของ title, content และ userId จาก body
// ตรวจสอบว่าทุก field (title, content, userId) มีค่าหรือไม่
if (!title || !content || !userId) {
// ถ้าขาดฟิลด์ใดฟิลด์หนึ่ง ส่ง response แจ้งว่าข้อมูลไม่สมบูรณ์ (status 400)
return Response.json(
{ error: 'Title, content, and userId are required' },
{ status: 400 }
)
}
// ถ้าข้อมูลสมบูรณ์ ทำการ insert post ใหม่ลงในตาราง posts ด้วยค่า title, content, userId
const [newPost] = await db.insert(posts).values({ title, content, userId }).returning()
// ส่งข้อมูลของ post ที่ถูกสร้างใหม่กลับไปใน response พร้อมสถานะ 201 (created)
return Response.json(newPost, { status: 201 })
} catch (error) {
// หากเกิดข้อผิดพลาด แสดงข้อความ error ใน console
console.error('Error creating post:', error)
// ส่ง response แจ้งว่ามีปัญหา internal server error
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

เสร็จแล้วให้ลองทำการยิง API ผ่าน POST /api/users เมื่อได้เรียบร้อย = schema post ถูกต้องและ API POST ก็สามารถสร้างข้อมูล posts ได้แล้วเรียบร้อย

drizzle-11.webp

Step ต่อมาเพิ่ม API GET /api/posts/:id ที่จะดึงข้อมูล post รายอันออกมาได้ โดยจะต้องทำการสร้าง file app/api/posts/[id]/route.ts ใหม่ขึ้นมาเนื่องจากเป็นการเพิ่ม path ใหม่เข้ามา

  • ทีนี้ สิ่งที่เราอยากได้ผ่าน API นี้ด้วยคือข้อมูล user ของคนสร้าง post โดยจะต้องนำข้อมูล user_id ทำการ join กลับเข้า table user เพื่อเอาข้อมูล user ออกมา
  • ใน Drizzle ได้มีคำสั่ง .join() ที่สามารถเชื่อมข้อมูลผ่าน Foreign key ไว้ได้เลย (หากมีการระบุ relation ผ่าน schema ไว้แล้วเรียบร้อย)
// นำเข้า db object ที่เชื่อมต่อกับฐานข้อมูล, schema ของตาราง posts และ users และ function eq จาก Drizzle ORM
import { db } from '@/db'
import { posts, users } from '@/db/schema'
import { eq } from 'drizzle-orm'
// function GET สำหรับดึงข้อมูล post path GET /api/posts/:id
export async function GET(
request: Request, // รับ request object
{ params }: { params: { id: string } } // รับค่า params จาก URL โดยมี id ของ post เป็น string
) {
try {
// แปลง id จาก string เป็น number เพื่อใช้ในการ query
const id = parseInt(params.id)
// ทำการ query ข้อมูลจากฐานข้อมูล โดยเลือก post และข้อมูลผู้ใช้ที่เกี่ยวข้อง
const [result] = await db
.select({
post: posts, // ดึงข้อมูลทั้งหมดจากตาราง posts
user: { // ดึงเฉพาะบาง column จากตาราง users
id: users.id, // ดึง user id
name: users.name, // ดึงชื่อผู้ใช้
email: users.email, // ดึงอีเมลผู้ใช้
},
})
.from(posts) // ทำการ query จากตาราง posts เป็นหลัก
.leftJoin(users, eq(posts.userId, users.id)) // เชื่อมข้อมูล users ผ่าน userId ของ posts และ id ของ users (LEFT JOIN)
.where(eq(posts.id, id)) // เงื่อนไขในการ query คือ post id ต้องตรงกับ id ที่ได้รับจาก params
.limit(1) // จำกัดผลลัพธ์ให้ได้เพียงแค่ 1 แถว
// ถ้าไม่พบผลลัพธ์ (ไม่มี post ที่ตรงกับ id นั้น)
if (!result) {
return Response.json({ error: 'Post not found' }, { status: 404 }) // ส่ง response แจ้งว่าไม่พบ post
}
// ถ้าพบผลลัพธ์ ส่งข้อมูล post และข้อมูลผู้ใช้ที่เกี่ยวข้องกลับไปใน response
return Response.json(result)
} catch (error) {
// ถ้ามีข้อผิดพลาดเกิดขึ้น แสดงข้อความ error ใน console
console.error('Error fetching post:', error)
// ส่ง response แจ้งว่าเกิดปัญหา internal server error
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

ผลลัพธ์หลังจากเพิ่ม code แล้ว เราก็จะได้ข้อมูล posts + user ออกมา

drizzle-03.webp

โดยฝั่งของ user ที่ GET /api/users/:id ที่ไฟล์ app/api/users/[id]/route.ts ก็สามารถเพิ่มการ query join เข้าไปได้เช่นกัน

import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq } from 'drizzle-orm'
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
// แปลง id จาก string เป็น integer
const id = parseInt(params.id)
// ดึงข้อมูล user และ posts โดยใช้ join
const userWithPosts = await db
.select({
// เลือกข้อมูล user ที่ต้องการ
user: {
id: users.id,
name: users.name,
email: users.email,
createdAt: users.createdAt,
},
// เลือกข้อมูล posts ที่ต้องการ
posts: {
id: posts.id,
title: posts.title,
content: posts.content,
createdAt: posts.createdAt,
},
})
.from(users)
// ใช้ leftJoin เพื่อรวมข้อมูล posts กับ users
.leftJoin(posts, eq(users.id, posts.userId))
// กรองเฉพาะ user ที่มี id ตรงกับที่ระบุ
.where(eq(users.id, id))
// ตรวจสอบว่าพบ user หรือไม่
if (userWithPosts.length === 0) {
return Response.json({ error: 'User not found' }, { status: 404 })
}
// จัดรูปแบบข้อมูลให้เหมาะสม
const result = {
// ดึงข้อมูล user จากผลลัพธ์แรก
user: userWithPosts[0].user,
// กรองและแปลงข้อมูล posts
posts: userWithPosts
// กรองเฉพาะ posts ที่มี id (ไม่เป็น null)
.filter((row) => row.posts?.id != null)
// แปลงให้เหลือเฉพาะข้อมูล posts
.map((row) => row.posts),
}
// ส่งผลลัพธ์กลับเป็น JSON
return Response.json(result)
} catch (error) {
// จัดการกับข้อผิดพลาด
console.error('Error fetching user:', error)
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

ผลลัพธ์

drizzle-04.webp

และนี่ก็คือ use case ของการใช้งานร่วมกับ relation ทุกคนสามารถลองประยุกต์ใช้กับเคสตัวเองเพิ่มเติมได้

migration ดัวย Drizzle Kit

Ref: https://orm.drizzle.team/docs/kit-overview

นอกเหนือจาก Drizzle สามารถเป็น ORM ที่เชื่อม object กับ table ได้แล้ว ตัว Drizzle เองก็ได้เตรียม library สำหรับทำ migration เอาไว้เช่นกัน

  • การทำ Migration ใน Drizzle (และใน ORM อื่นๆ) คือกระบวนการจัดการกับการเปลี่ยนแปลงโครงสร้างฐานข้อมูลภายใน project เช่น การสร้างตารางใหม่ การเพิ่มหรือลบ column การแก้ไข schema หรือการอัปเดต constraint ต่างๆ ของฐานข้อมูล
  • โดยมีข้อดีคือ สามารถควบคุมการเปลี่ยนแปลงได้, สามารถย้อนกลับได้ในกรณีที่มีปัญหาเกิดขึ้น รวมถึงลดความผิดพลาดการสร้าง Database จาก query SQL

โดยสำหรับ Drizzle สามารถทำได้ผ่าน library drizzle-kit

  • drizzle-kit คือเครื่องมือสำหรับการจัดการ Migration ใน Drizzle ORM โดยเฉพาะ ซึ่งช่วยให้การสร้างและจัดการ migration ในฐานข้อมูลง่ายขึ้น โดย drizzle-kit รองรับการทำงานกับหลายๆ ระบบฐานข้อมูล เช่น PostgreSQL, MySQL, SQLite และอื่นๆ

เราสามารถเริ่มใช้งาน drizzle-kit ได้ผ่านการลง library ด้วย npm

Terminal window
npm i drizzle-kit

หลังจากลง library เรียบร้อย drizzle-kit จะทำการอ่านค่าผ่าน drizzle.config.ts ที่อยู่ใน root project ทำการสร้าง file config drizzle-kit ขึ้นมา

// นำเข้า type Config จาก drizzle-kit เพื่อใช้ในการกำหนดรูปแบบของการตั้งค่าการเชื่อมต่อฐานข้อมูล
import type { Config } from 'drizzle-kit'
export default {
// ระบุไฟล์ schema ที่ใช้กำหนดโครงสร้างฐานข้อมูล (ในที่นี้คือ schema.ts ในโฟลเดอร์ db)
schema: './db/schema.ts',
// ระบุโฟลเดอร์ที่ไฟล์ migration จะถูกสร้างและจัดเก็บ (ในที่นี้คือโฟลเดอร์ drizzle)
out: './drizzle',
// ระบุประเภทของฐานข้อมูลที่ใช้ (ในที่นี้คือ postgresql)
dialect: 'postgresql'
} satisfies Config // ทำให้แน่ใจว่า object นี้ตรงกับรูปแบบที่กำหนดใน type Config ของ drizzle-kit

โดยท่าที่เราจะลองแบบง่ายที่สุดคือ เราจะทำการสร้าง script migration เป็น SQL โดยอ่านผ่าน schema ออกมา โดยตัว drizzle-kit สามารถทำได้ผ่านคำสั่ง

npx drizzle-kit generate

ผลลัพธ์ที่ได้ ก็จะได้ผลลัพธ์ที่ folder drizzle ตามที่ระบุใน config เมื่อเปิดออกมา เราก็จะเจอ sql file ที่สามารถนำไป run migration ได้

drizzle-05.webp

หน้าตาตัวอย่างไฟล์ drizzle/0000_naive_ronan.sql ที่ใช้สำหรับการนำไป run migration

CREATE TABLE IF NOT EXISTS "posts" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar(200) NOT NULL,
"content" text NOT NULL,
"user_id" integer,
"created_at" timestamp DEFAULT now()
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(100) NOT NULL,
"email" varchar(100) NOT NULL,
"created_at" timestamp DEFAULT now(),
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

ทีนี้เราจะลองทำ migration script นี้ไป run กับ PostgreSQL จริงๆ

  • เพื่อการทดสอบที่ง่ายของเรา เราจะขอลบ table ทั้งหมดที่เคยสร้างก่อนหน้านี้ทิ้งทั้งหมด (ก่อน run command นี้ เราจะไม่มี table อะไรใน database)
  • เพิ่ม config connection ของ postgreSQL ใน drizzle.config.ts ผ่าน dbCredentials.url
import type { Config } from 'drizzle-kit'
export default {
schema: './db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: 'postgresql://myuser:mypassword@localhost:5432/mydatabase',
},
} satisfies Config

เสร็จแล้ว ทำการ run command นี้เพื่อทำการ run script SQL migrate ของ Drizzle

Terminal window
npx drizzle-kit migrate

ผลลัพธ์ก็จะขึ้นมาใน command ว่า migrate แล้วเรียบร้อย

drizzle-09.webp

และเมื่อมาดูผลลัพธ์ที่ DB ก็จะเจอว่าได้ table users, posts ที่หน้าตาตาม schema ออกมา

drizzle-07.webp

เพิ่มเติม วิธีอื่นๆ ที่สามารถทำได้ ในกรณีที่เราไม่ได้ควบคุม database ด้วย migration เราสามารถ run command update schema โดยตรงผ่าน command push ของ drizzle kit ได้เช่นกัน ก็จะเป็นการ update database โดยตรงโดยอ่านจาก schema ของ drizzle

Terminal window
npx drizzle-kit push

ผลลัพธ์ command หลังจากใช้คำสั่ง Push (สามารถทดลองโดยการลองลบ table ทั้งหมดออกได้เช่นกัน)

drizzle-06.webp

รวมถึงตัว command มีการป้องกัน push ซ้ำโดยการเช็คจาก table ใน database ให้ด้วยเช่นกัน

drizzle-08.webp

วิธีนี้จะเป็นที่นิยมกับคนที่นำไปพัฒนากับ system ที่ไม่ได้มีการควบคุม migration ตั้งแต่แรก แต่หากเริ่ม system ใหม่ ก็ขอแนะนำให้ใช้วิธี migration เพื่อให้สามารถควบคุม version ของ database ไว้ได้ด้วยนะครับ

สรุปทั้งหมด

จากบทความนีเราได้พูดถึงการใช้งาน Next.js กับ Drizzle ORM โดยได้ example ขั้นตอน

  • การตั้งค่าเชื่อมต่อกับฐานข้อมูล PostgreSQL ตั้งแต่การสร้างโครงสร้างตาราง (schema)
  • การเขียน API สำหรับทำ CRUD ในระบบ
  • เราได้ใช้ Drizzle ORM เพื่อช่วยในการจัดการฐานข้อมูล โดยไม่ต้องเขียน SQL ดิบๆ แต่สามารถใช้ TypeScript ในการสร้างและตรวจสอบ code ได้ ทำให้เกิดความง่ายและปลอดภัยในการทำงานมากขึ้น
  • รวมถึงมีการเชื่อมความสัมพันธ์ของข้อมูลในตารางด้วย Relation ซึ่งช่วยในการดึงข้อมูลแบบมีโครงสร้างและความสัมพันธ์ระหว่างตารางออกมาได้

ในส่วนสุดท้าย เราได้ทดลองใช้ Drizzle Kit ในการจัดการ Migration ของฐานข้อมูล ซึ่งเป็นกระบวนการสำคัญในการควบคุมการเปลี่ยนแปลงโครงสร้างของฐานข้อมูลใน project ต่างๆ โดยการใช้ Migration จะช่วยให้การเปลี่ยนแปลงต่างๆ ของฐานข้อมูลมีความตรงไปตรงมาและเรียบร้อยมากขึ้น รวมถึงสามารถย้อนกลับได้หากมีปัญหา ซึ่งถือเป็นเครื่องมือที่มีประโยชน์ในการพัฒนาระบบใหญ่ที่มีการ update บ่อยๆได้

หวังว่าบทความนี้จะช่วยทำให้เพื่อนๆรู้จัก Drizzle ORM มากขึ้นนะครับ 😁


Related Post

Share on social media