TypeScript 現代化 ORM 實戰指南:Prisma 全面解析與高效操作手冊
Prisma 是一套現代化且具備完整 TypeScript 型別支援的 ORM(Object-Relational Mapping),透過自動產生 Type 定義檔,讓資料庫操作具備自動補全與型別檢查優勢。
1. 環境安裝與初始化
使用 pnpm 建立專案並安裝 Prisma CLI 與 Client:
# 初始化 TypeScript 專案
pnpm init
pnpm add typescript @types/node tsx -D
pnpm tsc --init
# 安裝 Prisma 開發依賴與 Runtime 套件
pnpm add prisma -D
pnpm add @prisma/client
# 初始化 Prisma(以 PostgreSQL 為例,預設產生 prisma/schema.prisma 與 .env)
pnpm prisma init --datasource-provider postgresql
在 .env 設定資料庫連線字串:
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
2. 定義 Schema 與資料庫關聯
編輯 prisma/schema.prisma,定義一對多(User 與 Post)以及多對多(Post 與 Tag)關聯:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
@@index([email])
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags Tag[]
createdAt DateTime @default(now())
@@index([authorId])
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
執行資料庫遷移(Migration)並生成 Client:
pnpm prisma migrate dev --name init
3. 基本 CRUD 操作
建立單一 Prisma Client 實例(建議在獨立檔案如 db.ts 中管理):
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
Create(新增)
支援單筆新增、巢狀新增(Nested Writes)與批次新增:
// 巢狀新增:建立 User 的同時建立關聯的 Post
const userWithPosts = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
posts: {
create: [
{ title: 'Prisma 入門指南', published: true },
{ title: 'TypeScript 最佳實踐', published: false },
],
},
},
include: { posts: true },
});
// 批次新增
await prisma.tag.createMany({
data: [{ name: 'TypeScript' }, { name: 'ORM' }, { name: 'Node.js' }],
skipDuplicates: true,
});
Read(查詢)
// 查詢單筆(支援 Unique 欄位)
const user = await prisma.user.findUnique({
where: { email: 'alice@example.com' },
});
// 複合條件查詢
const posts = await prisma.post.findMany({
where: {
published: true,
viewCount: { gte: 100 },
title: { contains: 'Prisma', mode: 'insensitive' },
},
select: {
id: true,
title: true,
author: { select: { name: true } },
},
});
Update(更新)與 Upsert
// 更新單筆
const updatedPost = await prisma.post.update({
where: { id: 1 },
data: { viewCount: { increment: 1 } },
});
// Upsert:存在則更新,不存在則新增
const profile = await prisma.user.upsert({
where: { email: 'bob@example.com' },
update: { name: 'Bob Updated' },
create: { email: 'bob@example.com', name: 'Bob' },
});
Delete(刪除)
// 刪除單筆
await prisma.post.delete({ where: { id: 1 } });
// 批次刪除
await prisma.post.deleteMany({ where: { published: false } });
4. 聚合與分組查詢(Aggregate & Group By)
Prisma 提供 aggregate 與 groupBy 計算統計數值:
// 聚合:計算總貼文數、最大/平均觀看次數
const stats = await prisma.post.aggregate({
_count: { id: true },
_avg: { viewCount: true },
_max: { viewCount: true },
where: { published: true },
});
// Group By:依作者統計發文數與總觀看數,並使用 having 過濾
const authorStats = await prisma.post.groupBy({
by: ['authorId'],
_count: { id: true },
_sum: { viewCount: true },
having: {
viewCount: {
_avg: { gt: 50 },
},
},
orderBy: {
_sum: { viewCount: 'desc' },
},
});
5. 分頁策略(Pagination)
Offset-based 分頁(skip / take)
適合資料量小、需要跳頁導航的場景,但在資料量龐大時效能較差。
const page = 3;
const pageSize = 10;
const posts = await prisma.post.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: 'desc' },
});
Cursor-based 分頁(cursor / take)
適合無限滾動(Infinite Scroll)或大資料量場景,利用索引欄位定位,查詢效率恆定。
const lastCursor = 42; // 上一頁最後一筆資料的 ID
const nextPosts = await prisma.post.findMany({
take: 10,
skip: 1, // 跳過游標本身
cursor: { id: lastCursor },
orderBy: { id: 'asc' },
});
6. Join 優化與 N+1 查詢問題解析
何謂 N+1 問題?
當要查詢 10 筆文章並取得其作者時,若先查出 10 筆 Post,再於迴圈內對每個 Post 發送 1 次 User 查詢,資料庫總共執行了 $1 + 10 = 11$ 次查詢。
Prisma 的處理機制
include/select(批次化查詢): Prisma 在預設關聯載入時,底層通常不會直接使用 SQLLEFT JOIN,而是將查詢拆為兩條並以IN聚合:
SELECT * FROM "Post";SELECT * FROM "User" WHERE id IN (1, 2, 3...);這在應用層自動拼裝,將 $N+1$ 次查詢收斂至 $2$ 次。
relationMode = "prisma"vs"foreignKeys": 若資料庫不支援外鍵(如 PlanetScale / Vitess),可在 schema 設定relationMode = "prisma",由 Prisma 在應用層模擬外鍵約束與 Cascade 行為。- Join 優化原則:
- 避免在
findMany裡過度深層巢狀include(如 User -> Posts -> Comments -> Likes)。 - 僅取所需欄位,使用
select代替全欄位include,降低記憶體佔用與網路傳輸。 - 確保外鍵欄位(如
authorId)建立了索引(Index)。
7. 進階 SQL 查詢(Raw SQL)
當 ORM API 無法表達複雜查詢(如 CTE、Window Functions、全文檢索)時,可使用型別安全的 Raw SQL:
import { Prisma } from '@prisma/client';
// 1. 查詢資料:$queryRaw(使用 tagged template 自動防止 SQL Injection)
const minViews = 50;
const results = await prisma.$queryRaw<Array<{ id: number; title: string; rank: number }>>`
WITH RankedPosts AS (
SELECT id, title, view_count,
DENSE_RANK() OVER (ORDER BY view_count DESC) as rank
FROM "Post"
WHERE view_count >= ${minViews}
)
SELECT * FROM RankedPosts WHERE rank <= 5;
`;
// 2. 執行指令:$executeRaw(用於 UPDATE / DELETE 等無回傳資料操作)
const affectedRows = await prisma.$executeRaw`
UPDATE "Post" SET "viewCount" = "viewCount" + 10 WHERE "published" = false;
`;
8. 實用進階技巧(Misc)
交易處理(Transactions)
Prisma 提供 Sequential 與 Interactive 兩種交易模式:
// 批次交易(平行或順序執行,全成功或全失敗)
await prisma.$transaction([
prisma.user.create({ data: { email: 'user1@example.com' } }),
prisma.post.deleteMany({ where: { published: false } }),
]);
// 互動式交易(Interactive Transaction,依賴前一步驟運算結果)
await prisma.$transaction(async (tx) => {
const sender = await tx.user.update({
where: { id: 1 },
data: { balance: { decrement: 100 } },
});
if (sender.balance < 0) {
throw new Error('餘額不足');
}
await tx.user.update({
where: { id: 2 },
data: { balance: { increment: 100 } },
});
});
Client Extensions(取代 Middleware)
Prisma 推薦使用 Extensions 來擴展功能(如軟刪除、自動 Audit Logs、欄位計算):
const xprisma = prisma.$extends({
name: 'auditExtension',
query: {
post: {
async create({ args, query }) {
console.log(`即將建立文章: ${args.data.title}`);
return query(args);
},
},
},
});
常用 Prisma CLI 指令
| 指令 | 說明 |
|---|---|
pnpm prisma db push |
直接將 Schema 同步到資料庫(適合原型開發,不留 Migration 紀錄) |
pnpm prisma migrate dev |
建立並套用 Migration 腳本(適合正式開發) |
pnpm prisma studio |
開啟視覺化 Web 介面瀏覽與編輯資料庫資料 |
pnpm prisma generate |
根據最新 Schema 重新產生 @prisma/client 型別 |