📘 Booklab Workshop

Day 2

PostgreSQL + Prisma — ให้ API ดึงข้อมูลจากฐานข้อมูลจริง

เวลา: 120 นาที จบคาบนี้จะได้: http://localhost:3000/api/books คืนข้อมูลจากฐานข้อมูลจริง ไม่ใช่ array ในโค้ด และค้นหาได้


ช่วงที่ 1 — ติดตั้ง PostgreSQL (25 นาที)

🗺️ ตำแหน่งตอนนี้ในระบบ

  • ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
  • ⚪ Angular (หน้าบ้าน) (ยังไม่ถึง)
  • ✅ NestJS (หลังบ้าน) (ทำไปแล้ว)
  • 🟢 PostgreSQL (ฐานข้อมูล) ← กำลังทำช่วงนี้

ต่อไป: ตั้งค่า Prisma เพื่อเชื่อมหลังบ้านเข้ากับฐานข้อมูล

1.1 ติดตั้ง PostgreSQL

ทำไมต้องมี: เป็น DBMS (ระบบจัดการฐานข้อมูล) ที่ทำงานเป็น server คือเป็นโปรแกรมที่รันค้างอยู่เบื้องหลังตลอดเวลา คอยรอรับคำสั่งจากโปรแกรมอื่น ต่างจากไฟล์ Excel ตรงที่หลายโปรแกรมเข้ามาอ่าน-เขียนพร้อมกันได้โดยข้อมูลไม่พัง

  1. เปิดเบราว์เซอร์ไปที่ enterprisedb.com/downloads/postgres-postgresql-downloads (เว็บทางการที่แจกตัวติดตั้ง Windows)
  2. เลือกดาวน์โหลด PostgreSQL 17 สำหรับ Windows x86-64
  3. ไฟล์ที่ได้จะชื่อประมาณ postgresql-17.x-x-windows-x64.exe
  4. ดับเบิลคลิกเพื่อเริ่มติดตั้ง (ถ้า Windows ถามสิทธิ์ admin ให้กด Yes)

ระหว่างติดตั้ง จะเจอหน้าจอเรียงกันแบบนี้:

หน้าจอ ต้องทำอะไร
Setup Wizard เปิดมา กด Next
Installation Directory ปล่อยค่า default (C:\Program Files\PostgreSQL\17) กด Next
Select Components ปล่อยติ๊กไว้ทุกช่อง ทั้ง PostgreSQL Server, pgAdmin 4, Stack Builder, Command Line Tools — ใช้ครบทุกตัว กด Next
Data Directory ปล่อยค่า default กด Next
Password ใส่ postgres แล้วจำให้ได้ (ทั้งห้องใช้รหัสเดียวกันเพื่อไม่ให้สับสนตอนตั้งค่าคาบหน้า) พิมพ์ยืนยันอีกครั้งในช่องที่สอง
Port ปล่อยเป็นค่า default 5432
Advanced Options (Locale) ปล่อยค่า default กด Next
Pre Installation Summary ตรวจผ่าน ๆ แล้วกด Next
Ready to Install กด Next เพื่อเริ่มติดตั้งจริง — ใช้เวลาไม่กี่นาที
จบการติดตั้ง จะมีกล่องถามว่าจะเปิด Stack Builder ต่อไหม ให้เอาเครื่องหมายถูกออก แล้วกด Finish (Stack Builder ไว้โหลดเครื่องมือเสริมที่วิชานี้ไม่ได้ใช้ เปิดทิ้งไว้จะเสียเวลาเปล่า)

💡 port 5432 = ช่องทางเข้าของ PostgreSQL คนละช่องกับหลังบ้านเรา (3000)

⚠️ ถ้าติดตั้งค้างหรือขึ้น error "database cluster installation failed" บางครั้งเกิดจากโปรแกรมป้องกันไวรัสของเครื่อง (เช่น Windows Defender) เข้าไปสแกนขณะกำลังสร้างไฟล์ฐานข้อมูล ลองปิดโปรแกรมป้องกันไวรัสชั่วคราวแล้วรันตัวติดตั้งใหม่อีกครั้ง หรือแจ้งอาจารย์เพื่อขอสิทธิ์ยกเว้นโฟลเดอร์ C:\Program Files\PostgreSQL

pgAdmin 4 จะติดตั้งมาให้พร้อมกันในขั้นตอนเดียวกัน (อยู่ในช่อง Select Components ที่ปล่อยติ๊กไว้) เป็นหน้าจอสำหรับดูและจัดการฐานข้อมูล ไม่ต้องไปหาโหลดแยก

1.2 สร้างฐานข้อมูล

เปิดโปรแกรม pgAdmin 4 จากเมนู Start (ครั้งแรกที่เปิดจะขอให้ตั้ง Master Password ของตัวโปรแกรมเอง — ตั้งอะไรก็ได้ จำไว้ใช้เฉพาะตอนเปิด pgAdmin ไม่เกี่ยวกับรหัสฐานข้อมูล)

จากนั้น: คลิกซ้ายที่ Servers → PostgreSQL 17 ครั้งแรกจะถามรหัสผ่าน ให้ใส่ postgres ที่ตั้งไว้ตอนติดตั้ง → คลิกขวาที่ DatabasesCreateDatabase

ตั้งชื่อว่า booklab แล้วกด Save

💡 server / database / table ต่างกันอย่างไร

  • server = ตัวอาคาร มีหนึ่งตัวบนเครื่องเรา
  • database = ห้องในอาคาร แยกโปรเจกต์กัน โปรเจกต์นี้ใช้ห้อง booklab
  • table = ตู้เอกสารในห้อง เก็บข้อมูลชนิดเดียวกัน เช่นตู้หนังสือ ตู้หมวดหมู่

✅ ต้องเห็น booklab โผล่ในรายการ Databases ทางซ้าย


ช่วงที่ 2 — ตั้งค่า Prisma (30 นาที)

🗺️ ตำแหน่งตอนนี้ในระบบ

  • ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
  • ⚪ Angular (หน้าบ้าน) (ยังไม่ถึง)
  • ✅ NestJS (หลังบ้าน) (ทำไปแล้ว)
  • 🟢 PostgreSQL (ฐานข้อมูล) ← กำลังทำช่วงนี้

ต่อไป: ใส่ข้อมูลตั้งต้นลงฐานข้อมูล

2.1 ติดตั้งและเริ่มต้น Prisma

powershell
cd C:\webdev\booklab
npm install prisma@6 @prisma/client@6 --workspace api
cd apps\api
npm exec -- prisma init --datasource-provider postgresql

⚠️ ทำไมต้องระบุเลข @6 ทุกครั้ง Prisma มีเวอร์ชัน 7 ออกมาแล้วซึ่งเปลี่ยนวิธีตั้งค่าไปมาก (ย้ายไฟล์ตั้งค่าฐานข้อมูลไปที่อื่น ใช้งานยากขึ้น และหาตัวอย่างสอนบนอินเทอร์เน็ตยากกว่าเวอร์ชัน 6 มาก) ถ้าไม่ระบุเลขเวอร์ชัน npm exec จะไปดึงเวอร์ชันล่าสุดมาให้เสมอ ซึ่งจะทำให้ขั้นตอนในใบงานนี้ใช้ไม่ได้เลย ต้องพิมพ์ @6 ต่อท้ายทุกครั้งที่ติดตั้ง Prisma ตลอดทั้งวิชานี้

💡 Prisma คืออะไร — และทำไมต้องมี ฐานข้อมูลพูดภาษา SQL ส่วนโค้ดเราพูดภาษา TypeScript Prisma คือล่ามที่แปลให้สองฝั่งคุยกันรู้เรื่อง

เครื่องมือประเภทนี้เรียกว่า ORM ประโยชน์คือเราเขียน findMany() แทนที่จะต่อประโยค SQL เป็นข้อความเอง ซึ่งนอกจากอ่านง่ายกว่าแล้ว ยังกันคนแอบยัดคำสั่งอันตรายเข้ามาทางช่องกรอกข้อมูลได้ด้วย (จะอธิบายเรื่องนี้ละเอียดในคาบที่ 6)

✅ ต้องเกิดไฟล์ prisma/schema.prisma และ .env

✅ ตรวจสอบเวอร์ชันให้ชัวร์

powershell
npm exec -- prisma --version

ต้องเห็นเลข 6.x.x ถ้าเห็น 7.x.x แปลว่าขั้นตอนก่อนหน้าพลาด ให้กลับไปทำข้อ 2.1 ใหม่ตั้งแต่บรรทัดแรก

2.2 ใส่รหัสฐานข้อมูล

เปิดไฟล์ apps/api/.env แก้เป็น

text
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/booklab?schema=public"

💡 อ่าน connection string นี้ยังไง

text
postgresql://[ผู้ใช้]:[รหัสผ่าน]@[เครื่อง]:[port]/[ชื่อ database]

💡 ทำไมต้องอยู่ในไฟล์ .env แยกต่างหาก เพราะมันมีรหัสผ่าน ถ้าเขียนปนไว้ในโค้ด แล้ววันหนึ่งเผลอส่งไฟล์นั้นให้คนอื่น (เช่น ส่งให้เพื่อนช่วยดู หรือแชร์โปรเจกต์ให้ใครดู) เท่ากับแจกรหัสฐานข้อมูลไปด้วย การแยกรหัสไว้ไฟล์เดียวทำให้จำง่ายว่า ไฟล์นี้ไฟล์เดียวที่ห้ามแชร์ให้ใคร

ประโยชน์อีกข้อคือเวลาย้ายไปรันที่อื่น เช่นฐานข้อมูลบนคลาวด์ เราแก้แค่บรรทัดเดียว ไม่ต้องแก้โค้ดเลย

2.3 ออกแบบตาราง

ก่อนลงมือเขียนโค้ด มาวาด ER Diagram (Entity-Relationship Diagram) ให้เห็นภาพความสัมพันธ์ก่อน

ER Diagram Category และ Book

💡 อ่านแผนผังนี้ยังไง

  • แต่ละกล่องคือหนึ่งตาราง (Entity) ข้างในคือช่อง (column) ของตารางนั้น
  • เส้นที่ลากเชื่อมสองกล่องคือ ความสัมพันธ์ (Relationship) เลข 1 กับ (มาก) ที่ปลายเส้นบอกว่าฝั่งไหนมีได้กี่แถว — ฝั่ง Category มีได้ 1 แถวต่อการเชื่อมหนึ่งครั้ง ฝั่ง Book มีได้ หลายแถว (∞)
  • PK = primary key, FK = foreign key, UNIQUE = ห้ามซ้ำ (ทบทวนความหมายได้จากการบ้านคาบที่แล้ว)

วาด ER diagram แบบนี้ก่อนเขียนโค้ดเสมอ จะช่วยให้เห็นภาพรวมความสัมพันธ์ก่อนลงรายละเอียด และเป็นขั้นตอนมาตรฐานที่ใช้ในงานจริงเวลาออกแบบฐานข้อมูล

ลองสร้างตารางแรกด้วยมือผ่าน pgAdmin ก่อน

ก่อนจะให้ Prisma ทำงานแทนทั้งหมด ลองสร้างตาราง Category (ตารางแรกจากแผนผังข้างบน) ด้วยมือผ่านหน้าจอ pgAdmin ดูสักครั้ง จะได้เห็นภาพว่าเบื้องหลัง Prisma ไปทำอะไรให้เราบ้าง

ใน pgAdmin คลิกขวาที่ Schemas → public → Tables เลือก Create → Table

ช่อง ใส่ค่า
แท็บ General → Name Category
แท็บ Columns → กด + เพิ่มช่องที่ 1 Name: id, Data type: integer, ติ๊ก Primary Key ติ๊ก Identity (ให้ฐานข้อมูลนับเลขให้เอง)
แท็บ Columns → กด + เพิ่มช่องที่ 2 Name: name, Data type: character varying, ติ๊ก Not Null, ติ๊ก Unique

กด Save (ไอคอนรูปแผ่นดิสก์) แล้ว refresh ที่ Tables จะเห็นตาราง Category เกิดขึ้นจริง

💡 สังเกตว่าขั้นตอนนี้ทำเองทีละคลิกล้วนๆ ไม่มีที่เก็บว่าใครสร้างตารางนี้เมื่อไหร่ ด้วยเหตุผลอะไร และถ้าจะสร้างตาราง Book ต่อ (ที่มี foreign key โยงกลับมาที่ Category แบบในแผนผัง) ก็ต้องตั้งค่าความสัมพันธ์เองอีกหลายคลิก แถมเพื่อนที่ clone โปรเจกต์นี้ไปต้องมานั่งคลิกสร้างซ้ำเป๊ะๆ ทุกขั้นตอนเอง

ลบตารางทดลองนี้ทิ้งก่อนไปต่อ — คลิกขวาที่ CategoryDelete/Drop เพราะขั้นตอนถัดไปเราจะให้ Prisma สร้างให้เองทั้งหมด (รวม Book ด้วย) จากไฟล์เดียว พร้อมเก็บประวัติการเปลี่ยนแปลงให้อัตโนมัติ — นี่คือเหตุผลที่โลกจริงใช้ ORM/migration tool แทนการคลิกสร้างตารางเองแบบที่เพิ่งทำ

จากแผนผังนี้ แปลงเป็นไฟล์ schema.prisma ได้ตรงๆ — แทนที่ทั้งไฟล์ apps/api/prisma/schema.prisma

prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Category {
  id    Int    @id @default(autoincrement())
  name  String @unique
  books Book[]
}

model Book {
  id         Int      @id @default(autoincrement())
  title      String
  author     String
  year       Int
  categoryId Int
  category   Category @relation(fields: [categoryId], references: [id])
}

💡 ไฟล์นี้คือแปลนบ้าน กระดาษแผ่นเดียวที่วาดไว้ว่าฐานข้อมูลมีตารางอะไร แต่ละตารางมีช่องอะไร และเชื่อมกันตรงไหน

💡 อ่านทีละบรรทัด

  • model Category = ตารางชื่อ Category
  • @id = ช่องนี้คือ primary key ตัวระบุแถวที่ไม่ซ้ำกัน (ที่ทบทวนมาจากวิชาฐานข้อมูล)
  • @default(autoincrement()) = ให้ฐานข้อมูลแจกเลขให้เองทีละ 1
  • @unique = ห้ามซ้ำ จะมีหมวดหมู่ชื่อเดียวกันสองอันไม่ได้
  • categoryId Int = foreign key ช่องที่เก็บว่าหนังสือเล่มนี้อยู่หมวดไหน
  • @relation(...) = บอก Prisma ว่า categoryId ชี้ไปที่ id ของตาราง Category
  • books Book[] ในฝั่ง Category = หมวดหนึ่งมีหนังสือได้หลายเล่ม

ทั้งหมดนี้คือความสัมพันธ์แบบ one-to-many — หนึ่งหมวดหมู่ ต่อ หลายเล่ม

2.4 สร้างตารางจริงในฐานข้อมูล

powershell
npm exec -- prisma migrate dev --name init

💡 migration คืออะไร คือบันทึกการต่อเติม ครั้งแรกสร้างตาราง ครั้งต่อไปเพิ่มช่อง เก็บไว้เป็นลำดับ เหมือน commit ของฐานข้อมูล

ประโยชน์คือเพื่อนที่ clone โปรเจกต์ไป รันคำสั่งเดียวก็ได้โครงสร้างตารางเหมือนเราเป๊ะ ไม่ต้องมานั่งสร้างตารางเองทีละช่อง

✅ ตรวจสอบ: กลับไปที่ pgAdmin → กด refresh ที่ booklab → เข้า Schemas → public → Tables ต้องเห็นตาราง Book และ Category

ลองคลิกขวาที่ตาราง BookPropertiesColumns ดูว่าช่องที่เราเขียนใน schema กลายเป็นคอลัมน์จริงครบไหม


ช่วงที่ 3 — ใส่ข้อมูลตั้งต้น (25 นาที)

🗺️ ตำแหน่งตอนนี้ในระบบ

  • ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
  • ⚪ Angular (หน้าบ้าน) (ยังไม่ถึง)
  • ✅ NestJS (หลังบ้าน) (ทำไปแล้ว)
  • 🟢 PostgreSQL (ฐานข้อมูล) ← กำลังทำช่วงนี้

ต่อไป: แก้ API ให้ดึงข้อมูลจากฐานข้อมูลจริงแทน array

3.1 เขียนสคริปต์ seed

สร้างไฟล์ apps/api/prisma/seed.ts

typescript
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  await prisma.book.deleteMany();
  await prisma.category.deleteMany();

  const programming = await prisma.category.create({
    data: { name: 'Programming' },
  });
  const design = await prisma.category.create({
    data: { name: 'Design' },
  });

  await prisma.book.createMany({
    data: [
      { title: 'Clean Code', author: 'Robert C. Martin', year: 2008, categoryId: programming.id },
      { title: 'The Pragmatic Programmer', author: 'Andy Hunt', year: 1999, categoryId: programming.id },
      { title: 'Refactoring', author: 'Martin Fowler', year: 1999, categoryId: programming.id },
      { title: 'The Design of Everyday Things', author: 'Don Norman', year: 1988, categoryId: design.id },
      { title: "Don't Make Me Think", author: 'Steve Krug', year: 2000, categoryId: design.id },
    ],
  });

  console.log('เพิ่มข้อมูลตั้งต้นเรียบร้อย');
}

main().finally(() => prisma.$disconnect());

💡 seed = ของตั้งต้นที่ใส่ไว้ให้ เหมือนบ้านตัวอย่างที่มีเฟอร์นิเจอร์วางแล้ว จะได้ไม่ต้องเปิดมาเจอห้องว่าง

💡 PrismaClient = ตัวล่ามที่เราเรียกใช้จริงในโค้ด 💡 deleteMany() สองบรรทัดแรก = ล้างของเก่าก่อน เพื่อให้รันซ้ำกี่ครั้งก็ได้ผลเหมือนเดิม 💡 ต้องสร้าง category ก่อน book เพราะหนังสือต้องมี categoryId ที่มีอยู่จริง ไม่งั้นฐานข้อมูลจะปฏิเสธ — นี่คือ foreign key ทำงาน

3.2 บอก Prisma ว่าไฟล์ seed อยู่ตรงไหน

เปิดไฟล์ apps/api/package.json เพิ่มช่องนี้ (วางไว้ระดับเดียวกับ "scripts" หรือ "dependencies" ก็ได้ ไม่ต้องอยู่ในนั้น)

json
"prisma": {
  "seed": "ts-node prisma/seed.ts"
}

⚠️ ห้ามข้ามขั้นตอนนี้ ถ้าไม่ใส่ npm exec -- prisma db seed ในขั้นถัดไปจะรันจบแบบไม่มี error และไม่มี output ใด ๆ เลย (เงียบสนิท) ทำให้เข้าใจผิดว่าใส่ข้อมูลสำเร็จทั้งที่ตารางยังว่างอยู่ — สาเหตุคือ Prisma ไม่รู้ว่าไฟล์ seed อยู่ที่ไหนถ้าไม่ได้ระบุไว้ตรงนี้

💡 ts-node = ตัวรัน TypeScript ตรง ๆ โดยไม่ต้อง compile เป็น JavaScript ก่อน ติดมาให้แล้วตอนสร้างโปรเจกต์ NestJS ในคาบ 1 ไม่ต้องติดตั้งเพิ่ม

3.3 รัน seed

powershell
npm exec -- prisma db seed

✅ ต้องเห็น เพิ่มข้อมูลตั้งต้นเรียบร้อย (อาจมี warning สีเหลืองเรื่อง package.json#prisma deprecated ปนมาด้วย — ไม่เป็นไร ข้ามได้ ยังใช้งานได้ปกติ)

3.4 ดูข้อมูลด้วย Prisma Studio

เปิด Terminal ใหม่

powershell
cd C:\webdev\booklab\apps\api
npm exec -- prisma studio

เบราว์เซอร์จะเปิดที่ localhost:5555

Prisma Studio แสดงตาราง Book แบบตารางคล้าย Excel

💡 Prisma Studio = หน้าจอดูข้อมูลแบบตาราง คล้าย Excel แก้ค่าได้ทันที และกดที่ช่อง category แล้วเด้งไปดูหมวดหมู่ที่เชื่อมกันได้ ซึ่งทำให้เห็นภาพ relation ชัดกว่าดูใน pgAdmin

ตั้งแต่คาบนี้ไป เวลาสงสัยว่า "ทำไมหน้าเว็บว่าง" ให้เปิดตัวนี้ดูก่อนเสมอ จะรู้ใน 3 วินาทีว่าไม่มีข้อมูล หรือโค้ดผิด

⚠️ Terminal นี้จะถูกยึดไว้ ถ้าจะพิมพ์คำสั่งอื่นให้เปิดอันใหม่

3.5 ลองเขียน SQL เอง

กลับไปที่ pgAdmin → คลิกขวาที่ booklabQuery Tool แล้วลองสองคำสั่งนี้

sql
SELECT * FROM "Book";
sql
SELECT b.title, b.year, c.name AS category
FROM "Book" b
JOIN "Category" c ON b."categoryId" = c.id
ORDER BY b.year;

💡 นี่คือสิ่งที่ Prisma แปลให้เราตลอดเวลา คำสั่งที่สองคือ JOIN ที่เรียนมาในวิชาฐานข้อมูล — ดึงข้อมูลจากสองตารางมาต่อกันโดยใช้ foreign key เป็นตัวเชื่อม

ต่อไปเราจะไม่เขียน SQL เองแล้ว แต่ต้องรู้ว่าเบื้องหลังมันคือสิ่งนี้

⚠️ ต้องใส่เครื่องหมาย " ครอบชื่อตาราง เพราะ Prisma ตั้งชื่อตารางด้วยตัวพิมพ์ใหญ่ ถ้าไม่ใส่ PostgreSQL จะมองเป็นตัวพิมพ์เล็กแล้วหาไม่เจอ


ช่วงที่ 4 — ต่อ API เข้ากับฐานข้อมูล (35 นาที)

🗺️ ตำแหน่งตอนนี้ในระบบ

  • ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
  • ⚪ Angular (หน้าบ้าน) (ยังไม่ถึง)
  • 🟢 NestJS (หลังบ้าน) ← กำลังทำช่วงนี้
  • 🟢 PostgreSQL (ฐานข้อมูล) ← กำลังทำช่วงนี้

ต่อไป: คาบหน้า: เริ่มสร้างหน้าบ้านด้วย Angular

4.1 สร้างตัวเชื่อม Prisma เข้ากับ NestJS

powershell
cd apps\api
npm exec -- nest g service prisma --no-spec --flat --skip-import

💡 ทำไมต้องใส่ flag เพิ่ม

  • --flat = สร้างไฟล์ไว้ที่ src/prisma.service.ts ตรง ๆ ไม่สร้างโฟลเดอร์ prisma/ มาครอบ (ต่างจาก books ที่อยากได้โฟลเดอร์แยก)
  • --skip-import = ตัวนี้ไม่ผูกกับแผนกไหนแผนกหนึ่งโดยเฉพาะ จะเอาไปใช้ในแผนกหนังสือเอง ไม่ต้องให้ CLI เอาไปลงทะเบียนใน app.module.ts ให้อัตโนมัติ

✅ ต้องเห็น ไฟล์ apps/api/src/prisma.service.ts เกิดขึ้น เปิดดูจะเป็นโครงเปล่า ๆ แบบนี้

typescript
import { Injectable } from '@nestjs/common';

@Injectable()
export class PrismaService {}

แก้ไฟล์นี้ให้เป็นแบบนี้

typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}

💡 @Injectable() = ป้ายบอก NestJS ว่า "คลาสนี้เป็นเครื่องมือที่แจกให้ที่อื่นใช้ได้" 💡 onModuleInit = ฟังก์ชันที่ NestJS เรียกให้อัตโนมัติตอนโปรแกรมเริ่มทำงาน เราใช้จังหวะนี้เชื่อมต่อฐานข้อมูล

4.2 สร้าง Service ของหนังสือ

powershell
npm exec -- nest g service books --no-spec

💡 service ต่างจาก controller อย่างไร

  • controller = พนักงานต้อนรับ รับเรื่องแล้วส่งต่อ ไม่ทำงานหนักเอง
  • service = คนทำงานหลังบ้านจริง ๆ คนที่เดินไปหยิบของจากฐานข้อมูล

แยกกันเพราะถ้าวันหนึ่งอยากเปลี่ยนวิธีเก็บข้อมูล เราแก้แค่ service ไม่ต้องแตะ controller เลย

แทนที่ทั้งไฟล์ apps/api/src/books/books.service.ts

typescript
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';

@Injectable()
export class BooksService {
  constructor(private prisma: PrismaService) {}

  findAll(keyword?: string) {
    return this.prisma.book.findMany({
      where: keyword
        ? { title: { contains: keyword, mode: 'insensitive' } }
        : undefined,
      include: { category: true },
      orderBy: { id: 'asc' },
    });
  }

  findOne(id: number) {
    return this.prisma.book.findUnique({
      where: { id },
      include: { category: true },
    });
  }
}

💡 constructor(private prisma: PrismaService) บรรทัดนี้แปลว่า "ฉันต้องใช้ PrismaService" แล้ว NestJS จะหยิบมาส่งให้เอง เราไม่ต้องสร้างเอง

💡 อ่านคำสั่ง Prisma

  • findMany() = ขอหลายแถว (เทียบเท่า SELECT * FROM)
  • where = เงื่อนไข (เทียบเท่า WHERE)
  • contains = มีคำนี้อยู่ข้างใน (เทียบเท่า LIKE '%คำ%')
  • mode: 'insensitive' = ไม่สนตัวพิมพ์เล็กใหญ่
  • include: { category: true } = ดึงข้อมูลหมวดหมู่ที่เชื่อมกันมาด้วย (เทียบเท่า JOIN ที่เพิ่งลองใน pgAdmin)
  • findUnique() = ขอแถวเดียวด้วย primary key

4.3 ให้ module รู้จัก PrismaService

แทนที่ apps/api/src/books/books.module.ts

typescript
import { Module } from '@nestjs/common';
import { BooksController } from './books.controller';
import { BooksService } from './books.service';
import { PrismaService } from '../prisma.service';

@Module({
  controllers: [BooksController],
  providers: [BooksService, PrismaService],
})
export class BooksModule {}

💡 providers = รายชื่อเครื่องมือที่แผนกนี้ใช้ได้ ถ้าไม่ใส่ NestJS จะบอกว่าหาไม่เจอ

4.4 แก้ controller ให้เรียก service

แทนที่ apps/api/src/books/books.controller.ts

typescript
import { Controller, Get, Param, Query, ParseIntPipe } from '@nestjs/common';
import { BooksService } from './books.service';

@Controller('books')
export class BooksController {
  constructor(private booksService: BooksService) {}

  @Get()
  findAll(@Query('q') q?: string) {
    return this.booksService.findAll(q);
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.booksService.findOne(id);
  }
}

💡 @Query('q') = รับค่าจาก URL ที่ต่อท้ายด้วย ?q=... 💡 @Param('id') = รับค่าจากส่วนของ URL เช่น /api/books/3 จะได้ id = 3 💡 ParseIntPipe = ตัวแปลงค่าจากข้อความเป็นตัวเลข เพราะทุกอย่างที่มาจาก URL เป็นข้อความหมด ถ้าแปลงไม่ได้จะตีกลับเป็น error ให้อัตโนมัติ

⚠️ @Get(':id') ต้องอยู่ล่างสุดเสมอ เพราะมันรับได้ทุกอย่าง ถ้าเอาไว้บน endpoint อื่นจะถูกมันกินหมด

4.5 ทดสอบ

ดู Terminal ที่รัน npm run dev:api ว่าโหลดใหม่แล้ว จากนั้นลองทั้งสาม URL

URL ควรได้
localhost:3000/api/books หนังสือ 5 เล่ม พร้อมข้อมูล category
localhost:3000/api/books?q=clean เฉพาะ Clean Code
localhost:3000/api/books/1 หนังสือเล่มเดียว

✅ สังเกตว่าแต่ละเล่มมี "category": { "id": 1, "name": "Programming" } ติดมาด้วย — นั่นคือผลของ include ที่เราเขียนไว้

🚀 ถ้าเสร็จก่อนเพื่อน

  1. เพิ่ม endpoint /api/categories คืนรายชื่อหมวดหมู่พร้อมจำนวนหนังสือในแต่ละหมวด (คำใบ้: _count)
  2. เพิ่มหนังสือใหม่ 3 เล่มใน seed.ts แล้วรัน npm exec -- prisma db seed ใหม่
  3. ลองค้นด้วยชื่อผู้แต่งแทนชื่อหนังสือ

❓ ปัญหาที่พบบ่อย

อาการ สาเหตุ วิธีแก้
P1000 Authentication failed รหัสใน .env ผิด ตรวจว่าเป็น postgres:postgres
pgAdmin ขอรหัสไม่เหมือนที่ตั้งไว้ตอนติดตั้ง สับสนระหว่าง Master Password (ของ pgAdmin เอง) กับรหัสฐานข้อมูล รหัสฐานข้อมูลคือ postgres ใช้ตอนคลิก Server ครั้งแรกเท่านั้น
P1001 Can't reach database server PostgreSQL ไม่ได้รันอยู่ เปิด Services ของ Windows หา postgresql แล้วสั่ง Start
P2003 Foreign key constraint failed ใส่ categoryId ที่ไม่มีอยู่จริง ตรวจว่าสร้าง category ก่อน book
Cannot find module '@prisma/client' ยังไม่ได้ generate รัน npm exec -- prisma generate
แก้ schema แล้วโค้ดไม่รู้จักช่องใหม่ ยังไม่ได้ migrate รัน npm exec -- prisma migrate dev --name ...
ข้อมูลเละอยากเริ่มใหม่ npm exec -- prisma migrate reset แล้ว seed ใหม่

📌 คำสั่ง Prisma ที่ต้องจำ

คำสั่ง ใช้เมื่อ
npm exec -- prisma migrate dev --name ชื่อ แก้ schema.prisma แล้วต้องการให้ตารางจริงเปลี่ยนตาม
npm exec -- prisma db seed ใส่ข้อมูลตั้งต้นใหม่
npm exec -- prisma studio เปิดดูข้อมูล
npm exec -- prisma migrate reset ล้างทุกอย่างกลับเป็นค่าตั้งต้น

คาบหน้า เริ่มทำหน้าบ้านด้วย Angular