รู้จักกับ Drizzle ORM ผ่าน Next.js
/ 14 min read
สามารถดู 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 คือ
- Typed SQL: รองรับการสร้างและจัดการ query SQL ในแบบที่มีการตรวจสอบ Type เมื่อใช้กับ TypeScript
- Schema Generation: สามารถสร้าง schema ของตารางในฐานข้อมูลโดยใช้ JavaScript/TypeScript และตรวจสอบการเปลี่ยนแปลง (migrations) ได้ง่าย
- Lightweight: ออกแบบมาให้มีขนาดเล็ก
- Compatible with Multiple Databases: รองรับหลายฐานข้อมูล เช่น PostgreSQL, MySQL, SQLite
จุดที่ทำให้หลายๆคนชอบความเป็น Drizzle คือ
- คำสั่งของ library มีคล้ายๆกับ SQL สามารถใช้ idea เดียวกันเวลา query map กลับมา ORM ได้ไม่ยาก
- แยกส่วนระหว่าง ORM และ migration (แยก library สำหรับทำ migration ไว้) ทำให้ Drizzle มีขนาดที่ไม่ใหญ่มาก (Lightweight)
- ใช้ร่วมกับ 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
npx create-next-app@latest
แล้วเลือกตัวเลือกตามนี้

เมื่อเรียบร้อย เราก็จะได้ 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
เมื่อเสร็จเรียบร้อย ให้ลอง SELECT query ออกมาเพื่อดูผลลัพธ์การเพิ่ม

Step ต่อมา เราจะลองเอา Next.js ต่อเข้า database ตัวนี้ผ่าน Drizzle กัน
ลง Drizzle ลง Next.js
เริ่มต้น สิ่งที่เราจะต้องทำคือ ลง Drizzle library เข้าสู่ project สามารถลงได้ด้วย command
npm install drizzle-orm pgnpm i --save-dev @types/pg
โดย library แต่ละตัวนั้น
drizzle-orm
เป็น library หลักของ Drizzle สำหรับจัดการ ORMpg
เป็น 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 สำหรับ PostgreSQLimport { 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-ormimport { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core'
// กำหนดโครงสร้างตาราง 'users' ในฐานข้อมูล PostgreSQLexport 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

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 เพื่อกำหนดโครงสร้างของข้อมูลที่ต้องการรับเมื่อสร้าง userinterface 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/usersexport 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 userinterface 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
เอาไว้ มีความสัมพันธ์กันประมาณนี้
ทำการ 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 สำหรับ PostgreSQLimport { 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 ของตาราง postsimport { db } from '@/db'import { posts } from '@/db/schema'
// กำหนด interface CreatePostRequest เพื่ออธิบายรูปแบบข้อมูลที่ใช้ในการสร้าง postinterface 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 ได้แล้วเรียบร้อย

Step ต่อมาเพิ่ม API GET /api/posts/:id
ที่จะดึงข้อมูล post รายอันออกมาได้ โดยจะต้องทำการสร้าง file app/api/posts/[id]/route.ts
ใหม่ขึ้นมาเนื่องจากเป็นการเพิ่ม path ใหม่เข้ามา
- ทีนี้ สิ่งที่เราอยากได้ผ่าน API นี้ด้วยคือข้อมูล user ของคนสร้าง post โดยจะต้องนำข้อมูล
user_id
ทำการ join กลับเข้า tableuser
เพื่อเอาข้อมูล user ออกมา - ใน Drizzle ได้มีคำสั่ง
.join()
ที่สามารถเชื่อมข้อมูลผ่าน Foreign key ไว้ได้เลย (หากมีการระบุ relation ผ่าน schema ไว้แล้วเรียบร้อย)
// นำเข้า db object ที่เชื่อมต่อกับฐานข้อมูล, schema ของตาราง posts และ users และ function eq จาก Drizzle ORMimport { db } from '@/db'import { posts, users } from '@/db/schema'import { eq } from 'drizzle-orm'
// function GET สำหรับดึงข้อมูล post path GET /api/posts/:idexport 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 ออกมา

โดยฝั่งของ 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 }) }}
ผลลัพธ์

และนี่ก็คือ 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
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/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-breakpointCREATE 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-breakpointDO $$ 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
npx drizzle-kit migrate
ผลลัพธ์ก็จะขึ้นมาใน command ว่า migrate แล้วเรียบร้อย

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

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

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

วิธีนี้จะเป็นที่นิยมกับคนที่นำไปพัฒนากับ 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 มากขึ้นนะครับ 😁
- ทำเว็บ Blog ด้วย Next.js และ Strapiมี Video
ภาคต่อจาก Next.js เราจะลองนำ Next.js มาสร้างเว็บ Content จริงๆกันผ่าน Strapi
- รู้จักกับ Web Vitals guideline การสร้าง UX ที่ดีออกมากันมี Video
รู้จักกับคำศัพท์พื้นฐานของ Web Vitals และ use case ต่างๆของ Web Vitals กัน
- Astro และ Static site generatorมี Video
มารู้จักกับ Astro Framework สำหรับทำเว็บ Static สำหรับเว็บทำ Content โดยเฉพาะกัน
- มารู้จักกับ SQL Transaction กันว่ามันคืออะไร ?มี Video
มารู้จักเรื่องราวของการทำ Transaction และ Deadlock ผ่าน SQL กันว่ามันคืออะไร