บทความนี้จะพาไปรู้จักกับไลบรารี่ที่ช่วยในเรื่องของการเขียนโปรแกรมต่อฐานข้อมูล ชื่อ Prisma ซึ่งไลบรารี่นี้จะช่วยให้เราเขียนโปรแกรมและจัดการเกี่ยวกับฐานข้อมูลได้ง่ายยิ่งขึ้น เนื่องจากเราไม่จำเป็นต้องใช้คำสั่ง SQL ในการจัดการข้อมูลในฐานข้อมูลเลย เพราะในการจัดการข้อมูลในฐานข้อมูลนั้นเราจะใช้ฟังก์ชันต่างๆ ของ Prisma เป็นตัวจัดการ ถ้าพร้อมแล้วเราไปเริ่มเรียนรู้การใช้งาน Prisma กันก่อน จากนั้นจะเข้าสู่ตัวอย่างการทำ Pagination ด้วย Prisma
ข้อมูลเบื้องต้น รู้ก่อนศึกษา Prisma
- Prisma รองรับทั้งภาษา JavaScript และ TypeScript ในตัวอย่างนี้จะใช้ TypeScript
- Prisma ต้องการ Node.js v14.17.0 หรือใหม่กว่า
- Prisma เป็นไลบรารี่แบบ ORM ดังนั้นเราต้องศึกษาฟังก์ชันและวิธีการกำหนดโครงสร้างข้อมูลตามที่ Prisma กำหนด
- Prisma รองรับการติดต่อฐานข้อมูล SQLite, PostgreSQL, MySQL, MongoDB ในตัวอย่างนี้จะใช้ SQLite
- Prisma จะมี Prisma Client เป็นโค้ดที่สร้างจากการกำหนดโครงสร้างในไฟล์ .prisma ทำให้โปรแกรมของเรารู้จักโครงสร้างและฟังก์ชันต่างๆ ของฐานข้อมูลที่เราสร้างขึ้น
- Prisma ยังมีข้อจำกัดในการดึงข้อมูลอยู่บ้างเช่น การทำ Full Text Search (อยู่ระหว่างเวอร์ชันทดสอบ), การ select ข้อมูลใน Group by เป็นต้น
1. สร้าง โปรเจค TypeScript และ ติดตั้ง Prisma
mkdir hello-prisma
cd hello-prisma
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init
npm install prisma --save-dev
npx prisma init --datasource-provider sqlite
2. สร้างโมเดลของฐานข้อมูล
เมื่อเราทำการกำหนดค่าเริ่มต้นของ prisma ให้กับโปรเจคของเราแล้ว prisma จะสร้างไฟล์ scheme.prisma ในโฟลเดอร์ prisma อัตโนมัติ ต่อไปเราจะมาสร้างโมเดล (ตาราง) ในฐานข้อมูลในไฟล์ scheme.prisma กัน
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
ทำการพิมพ์โมลเดล User และ Post ตามตัวอย่าง เอาไว้ที่บรรทัดล่างสุดของไฟล์ scheme.prisma
โมเดลตัวนี้จะเป็นอธิบายฟิวด์ต่างๆ ของแต่ละตารางที่เราจะออกแบบในฐานข้อมูล ในตัวอย่างนี้จะมี 2 ตาราง คือ ตาราง User และ ตาราง Post
ตาราง User จะเก็บข้อมูล id, email, name
ตาราง Post จะเก็บข้อมูล id, title, content, published, authorId
และความสัมพันธ์ระหว่างตาราง User กับ ตาราง Post คือ 1-Many หมายความว่า 1 User มี Post ได้มากกว่า 1 Post แต่ว่า Post จะต้องมี 1 User
ดังนั้นการกำหนดโมเดลใน Prisma จึงต้องระบุความสัมพันธ์เข้าไปด้วย ดังนี้
ตาราง User ต้องเพิ่ม posts Post[] เข้ามา เพื่อบอกว่า User สามารถมีได้หลาย Post
ตาราง Post ต้องเพิ่ม author User @relation(fields: [authorId], references: [id]) เพื่อบอกว่า Post นี้เป็นของ User คนไหน
ซึ่งทั้งสองฟิวด์นี้ (posts ในโมเดล User และ author ในโมเดล Post) จะไม่ปรากฏในตารางในฐานข้อมูล แต่จะเป็นความสัมพันธ์เพื่อทำให้ Prisma นั้นสร้างโค้ดและฟังก์ชันให้เราใช้งานต่อไป
3. รันสร้างฐานข้อมูล
npx prisma migrate dev --name init
เมื่อรันคำสั่งนี้แล้ว ไฟล์ฐานข้อมูลจะถูก Prisma สร้างขึ้นโดยอัตโนมัติ (ไฟล์ฐานข้อมูลจะอยู่ในโฟลเดอร์ prisma)
4. เขียนคำสั่ง typescript เพื่อทดสอบการเรียกและจัดการข้อมูลในฐานข้อมูล
touch index.ts
พิมพ์คำสั่งต่อไปนี้ในไฟล์ index.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// ... you will write your Prisma Client queries here
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
4.1 เพิ่ม User ในฟังก์ชั่น main() ดังนี้
async function main() {
const user = await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@prisma.io',
},
})
console.log(user)
}
prisma จะมีวิธีการเรียกใช้งานฟังก์ชันของโมเดลคือ prisma.[ชื่อโมเดล].[ฟังก์ชันที่ใช้งาน] จากตัวอย่างจะเป็นการสร้างข้อมูลในโมเดล user ก็จะใช้คำสั่ง prisma.user.create()
รันโปรแกรมโดยใช้คำสั่ง
npx ts-node index.ts
โปรแกรมจะแสดงผลลัพธ์ออกมาแบบนี้
{ id: 1, email: 'alice@prisma.io', name: 'Alice' }
4.2 เรียกดูข้อมูลในตาราง User
async function main() {
const users = await prisma.user.findMany()
console.log(users)
}
รันดูผลลัพธ์ ด้วยคำสั่ง npx ts-node index.ts
[{ id: 1, email: 'alice@prisma.io', name: 'Alice' }]
4.3 เพิ่มข้อมูล User และ Post ของ User นั้นเข้าไปด้วยในคำสั่งเดียวกัน
async function main() {
const user = await prisma.user.create({
data: {
name: 'Bob',
email: 'bob@prisma.io',
posts: {
create: {
title: 'Hello World',
},
},
},
})
console.log(user)
}
การเพิ่มข้อมูล user และ post พร้อมกันจะใช้คำสั่ง prisma.user.create() เช่นเดิม แต่เพิ่มข้อมูล post เข้าไปที่ posts ด้วย
จะสังเกตุได้ว่า จะมีคำสั่ง create ใน posts อีกทีเพิ่มสั่งให้เพิ่มค่า post ใหม่เข้าไป
รันดูผลลัพธ์ ด้วยคำสั่ง npx ts-node index.ts
{ id: 2, email: 'bob@prisma.io', name: 'Bob' }
4.4 เรียกดูข้อมูลข้อมูลในตาราง User โดยให้แสดง Post ด้วย
async function main() {
const usersWithPosts = await prisma.user.findMany({
include: {
posts: true,
},
})
console.dir(usersWithPosts, { depth: null })
}
ฟังก์ชั่นในการเรียกดูข้อมูลในตาราง user ใช้คำสั่ง prisma.user.findMany() และหากต้องการให้แสดงรายการของ Post ด้วยจะต้องเพิ่ม include: {posts: true} เข้าไปด้วย
ซึ่งฟิวด์ posts คือ ฟิวด์ที่เป็น relation ของโมเดล user
รันดูผลลัพธ์ ด้วยคำสั่ง npx ts-node index.ts
[
{ id: 1, email: 'alice@prisma.io', name: 'Alice', posts: [] },
{
id: 2,
email: 'bob@prisma.io',
name: 'Bob',
posts: [
{
id: 1,
title: 'Hello World',
content: null,
published: false,
authorId: 2
}
]
}
]
5. รู้จักกับเครื่องมือของ Prisma นั้นก็คือ Prisma studio
Prisma studio เป็นเครื่องมือเอาไว้สำหรับจัดการข้อมูลในฐานข้อมูล ใช้งานโดยใช้คำสั่ง
npx prisma studio
6. ทดสอบเพิ่มข้อมูล User และ Post แบบเยอะๆๆ
// create data
const userData = Array.from("ABCDEFGHIJK").map((i) => ({
name: `user-${i}`,
email: `user-${i}@email.com`,
posts: {
create: [
{ title: `Hello world by user-${i}-1` },
{ title: `Hello world by user-${i}-2` },
{ title: `Hello world by user-${i}-3` },
{ title: `Hello world by user-${i}-4` },
{ title: `Hello world by user-${i}-5` },
{ title: `Hello world by user-${i}-6` },
{ title: `Hello world by user-${i}-7` },
{ title: `Hello world by user-${i}-8` },
{ title: `Hello world by user-${i}-9` },
{ title: `Hello world by user-${i}-10` }
]
},
}))
// insert data
userData.forEach(async (u) => {
const user = await prisma.user.create({
data: u
})
console.log(user)
})
สร้างข้อมูลเตรียมไว้ในอาเรย์ก่อน แล้วจึงวนลูปอาเรย์ในการสร้างข้อมูลอีกครั้ง
รันดูผลลัพธ์ ด้วยคำสั่ง npx ts-node index.ts
{ id: 6, email: 'user-A@email.com', name: 'user-A' }
{ id: 7, email: 'user-H@email.com', name: 'user-H' }
{ id: 8, email: 'user-E@email.com', name: 'user-E' }
{ id: 9, email: 'user-K@email.com', name: 'user-K' }
{ id: 10, email: 'user-C@email.com', name: 'user-C' }
{ id: 11, email: 'user-B@email.com', name: 'user-B' }
{ id: 12, email: 'user-D@email.com', name: 'user-D' }
{ id: 13, email: 'user-I@email.com', name: 'user-I' }
{ id: 14, email: 'user-F@email.com', name: 'user-F' }
{ id: 15, email: 'user-J@email.com', name: 'user-J' }
{ id: 16, email: 'user-G@email.com', name: 'user-G' }
เมื่อดูใน prisma studio ในตาราง User จะพบว่ามี user ใหม่ตามที่เราเพิ่มเข้าไปแล้ว และแต่ละ user ใหม่ก็มี post เป็นของตนเองอีกคนละ 10 posts
7. การทำ Pagination
เวลาเราต้องการแบ่งข้อมูลออกเป็นส่วนๆ ในเวลาที่แสดงผล เพื่อลดเวลาในการดึงข้อมูลและเพิ่มประสิทธิภาพโปรแกรม
ใน Prisma ก็มีฟังก์ชันที่ช่วยในการทำ Pagnation ให้อยู่ 2 แบบ คือ
- แบบทั่วไป ที่ใช้ skip และ take
- แบบที่ใช้ cursor
7.1 การเรียกข้อมูลแบบ Pagination แบบที่ 1 (skip and take)
const results = await prisma.post.findMany({
skip: 3,
take: 4,
})
console.log(results)
คำสั่งในการเรียก pagination แบบแรก คือ prisma.[ชื่อโมเดล].findMany({skip: N, take: M})
skip = ให้ข้ามการแสดงข้อมูลไปจำนวน N รายการ
take = ให้แสดงข้อมูลต่อจากที่ skip จำนวน M รายการ ** ซึ่งค่า skip และ take ไม่จำเป็นต้องเท่ากัน
รันดูผลลัพธ์ ด้วยคำสั่ง npx ts-node index.ts
[
{
id: 4,
title: 'Hello world by user-2-3',
content: null,
published: false,
authorId: 3
},
{
id: 5,
title: 'Hello world by user-2-4',
content: null,
published: false,
authorId: 3
},
{
id: 6,
title: 'Hello world by user-2-5',
content: null,
published: false,
authorId: 3
},
{
id: 7,
title: 'Hello world by user-2-6',
content: null,
published: false,
authorId: 3
}
]
7.2 การเรียกข้อมูลแบบ Pagination แบบที่ 2 (cursor based)
const queryResults = await prisma.post.findMany({
take: 4,
skip: 1, // Skip the cursor
cursor: {
id: 3,
},
orderBy: {
id: 'asc',
},
})
console.log(queryResults)
การเรียกข้อมูลแบบ Pagination แบบที่ 2 จำเป็นต้องรู้ cursor ว่าอยู่ที่ตำแหน่งไหน โดยเราจะต้องคำนวนมาก่อน (ในตัวอย่างเป็นการกำหนด cursor ไว้ที่ id = 3) และจะต้องเรียกตามฟิวด์ที่เป็น cursor ด้วย
ส่วนการ skip: 1 เป็นข้อบังคับว่าต้องใส่เข้ามาด้วยเพื่อทำให้ผลลัพธ์ออกมาถูกต้อง
รันดูผลลัพธ์ ด้วยคำสั่ง npx ts-node index.ts
[
{
id: 4,
title: 'Hello world by user-2-3',
content: null,
published: false,
authorId: 3
},
{
id: 5,
title: 'Hello world by user-2-4',
content: null,
published: false,
authorId: 3
},
{
id: 6,
title: 'Hello world by user-2-5',
content: null,
published: false,
authorId: 3
},
{
id: 7,
title: 'Hello world by user-2-6',
content: null,
published: false,
authorId: 3
}
]
บทส่งท้าย
ในบทความนี้เราได้รู้จักกับการใช้งาน Prisma เบื้องต้นและการดึงข้อมูลแบบ Pagination ด้วย Prisma ซึ่งมีอยู่ 2 แบบ คือ
1. แบบที่คำนวนตามปกติโดยใช้ skip และ take แบบนี้จะใช้วิธีดึงข้อมูลแบบ OFFSET จากฐานข้อมูล หมายถึงฐานข้อมูลจะอ่านข้อมูลที่ skip ไปทุกรายการก่อนที่จะแสดงผลเฉพาะจำนวนรายการที่ take ซึ่งจะมีผลกระทบต่อประสิทธิภาพของโปรแกรมเมื่อมีการ skip หลายรายการเช่น skip: 200,000 รายการ เป็นต้น
2. แบบใช้ cursor แบบนี้จะช่วยให้โปรแกรมข้ามชุดข้อมูลอื่นๆ และจะทำการดึงข้อมูลต่อเมื่อมีค่า cursor เท่ากับที่กำหนดหรือมากกว่า โดยโปรแกรมจะไม่ทำการอ่านข้อมูลทุกข้อมูลก่อนหน้าเหมือนแบบแรก ซึ่งจะทำให้ประสิทธิภาพของโปรแกรมดีกว่า แต่เราจะต้องกำหนด cursor และจัดเรียงข้อมูลตามฟิวด์ของ cursor ด้วย ซึ่งการทำ pagination แบบนี้จะเหมาะกับการทำที่ ปุ่ม หน้าก่อนหน้า และ หน้าถัดไป
ในการต่อยอดใช้งาน Prisma นี้ ก็จะเป็นการใช้งานร่วมกับเฟรมเวิร์คที่เป็น React Full Stack เช่น Remix และ Next.js