Menu Close

TypeScript : ทำ Pagination ง่ายๆ ด้วย Prisma

บทความนี้จะพาไปรู้จักกับไลบรารี่ที่ช่วยในเรื่องของการเขียนโปรแกรมต่อฐานข้อมูล ชื่อ 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
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

Prisma Studio – ตาราง User

7. การทำ Pagination

เวลาเราต้องการแบ่งข้อมูลออกเป็นส่วนๆ ในเวลาที่แสดงผล เพื่อลดเวลาในการดึงข้อมูลและเพิ่มประสิทธิภาพโปรแกรม

ใน Prisma ก็มีฟังก์ชันที่ช่วยในการทำ Pagnation ให้อยู่ 2 แบบ คือ

  1. แบบทั่วไป ที่ใช้ skip และ take
  2. แบบที่ใช้ 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

อ้างอิง

  1. https://www.prisma.io/docs/getting-started/quickstart
  2. https://www.prisma.io/docs/concepts/components/prisma-client/pagination
Posted in typescript

ใส่ความเห็น