📋 目錄
想把 Express 加上資料庫,卻被 SQL 的 INSERT INTO、JOIN、foreign key 搞得一個頭兩個大?Prisma 讓你用 TypeScript 的語法操作資料庫,學一套工具就能涵蓋大多數常見的資料庫操作需求。這篇整理 Prisma 的核心概念、Schema 定義與 CRUD 操作實務。
前言:為什麼要學 ORM?
傳統上,Node.js 連接資料庫需要寫 SQL:
INSERT INTO users (name, email) VALUES ('小明', 'xiaoming@example.com');
SELECT * FROM users WHERE email = 'xiaoming@example.com';
SQL 功能強大,但缺點也很明顯:
- 語法瑣碎:大小寫、空格、單引號都要注意,一個小錯誤就debug半天
- 沒有型別檢查:寫錯欄位名稱,SQL 不會告訴你,要等到 query 執行失敗才知道
- 跨資料庫語法不同:MySQL 和 PostgreSQL 的語法有些許差異,換資料庫等於重新學
ORM(Object-Relational Mapping)的思路是:用程式語言(TypeScript)來描述資料結構和操作,而不是用 SQL 字串。Prisma 是目前 Node.js/TypeScript 生態中最受歡迎的 ORM 之一。
Prisma 的核心組成
Prisma 分三層:
- Prisma Schema:用一個
schema.prisma檔案定義資料庫結構(相當於 SQL 的 CREATE TABLE) - Prisma Client:一個型別安全的 TypeScript SDK,用於在程式碼中操作資料庫
- Prisma Migrate:根據 Schema 自動產生資料庫遷移腳本
第一步是寫 schema.prisma:
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite" // 開發環境用 SQLite,之後可以換成 postgresql、mysql 等
url = env("DATABASE_URL")
}
// 定義 User 模型
model User {
id Int @id @default(autoincrement())
name String
email String @unique // email 不可重複
createdAt DateTime @default(now())
// 關聯:一個使用者可以有多筆訂單
orders Order[]
}
// 定義 Order 模型
model Order {
id Int @id @default(autoincrement())
amount Int
status String @default("pending") // pending / paid / cancelled
// 關聯:每筆訂單屬於一個使用者
userId Int
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
}
這段 Schema 翻譯成白話:
- User:有 id、name、email、createdAt,一個 User 可以有多筆 Order
- Order:有 id、amount、status、createdAt,每筆 Order 屬於一個 User
用 @id 標記主鍵,@default(autoincrement()) 自動遞增,@unique 代表不可重複——這些標記的語意直觀,不需要懂 SQL 語法細節。
初始化 Prisma
# 安裝 Prisma CLI 與 Client
npm install prisma --save-dev
npm install @prisma/client
# 在專案根目錄初始化 Prisma
npx prisma init
初始化完成後,Prisma 會在 prisma/schema.prisma 產生 Schema 檔案,並在 .env 產生 DATABASE_URL 設定。
Prisma Client:用 TypeScript 操作資料庫
設定好 Schema 之後,執行 npx prisma generate,Prisma 會根據你的 Schema 自動產生一個型別安全的 Client,讓你用 TypeScript 操作資料庫。
基本 CRUD 操作
// lib/prisma.ts
// 建立一個 Prisma Client 實例(整個專案共用一個)
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;
// 插入一筆新使用者(相當於 INSERT INTO)
const newUser = await prisma.user.create({
data: {
name: '小明',
email: 'xiaoming@example.com',
},
});
console.log(`Created user: ${newUser.name}, ID: ${newUser.id}`);
// 查詢所有使用者(相當於 SELECT * FROM users)
const users = await prisma.user.findMany();
users.forEach(user => {
console.log(`${user.id}: ${user.name} (${user.email})`);
});
// 條件查詢:找出特定使用者(相當於 SELECT * FROM users WHERE email = ?)
const user = await prisma.user.findUnique({
where: { email: 'xiaoming@example.com' },
});
if (user) {
console.log(`Found: ${user.name}`);
}
// 更新使用者(相當於 UPDATE users SET name = ? WHERE id = ?)
const updatedUser = await prisma.user.update({
where: { id: 1 },
data: { name: '大明' },
});
// 刪除使用者(相當於 DELETE FROM users WHERE id = ?)
await prisma.user.delete({
where: { id: 1 },
});
關聯查詢:一次拿到訂單和使用者
這是 Prisma 最強大的地方——用 TypeScript 語法處理 SQL JOIN:
// 找出所有使用者,順便拿到每個使用者的訂單
const usersWithOrders = await prisma.user.findMany({
include: {
orders: true, // 包含該使用者的所有訂單
},
});
usersWithOrders.forEach(user => {
console.log(`${user.name} 的訂單:`);
user.orders.forEach(order => {
console.log(` - 訂單 ${order.id}:$${order.amount} (${order.status})`);
});
});
// 查詢特定使用者的訂單,並篩選已付款的
const paidOrders = await prisma.order.findMany({
where: {
userId: 1,
status: 'paid', // 只取已付款的訂單
},
orderBy: {
createdAt: 'desc', // 依時間倒序
},
});
Prisma + Express 實戰
把 Prisma 接入 Express,把之前 RESTful API 教的知識用上來:
// server-with-db.mjs
import express from 'express';
import { PrismaClient } from '@prisma/client';
const app = express();
const prisma = new PrismaClient();
app.use(express.json());
// GET /api/users — 取得所有使用者
app.get('/api/users', async (req, res) => {
const users = await prisma.user.findMany({
include: { orders: true }, // 包含訂單數量
});
res.json(users);
});
// GET /api/users/:id — 取得特定使用者
app.get('/api/users/:id', async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: parseInt(req.params.id) },
include: { orders: true },
});
if (!user) {
return res.status(404).json({ error: '使用者不存在' });
}
res.json(user);
});
// POST /api/users — 建立新使用者
app.post('/api/users', async (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'name 和 email 是必填欄位' });
}
try {
const newUser = await prisma.user.create({
data: { name, email },
});
res.status(201)
.set('Location', `/api/users/${newUser.id}`)
.json(newUser);
} catch (err) {
// Prisma 在 email @unique 衝突時會丟出 P2002 錯誤碼
if (err.code === 'P2002') {
return res.status(409).json({ error: '這個 email 已經被註冊了' });
}
console.error('Unexpected error:', err);
res.status(500).json({ error: '伺服器錯誤' });
}
});
// DELETE /api/users/:id
app.delete('/api/users/:id', async (req, res) => {
try {
await prisma.user.delete({
where: { id: parseInt(req.params.id) },
});
res.status(204).end();
} catch (err) {
if (err.code === 'P2025') {
return res.status(404).json({ error: '使用者不存在' });
}
console.error('Unexpected error:', err);
res.status(500).json({ error: '伺服器錯誤' });
}
});
app.listen(3000, () => {
console.log('Server with Prisma running on http://localhost:3000');
});
為什麼前端工程師該選 Prisma?
跟其他 ORM 比起來(Sequelize、TypeORM),Prisma 的優勢:
- Schema 即文件:
schema.prisma本身就是資料庫結構的完整文件,團隊成員一看就懂 - Migration 簡單:修改 Schema 後執行
npx prisma migrate dev,Prisma 自動產生 SQL 遷移腳本 - 錯誤碼友善:Prisma 有完整的錯誤碼體系,P2002 是 unique 衝突、P2025 是找不到記錄——這些錯誤碼讓除錯有方向
- 型別安全:Prisma Client 是根據你的 Schema 自動產生的,Query 結果有完整的 TypeScript 型別
常見問題
Q:Prisma 效能比其他 ORM 差嗎?
A:Prisma Client 產生的 query 已經過高度優化,在多數網頁應用的規模下(幾千到幾萬筆資料)效能差異感受不到。如果真的遇到效能瓶頸,Prisma 也支援Raw Query,讓你在必要時直接寫 SQL。
Q:Prisma 支援哪些資料庫?
A:PostgreSQL、MySQL、SQLite、SQL Server、MongoDB(預覽)。開發階段推薦用 SQLite——不需要另外安裝資料庫服務,一個檔案就能跑,團隊成員之間分享容易。
Q:什麼時候該上正式資料庫?
A:當 SQLite 開始遇到以下情況時,就是換正式資料庫的時機:
- 需要多人同時寫入(SQLite 寫入是單執行緒)
- 需要備援機制
- 資料量開始接近 1GB
- 需要更嚴格的 Transaction 支援
總結:從 SQL 字串地獄解脫
Prisma 的核心價值:用你已經會的 TypeScript 語法操作資料庫,不需要另外學 SQL 語法細節。
這篇文章涵蓋的範圍:
- Schema 定義:描述資料結構
- Prisma Client:插入、查詢、更新、刪除
- 關聯查詢:一次拿到相關資料
- 實際 Express 整合
下一步,建議把 Prisma + Express 的組合實際跑起來,用 SQLite 當開發資料庫,做一個完整的 CRUD API 出來。資料庫的學習最有效的方式是實作——看再多文件,都不如親手建一個、壞一個、修一個。
本篇是「前端工程師後端入門」系列第三篇。系列前兩篇:《從 Node.js 原生 HTTP 到 Express 框架》、《RESTful API 設計:前端視角的 API 思維》。