3. Prisma Schema定义与迁移

Prisma 的核心工作方式可以概括为“先声明、再生成、后迁移”。schema.prisma 是整个数据访问层的唯一事实来源:它既描述数据库结构,也驱动 Prisma Client 的类型生成,还作为 Prisma Migrate 计算迁移差异的输入。理解这一节,关键不是记住语法,而是建立“Schema 声明 → 迁移 SQL → 数据库实际结构 → Client 类型”这条闭环链路。

一、Schema 的三段式结构与数据源声明

一个典型的 schema.prisma 由三部分组成:generator 定义客户端生成目标,datasource 定义数据库连接,model 定义数据模型。下面是一个教学示例,假设我们为一个小型内容管理系统建模:

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?
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  tags      Tag[]
  createdAt DateTime @default(now())
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

enum Role {
  USER
  ADMIN
}

这里 provider = "postgresql" 决定了迁移生成的 SQL 方言;url = env("DATABASE_URL") 表示连接串从环境变量读取,避免把凭据写进版本库。切换数据库时,provider 与连接串需要同步调整,否则迁移 SQL 会与目标数据库不兼容。

二、字段类型与属性修饰符

Prisma 的标量类型包括 String、Int、BigInt、Float、Decimal、Boolean、DateTime、Json、Bytes。其中 Decimal 适合金额等需要精确计算的场景,Float 是浮点数,不适合直接用于财务累加。类型后的 ? 表示可空,例如 name String? 对应数据库中的 NULL 允许。

属性修饰符决定字段在数据库层面的约束与行为,常用的有:

  • @id:主键;@default(autoincrement()) 让数据库自增生成。
  • @unique:唯一约束,如 email、Tag.name。
  • @default(...):默认值,可写常量、now()、uuid() 或枚举值。
  • @updatedAt:Prisma Client 在更新记录时自动写入当前时间。
  • @map("..."):把字段映射到数据库中不同命名的列,常用于适配既有表结构。
  • @@index([...]) 与 @@unique([...]):模型级复合索引与复合唯一约束。

需要注意,@updatedAt 由 Prisma Client 维护,而不是数据库触发器;如果绕过 Prisma 直接写库,该字段不会自动更新。这是排查“更新时间不变化”问题的常见切入点。

三、关系声明:一对多与多对多

Prisma 的关系通过两侧字段共同表达。以 User 与 Post 为例,Post.authorId 是外键标量字段,Post.author 是关系字段,@relation(fields: [authorId], references: [id]) 明确外键指向 User.id;User.posts Post[] 是反向关系。两侧必须成对出现,否则 prisma validate 会报错。

多对多关系在 Prisma 中通常写成两侧数组,例如 Post.tags Tag[] 与 Tag.posts Post[]。Prisma Migrate 会隐式创建一张连接表(如 _PostToTag),包含两个外键列并建立复合唯一索引。如果需要给关系附加额外字段(例如“打标签时间”),就必须显式定义连接模型,把隐式多对多改为“一对多 + 一对多”的组合。

Prisma Schema 驱动的迁移与客户端生成链路 schema.prisma generator client datasource db model User { ... } model Post { ... } enum Role { ... } prisma/migrations/ 20240101_init/ migration.sql 20240210_add_tag/ migration.sql migration_lock.toml PostgreSQL User Post Tag _PostToTag _prisma_migrations migrate dev migrate deploy Prisma Client prisma.user.findMany() prisma.post.create() 类型安全 + 自动补全 prisma generate 运行时查询 Schema 是唯一事实来源:迁移负责结构演进,Client 负责类型安全访问,_prisma_migrations 表记录已应用迁移

四、Prisma Migrate 工作流

开发阶段的标准流程是:修改 schema.prisma → 执行 npx prisma migrate dev --name add_tag → Prisma 对比当前数据库结构与 Schema,生成新的迁移 SQL 并应用到开发库,同时触发 Client 重新生成。迁移文件一旦生成就应提交到版本库,它是团队协作与生产部署的依据。

# 开发环境:生成并应用迁移
npx prisma migrate dev --name add_tag

# 生产环境:只应用已有迁移,不生成新迁移
npx prisma migrate deploy

# 查看迁移状态与差异
npx prisma migrate status

# 校验 Schema 语法与关系完整性
npx prisma validate

生产环境必须使用 migrate deploy,它只按顺序执行 migrations 目录中尚未应用的迁移,不会尝试修改 Schema 或重置数据库。若生产库结构与迁移历史不一致,migrate status 会提示存在未应用的迁移或漂移,此时应先排查,而不是直接执行 migrate reset——后者会清空数据库,仅适用于本地开发。

五、数据库版本管理与漂移处理

Prisma 在数据库中维护一张 _prisma_migrations 表,记录每个迁移的名称、校验和、开始与结束时间。当有人手动改了数据库结构(例如直接 ALTER TABLE),迁移历史与真实结构就会产生“漂移”。处理漂移的常规做法是:先用 npx prisma migrate diff 对比数据库与 Schema 的差异,确认差异内容后,要么生成一个补充迁移把手工改动纳入版本管理,要么在开发环境用 migrate reset 重建并重新应用全部迁移。

不要在生产环境执行 prisma migrate reset 或 db push。db push 适合原型阶段快速同步结构,但它不生成迁移文件,无法形成可审计的版本历史。
团队协作时,迁移文件必须提交到 Git;拉取他人迁移后,先执行 npx prisma migrate dev 让本地库追上,再继续开发。若多人同时修改同一模型,容易产生迁移冲突,应尽早合并并重新生成迁移。

把这条链路串起来看:Schema 决定迁移内容,迁移决定数据库结构,数据库结构又决定 Client 能安全访问哪些字段。任何一环脱节,都会表现为运行时报错、类型不匹配或数据不一致。因此,每次改模型后,建议按“validate → migrate dev → generate → 运行查询验证”的顺序走一遍,确保声明、结构与代码三者始终一致。