跳到主要内容

引用操作

引用操作决定了当您的应用程序删除或更新关联记录时,该记录会发生什么。

从 2.26.0 版本开始,您可以在 Prisma schema 中的关系字段上定义引用操作。这使您能够在 Prisma ORM 级别定义引用操作,例如级联删除和级联更新。

信息

版本差异

  • 如果您使用 3.0.1 或更高版本,则可以按照此页面上的描述使用引用操作。
  • 如果您使用的版本介于 2.26.0 和 3.0.0 之间,则可以按照此页面上的描述使用引用操作,但必须启用预览功能标志 referentialActions
  • 如果您使用 2.25.0 或更早版本,则可以在数据库中手动配置级联删除。

在以下示例中,将 onDelete: Cascade 添加到 Post 模型上的 author 字段意味着删除 User 记录也将删除所有相关的 Post 记录。

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
}

model User {
id Int @id @default(autoincrement())
posts Post[]
}

如果您未指定引用操作,Prisma ORM 将使用默认设置

危险

如果您从早于 2.26.0 的版本升级:检查引用操作的升级路径部分至关重要。Prisma ORM 对引用操作的支持取消了 Prisma Client 中阻止运行时级联删除的安全网。如果您在使用此功能时未升级数据库,旧的默认操作 - ON DELETE CASCADE - 将会生效。这可能会导致您未预料到的级联删除。

什么是引用操作?

引用操作是定义当您运行updatedelete 查询时,数据库如何处理被引用记录的策略。

数据库级别的引用操作

引用操作是外键约束的功能,用于维护数据库中的引用完整性。

在 Prisma schema 中定义数据模型之间的关系时,您使用关系字段(数据库中不存在)和标量字段(数据库中存在)。这些外键在数据库级别连接模型。

引用完整性规定,这些外键必须引用相关数据库表中现有的主键值。在您的 Prisma schema 中,这通常由相关模型上的 id 字段表示。

默认情况下,数据库将拒绝任何违反引用完整性的操作,例如,删除被引用的记录。

如何使用引用操作

引用操作在@relation 属性中定义,并映射到底层数据库中外键约束上的操作。如果您未指定引用操作,Prisma ORM 将退回到默认设置

以下模型定义了 UserPost 之间的一对多关系,以及 PostTag 之间的多对多关系,并显式定义了引用操作

schema.prisma
model User {
id Int @id @default(autoincrement())
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
title String
tags TagOnPosts[]
User User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
userId Int?
}

model TagOnPosts {
id Int @id @default(autoincrement())
post Post? @relation(fields: [postId], references: [id], onUpdate: Cascade, onDelete: Cascade)
tag Tag? @relation(fields: [tagId], references: [id], onUpdate: Cascade, onDelete: Cascade)
postId Int?
tagId Int?
}

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

此模型显式定义了以下引用操作

  • 如果您删除一个 Tag,则在使用 Cascade 引用操作的情况下,TagOnPosts 中对应的标签分配也将被删除
  • 如果您删除一个 User,由于 SetNull 引用操作,通过将字段值设置为 Null,作者将从所有帖子中删除。为了允许这样做,Post 中的 UseruserId 必须是可选字段。

Prisma ORM 支持以下引用操作

引用操作默认设置

如果您未指定引用操作,Prisma ORM 将使用以下默认设置

子句可选关系强制关系
onDeleteSetNull (设空)Restrict (限制)
onUpdateCascade (级联)Cascade (级联)

例如,在以下 schema 中,所有 Post 记录都必须通过 author 关系连接到 User

model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id])
authorId Int
}

model User {
id Int @id @default(autoincrement())
posts Post[]
}

schema 没有在强制性的 author 关系字段上显式定义引用操作,这意味着默认的 onDeleteRestrictonUpdateCascade 引用操作将生效。

注意事项

以下注意事项适用

  • 引用操作不支持隐式多对多关系。要使用引用操作,您必须定义显式多对多关系并在连接表上定义引用操作。
  • 引用操作和必需/可选关系的某些组合是不兼容的。例如,在必需关系上使用 SetNull 在删除被引用记录时会导致数据库错误,因为这将违反非空约束。有关更多信息,请参阅此 GitHub issue

引用操作的类型

下表显示了每个数据库支持哪些引用操作。

数据库Cascade (级联)Restrict (限制)NoAction (不操作)SetNull (设空)SetDefault (设默认值)
PostgreSQL✔️✔️✔️✔️⌘✔️
MySQL/MariaDB✔️✔️✔️✔️❌ (✔️†)
SQLite✔️✔️✔️✔️✔️
SQL Server✔️❌‡✔️✔️✔️
CockroachDB✔️✔️✔️✔️✔️
MongoDB††✔️✔️✔️✔️

引用操作的特殊情况

引用操作是 ANSI SQL 标准的一部分。但是,在某些情况下,一些关系型数据库会偏离该标准。

MySQL/MariaDB

MySQL/MariaDB 以及底层的 InnoDB 存储引擎不支持 SetDefault。具体行为取决于数据库版本

  • 在 MySQL 8 及更高版本以及 MariaDB 10.5 及更高版本中,SetDefault 实际上是 NoAction 的别名。您可以使用 SET DEFAULT 引用操作定义表,但在运行时会触发外键约束错误。
  • 在 MySQL 5.6 及更高版本以及 MariaDB 10.5 之前版本中,尝试使用 SET DEFAULT 引用操作创建表定义会因语法错误而失败。

因此,当您将 mysql 设置为数据库 provider 时,Prisma ORM 会警告用户在 Prisma schema 中用其他操作替换 SetDefault 引用操作。

PostgreSQL

PostgreSQL 是 Prisma ORM 支持的唯一允许您定义引用非空字段的 SetNull 引用操作的数据库。但是,当此操作在运行时触发时,会引发外键约束错误。

因此,在(默认的)foreignKeys 关系模式下将 postgres 设置为数据库 provider 时,Prisma ORM 会警告用户将任何包含在具有 SetNull 引用操作的 @relation 属性中的字段标记为可选。对于所有其他数据库 provider,Prisma ORM 会因验证错误而拒绝 schema。

SQL Server

Restrict 不适用于 SQL Server 数据库,但您可以使用NoAction 代替。

Cascade (级联)

  • onDelete: Cascade 删除被引用记录将触发引用记录的删除。
  • onUpdate: Cascade 如果依赖记录的被引用标量字段被更新,则更新关系标量字段。

示例用法

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
authorId Int
}

model User {
id Int @id @default(autoincrement())
posts Post[]
}
使用 Cascade 的结果

如果删除了一个 User 记录,则他们的帖子也会被删除。如果用户的 id 被更新,则相应的 authorId 也会被更新。

如何使用级联删除

Restrict (限制)

  • onDelete: Restrict 如果存在任何引用记录,则阻止删除。
  • onUpdate: Restrict 阻止被引用记录的标识符被更改。

示例用法

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id], onDelete: Restrict, onUpdate: Restrict)
authorId Int
}

model User {
id Int @id @default(autoincrement())
posts Post[]
}
使用 Restrict 的结果

有帖子的 User 无法删除。Userid 无法更改。

警告

Restrict 操作在Microsoft SQL Server 上不可用,并会触发 schema 验证错误。相反,您可以使用NoAction,它会产生相同的结果,并与 SQL Server 兼容。

NoAction (不操作)

NoAction 操作与 Restrict 类似,两者的区别取决于所使用的数据库

  • PostgreSQL: NoAction 允许将检查(是否存在表中被引用行)延迟到事务后期。有关更多信息,请参阅PostgreSQL 文档
  • MySQL: NoAction 的行为与 Restrict 完全相同。有关更多信息,请参阅MySQL 文档
  • SQLite: 当相关主键被修改或删除时,不执行任何操作。有关更多信息,请参阅SQLite 文档
  • SQL Server: 当被引用记录被删除或修改时,会引发错误。有关更多信息,请参阅SQL Server 文档
  • MongoDB(3.6.0 版本起预览):当记录被修改或删除时,不会对任何相关记录执行任何操作。
警告

如果您在Prisma Client 中管理关系,而不是在数据库中使用外键,您应该了解目前 Prisma ORM 只实现了引用操作。外键还会创建约束,使得无法以违反这些约束的方式操作数据:数据库不会执行查询,而是响应一个错误。如果您在 Prisma Client 中模拟引用完整性,这些约束将不会被创建,因此如果您将引用操作设置为 NoAction,将没有任何检查来阻止您破坏引用完整性。

示例用法

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
authorId Int
}

model User {
id Int @id @default(autoincrement())
posts Post[]
}
使用 NoAction 的结果

有帖子的 User 无法删除。Userid 无法更改。

SetNull (设空)

  • onDelete: SetNull 引用对象的标量字段将被设置为 NULL

  • onUpdate: SetNull 更新被引用对象的标识符时,引用对象的标量字段将被设置为 NULL

SetNull 仅适用于可选关系。在必需关系上,由于标量字段不能为 null,将抛出运行时错误。

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
author User? @relation(fields: [authorId], references: [id], onDelete: SetNull, onUpdate: SetNull)
authorId Int?
}

model User {
id Int @id @default(autoincrement())
posts Post[]
}
使用 SetNull 的结果

删除 User 时,其所有撰写的帖子的 authorId 将被设置为 NULL

更改 Userid 时,其所有撰写的帖子的 authorId 将被设置为 NULL

SetDefault (设默认值)

  • onDelete: SetDefault 引用对象的标量字段将被设置为该字段的默认值。

  • onUpdate: SetDefault 引用对象的标量字段将被设置为该字段的默认值。

这些操作要求使用@default 为关系标量字段设置默认值。如果未为任何标量字段提供默认值,将抛出运行时错误。

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
authorUsername String? @default("anonymous")
author User? @relation(fields: [authorUsername], references: [username], onDelete: SetDefault, onUpdate: SetDefault)
}

model User {
username String @id
posts Post[]
}
使用 SetDefault 的结果

删除 User 时,其现有帖子的 authorUsername 字段值将被设置为 'anonymous'。

更改 Userusername 时,其现有帖子的 authorUsername 字段值将被设置为 'anonymous'。

数据库特定要求

如果您的数据模型中存在自引用关系循环关系,则 MongoDB 和 SQL Server 对引用操作有特定要求。如果您的关系存在多个级联路径,则 SQL Server 也有特定要求。

从 2.25.0 及更早版本的升级路径

升级时有几种路径可供选择,它们会根据期望的结果产生不同的结果。

如果您当前使用 migration 工作流程,可以运行 Introspection 来检查默认设置如何在您的 schema 中体现。如果需要,您可以手动更新数据库。

您也可以选择跳过检查默认设置,直接运行 migration 来使用新的默认值更新数据库。

以下假设您已升级到 2.26.0 或更高版本并启用了预览功能标志,或者已升级到 3.0.0 或更高版本

使用 Introspection (自省)

如果您对数据库进行自省 (Introspect),数据库级别配置的引用操作将反映在您的 Prisma Schema 中。如果您一直使用 Prisma Migrate 或 prisma db push 来管理数据库 schema,这些很可能是 2.25.0 及更早版本中的默认值

当您运行 Introspection 时,Prisma ORM 会将数据库中的所有外键与 schema 进行比较,如果 SQL 语句 ON DELETEON UPDATE 不匹配默认值,它们将在 schema 文件中显式设置。

完成自省后,您可以查看 schema 中的非默认子句。最重要的是查看 onDelete 子句,在 2.25.0 及更早版本中,其默认值为 Cascade

警告

如果您正在使用 delete()deleteMany() 方法,现在将执行级联删除,因为 referentialActions 预览功能移除了 Prisma Client 中先前阻止运行时级联删除的安全网。务必检查您的代码并进行相应的调整。

请确保您对 schema 中所有 onDelete: Cascade 的情况感到满意。如果不是,请选择以下任一方式:

  • 修改您的 Prisma schema 并运行 db pushdev migrate 来更改数据库

或者

  • 如果您使用仅 Introspection 的工作流程,则手动更新底层数据库

以下示例将导致级联删除:如果 User 被删除,则其所有 Post 也将被删除。

一个博客 schema 示例

model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
}

model User {
id Int @id @default(autoincrement())
posts Post[]
}

使用 Migration (迁移)

当运行 Migration (迁移)(或 prisma db push 命令)时,新的默认设置将应用于您的数据库。

信息

与首次运行 Introspect 不同,新的引用操作子句和属性不会由 Prisma VSCode 扩展自动添加到您的 prisma schema 中。如果您希望使用除新默认设置之外的任何设置,则必须手动添加它们。

在您的 Prisma schema 中显式定义引用操作是可选的。如果您未显式定义关系引用操作,Prisma ORM 将使用新的默认设置

请注意,引用操作可以逐个关系添加。这意味着您可以将它们添加到单个关系中,而其余关系则通过不手动指定任何内容来保留默认设置。

检查错误

在升级到 2.26.0 并启用引用操作预览功能之前,Prisma ORM 在使用 delete()deleteMany() 时会阻止记录删除,以维护引用完整性。Prisma Client 会抛出一个自定义运行时错误,错误代码为 P2014

升级并启用引用操作预览功能后,Prisma ORM 不再执行运行时检查。您可以改为指定自定义引用操作来维护关系间的引用完整性。

当您使用 NoActionRestrict 阻止记录删除时,2.26.0 版本后的错误消息与 2.26.0 版本前不同。这是因为它们现在由数据库触发,而不是 Prisma Client。新的预期错误代码是 P2003

为确保您能捕获这些新错误,请相应地调整您的代码。

捕获错误的示例

以下示例使用了下方博客 schema,其中 PostUser 之间存在一对多关系,并在 author 字段上设置了 Restrict 引用操作。

这意味着如果一个用户有帖子,该用户(及其帖子)无法删除。

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id], onDelete: Restrict)
authorId String
}

model User {
id Int @id @default(autoincrement())
posts Post[]
}

在升级并启用引用操作预览功能之前,尝试删除有帖子的用户时会收到的错误代码是 P2014,其消息为

“您尝试进行的更改会违反 {model_a_name} 和 {model_b_name} 模型之间必需的关系 '{relation_name}'。”

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
try {
await prisma.user.delete({
where: {
id: 'some-long-id',
},
})
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2014') {
console.log(error.message)
}
}
}
}

main()

为确保您的代码检查正确的错误,请修改您的检查以查找 P2003,它将返回消息

“外键约束在字段上失败: {field_name}”

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
try {
await prisma.user.delete({
where: {
id: 'some-long-id'
}
})
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2014') {
if (error.code === 'P2003') {
console.log(error.message)
}
}
}
}

main()