【AI全栈】Prisma ORM 入门——用 TypeScript 的方式管理数据库

2835 字
14 分钟
【AI全栈】Prisma ORM 入门——用 TypeScript 的方式管理数据库

还在手写 SQL 字符串?还在担心”字段名写错了但 TypeScript 不知道”?Prisma 让你用 TypeScript 的类型安全方式操作数据库。本文以 Card Learning Demo 为实例,从 Schema 设计到 CRUD 操作,完整走一遍 Prisma 开发流程。


一、什么是 ORM,为什么要用 Prisma#

1.1 ORM 是什么#

ORM(Object-Relational Mapping,对象关系映射)解决的是一个很具体的问题:

数据库里的”表和行”,和代码里的”类和对象”,是两种不同的世界。

没有 ORM 的时候,你要手动在这两个世界之间来回翻译:

// 手写 SQL 的痛苦
const rows = await db.query(
`SELECT * FROM cards WHERE status = $1 AND difficulty = $2 ORDER BY created_at DESC LIMIT $3 OFFSET $4`,
['todo', 'basic', 10, 0]
)
// rows 是 any[],字段名写错了?TypeScript 不知道
// SQL 字符串拼错了?运行时才报错
// 字段改名了?全局搜索字符串替换,漏掉一处就是 bug

ORM 把这个翻译过程自动化了:

// Prisma 的方式
const cards = await prisma.card.findMany({
where: { status: 'todo', difficulty: 'basic' },
orderBy: { createdAt: 'desc' },
take: 10, skip: 0,
})
// cards 是 Card[],类型完整,字段名写错编译就报错

ORM 的核心价值:

  • 类型安全:字段名、字段类型,编译时就能检查
  • 自动补全:IDE 知道 card 有哪些字段,不用查文档
  • 迁移管理:Schema 变更有版本记录,不用手动维护 SQL 文件

ORM 的代价:

  • 复杂查询(多表 JOIN、窗口函数)有时不如手写 SQL 直观
  • 有学习成本,需要理解 ORM 的查询 API
  • 生成的 SQL 不一定是最优的(需要 EXPLAIN 验证)

这是取舍,不是银弹。对于大多数业务 CRUD,ORM 的收益远大于代价。

1.2 为什么选 Prisma,而不是 TypeORM#

Node.js 生态里最常见的两个 ORM 是 TypeORM 和 Prisma。我学的时候也纠结过,后来选了 Prisma,原因是:

对比维度手写 SQLTypeORMPrisma
类型安全❌ 字符串拼错编译不报错⚠️ 装饰器复杂,类型推导不完整✅ 自动生成完整类型
Schema 管理手动维护 SQL 文件自动/手动混用,容易混乱prisma migrate dev 一条命令
学习曲线低入门但容易出错中高,装饰器 + 实体类 + Repository 三层,Schema 文件直观易读
IDE 支持一般自动补全 + 类型提示
迁移文件手写自动生成但可读性差自动生成,SQL 可读,可手动修改

TypeORM 的问题不是”不好”,而是装饰器写法在复杂场景下类型推导容易失真——你以为有类型保护,实际上 any 悄悄溜进来了。Prisma 的设计哲学不同:Schema 文件是唯一的事实源,所有类型从 Schema 自动生成,没有歧义。

Prisma 另一个我很喜欢的设计:Schema 文件即文档。新人接手项目,打开 schema.prisma,所有表结构、字段类型、关系一目了然——不用去数据库里反向查,也不用翻散落在各处的实体类文件。


二、环境搭建#

2.1 安装#

Terminal window
pnpm add prisma @prisma/client
pnpm add @prisma/adapter-pg # PG 驱动

2.2 初始化#

Terminal window
npx prisma init

生成 prisma/schema.prisma.env 中的 DATABASE_URL

2.3 用 Docker 启动 PostgreSQL#

docker-compose.yml
services:
postgres:
image: postgres:17-alpine
ports: ['15432:5432']
environment:
POSTGRES_USER: journey
POSTGRES_PASSWORD: journey123
POSTGRES_DB: ai_journey_lab
Terminal window
docker compose up -d

2.4 配置连接#

.env
DATABASE_URL=postgresql://journey:journey123@localhost:15432/ai_journey_lab

三、Schema 设计:用代码描述数据库#

3.1 基础模型#

model Card {
id String @id @default(uuid())
title String
content String
summary String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("cards")
}
语法含义
@id主键
@default(uuid())默认生成 UUID
@default(now())默认取当前时间
@updatedAt每次更新自动刷新时间
String?可为空字段
@map("created_at")Prisma 字段名 → 数据库列名映射
@@map("cards")Prisma 模型名 → 数据库表名映射

3.2 Prisma Enum#

enum CardStatus {
todo
doing
done
}
enum CardDifficulty {
basic
medium
advanced
}
model Card {
difficulty CardDifficulty @default(basic)
status CardStatus @default(todo)
}

Enum 的好处:数据库层面约束合法值,TypeScript 编译时检查——不会出现 status = 'invalid' 这种运行时才爆的错误。

3.3 多对多关系:Card ↔ Tag#

Card 和 Tag 是多对多关系(一张卡片多个标签,一个标签多张卡片),需要中间表:

model Tag {
id String @id @default(uuid())
name String @unique
@@map("tags")
}
model CardTag {
cardId String
tagId String
card Card @relation(fields: [cardId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([cardId, tagId])
@@map("card_tags")
}

关键概念

  • @relation(fields: [cardId], references: [id]) — cardId 是外键,指向 Card.id
  • @@id([cardId, tagId]) — 联合主键,防止重复关联
  • onDelete: Cascade — 删除 Card 时自动删除关联的 CardTag 记录

3.4 一对多关系:Card → LearningRecord#

model LearningRecord {
id String @id @default(uuid())
cardId String @map("card_id")
action String
note String?
card Card @relation(fields: [cardId], references: [id], onDelete: Cascade)
@@map("learning_records")
}

一对多不需要中间表——在”多”的一方(LearningRecord)放外键 card_id 即可。

3.5 生成数据库#

Terminal window
npx prisma migrate dev --name init

Prisma 自动生成迁移 SQL 并执行。prisma/migrations/ 目录下可以看到每一步的 SQL 文件:

-- prisma/migrations/.../migration.sql
CREATE TABLE "cards" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "cards_pkey" PRIMARY KEY ("id")
);

迁移文件是纯 SQL,可读、可审查、可手动修改——这点比 TypeORM 自动生成的迁移文件友好很多。


四、Prisma Studio:内置 GUI#

Terminal window
npx prisma studio

浏览器打开 http://localhost:5555,可以:

  • 浏览所有表的数据(表格视图)
  • 直接新增 / 编辑 / 删除记录
  • 查看表之间的关系

这是 Prisma 内置的 GUI 工具,相当于一个轻量版的 pgAdmin,适合开发阶段快速查看和修改数据。我通常用它来验证 Service 层写完之后数据是否正确写进去了——比 console.log 直观多了。


五、NestJS 集成:PrismaService#

5.1 PrismaService 封装#

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! })
super({ adapter })
}
async onModuleInit() { await this.$connect() }
async onModuleDestroy() { await this.$disconnect() }
}

关键点:

  • extends PrismaClient — 继承 Prisma 客户端,获得所有 model 方法
  • onModuleInit — NestJS 启动时自动连接数据库
  • onModuleDestroy — NestJS 关闭时自动断开连接
  • 模块加上 @Global() → 任何 Service 都能注入,不用每个模块都 imports

5.2 CardsService CRUD#

@Injectable()
export class CardsService {
constructor(private readonly prisma: PrismaService) {}
// 分页列表 + 关键词搜索 + 动态排序
async findAll(query: QueryCardsDto) {
const where: Prisma.CardWhereInput = {}
if (query.keyword) {
where.OR = [
{ title: { contains: query.keyword, mode: 'insensitive' } },
{ summary: { contains: query.keyword, mode: 'insensitive' } },
{ content: { contains: query.keyword, mode: 'insensitive' } },
]
}
if (query.status) where.status = query.status
if (query.difficulty) where.difficulty = query.difficulty
const orderBy = {
[query.sortBy]: query.sortOrder,
} satisfies Prisma.CardOrderByWithRelationInput
const [items, total] = await this.prisma.$transaction([
this.prisma.card.findMany({ where, skip, take, orderBy }),
this.prisma.card.count({ where }),
])
return { items, meta: { page, pageSize, total } }
}
// 单条查询
async findOne(id: string) {
const card = await this.prisma.card.findUnique({ where: { id } })
if (!card) throw new NotFoundException('卡片不存在')
return card
}
// 创建
async create(dto: CreateCardDto) {
return this.prisma.card.create({
data: {
title: dto.title,
content: dto.content || '',
difficulty: dto.difficulty,
status: dto.status,
}
})
}
// 更新
async update(id: string, dto: UpdateCardDto) {
await this.findOne(id) // 先确认存在,不存在抛 404
return this.prisma.card.update({ where: { id }, data: dto })
}
// 删除
async remove(id: string) {
await this.findOne(id)
return this.prisma.card.delete({ where: { id } })
}
}

六、Seeding:初始化数据#

prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const demos = [
{ id: 'prompt-template-weekly-report', title: '角色驱动 · 智能报告' },
{ id: 'chat', title: '多轮对话 · AI 助手' },
]
for (const demo of demos) {
await prisma.demo.upsert({
where: { id: demo.id },
update: demo,
create: demo,
})
}
}
main().catch(console.error).finally(() => prisma.$disconnect())

upsert = update + insert:有则更新,无则创建。seed 脚本的标配——重复执行不会报错,也不会产生重复数据。

执行:npx prisma db seed


七、Prisma + Zod:为什么不用 class-validator#

这是我在学 NestJS 时纠结过的一个问题。NestJS 官方文档推荐的是 class-validator + class-transformer,但我最后选了 Zod。

7.1 class-validator 的问题#

// class-validator 的写法
import { IsString, IsEnum, MinLength, IsOptional } from 'class-validator'
export class CreateCardDto {
@IsString()
@MinLength(1)
title: string
@IsEnum(CardDifficulty)
@IsOptional()
difficulty?: CardDifficulty
}

看起来没问题,但有几个坑:

坑 1:装饰器不是类型系统的一部分

@IsString() 只在运行时校验,TypeScript 编译时不知道它的存在。如果请求里没传 title,运行时就是 undefined——编译不报错,运行才爆。

坑 2:需要 class-transformer 配合,增加复杂度

class-validator 只校验,不转换。要把 JSON 请求体转成类实例,还需要 class-transformerplainToInstance。两个库配合,配置项多,容易出错。

坑 3:类型推导不完整

// ❌ class-validator:字段声明是 string,但实际可能是 undefined
const dto = plainToInstance(CreateCardDto, body)
dto.title // TypeScript 认为是 string,实际可能是 undefined

7.2 Zod 的优势#

// Zod 的写法
import { z } from 'zod'
export const createCardSchema = z.object({
title: z.string().trim().min(1, '标题不能为空'),
difficulty: z.enum(['basic', 'medium', 'advanced']).default('basic'),
status: z.enum(['todo', 'doing', 'done']).default('todo'),
content: z.string().optional().default(''),
}).strict()
export type CreateCardDto = z.infer<typeof createCardSchema>

优势 1:类型从 Schema 自动推导,没有歧义

z.infer<typeof createCardSchema> 生成的类型是精确的——parse 成功之后,返回值的每个字段都有正确的类型,不会有 undefined 漏网。

优势 2:parse 即校验 + 转换,一步到位

const dto = createCardSchema.parse(body)
// dto 是完整的 CreateCardDto 类型,已经过校验和转换
// 如果校验失败,直接抛 ZodError,不需要额外配置

优势 3:和 Prisma 的设计哲学一致

Prisma:Schema 文件 → 自动生成类型 Zod:Schema 对象 → 自动推导类型

两者都是”Schema 即类型”的思路,组合起来非常自然:

// 请求校验(Zod)
const createCardSchema = z.object({
title: z.string().trim().min(1),
difficulty: z.enum(['basic', 'medium', 'advanced']).default('basic'),
}).strict()
// 数据库层(Prisma Schema)
// enum CardDifficulty { basic medium advanced }
// model Card { difficulty CardDifficulty @default(basic) }

请求进来 → Zod 拦截非法值(返回 400)→ Prisma 写入合法数据 → PG enum 再拦一层。三层防护,每层职责清晰。

7.3 什么时候还是用 class-validator#

如果项目已经大量使用 class-validator,或者团队对它更熟悉,没必要强行换。Zod 的优势在新项目TypeScript 优先的场景下更明显。


八、常用 Prisma 操作速查#

操作Prisma API说明
列表findMany({ where, skip, take, orderBy })分页查询
单条findUnique({ where: { id } })主键查询
创建create({ data })插入
更新update({ where, data })部分更新
删除delete({ where })删除
统计count({ where })行数
创建或更新upsert({ where, create, update })seed 常用
事务$transaction([...])批量执行
模糊搜索{ contains: 'kw', mode: 'insensitive' }ILIKE
动态排序{ [sortBy]: sortOrder } satisfies Prisma.XxxOrderByInput类型安全动态排序

九、GUI 开发工作流#

推荐的开发节奏:

1. 写 schema.prisma → 描述数据模型
2. prisma migrate dev → 生成 SQL + 建表
3. prisma studio → GUI 查看表结构
4. 写 Service 层 CRUD 逻辑
5. prisma studio / pgAdmin → 验证数据是否正确写入

Prisma Studio + pgAdmin 双 GUI 组合:Studio 快速 CRUD 浏览,pgAdmin 深度分析(执行计划、索引、连接监控)。两个工具各有侧重,开发阶段都用得上。


写在最后#

Prisma 的核心价值不是”让你不写 SQL”,而是让你用类型安全的方式操作数据库

Schema 文件是唯一的事实源——它同时是文档、迁移源、类型生成器。配合 Zod 做请求校验,整条链路从请求到数据库都有类型保护,写起来很踏实。

这是我在 Card Learning Demo 里实际用下来的感受,不一定是最优解,但目前对我来说是最顺手的一套。

下一篇介绍 Card Learning Demo 全栈项目实战——从设计到 CRUD 接口的完整实现:Card Learning Demo——一个全栈数据库学习项目


昇哥 · 2026年5月 学 Prisma + NestJS 途中,顺手把想清楚的事写下来。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【AI全栈】Prisma ORM 入门——用 TypeScript 的方式管理数据库
https://blog.fridolph.top/posts/2026-05-16__prisma-guide/
作者
Fridolph
发布于
2026-05-06
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录