Skip to main content

事务和批量查询

数据库事务是指保证整体成功或失败的一系列读/写操作。本节介绍 Prisma 客户端 API 支持事务的方式。

¥A database transaction refers to a sequence of read/write operations that are guaranteed to either succeed or fail as a whole. This section describes the ways in which the Prisma Client API supports transactions.

事务概览

¥Transactions overview

info

在 Prisma ORM 版本 4.4.0 之前,你无法设置事务的隔离级别。始终应用数据库配置中的隔离级别。

¥Before Prisma ORM version 4.4.0, you could not set isolation levels on transactions. The isolation level in your database configuration always applied.

开发者通过将操作封装在事务中来利用数据库提供的安全保证。这些保证通常使用 ACID 缩写来概括:

¥Developers take advantage of the safety guarantees provided by the database by wrapping the operations in a transaction. These guarantees are often summarized using the ACID acronym:

  • 原子:确保事务的所有操作成功或全部失败。事务要么成功提交,要么中止并回滚。

    ¥Atomic: Ensures that either all or none operations of the transactions succeed. The transaction is either committed successfully or aborted and rolled back.

  • 持续的:确保事务前后的数据库状态有效(即维护有关数据的任何现有不变量)。

    ¥Consistent: Ensures that the states of the database before and after the transaction are valid (i.e. any existing invariants about the data are maintained).

  • 孤立:确保并发运行的事务具有与串行运行相同的效果。

    ¥Isolated: Ensures that concurrently running transactions have the same effect as if they were running in serial.

  • 耐用性:确保事务成功后,所有写入都会被持久存储。

    ¥Durability: Ensures that after the transaction succeeded, any writes are being stored persistently.

虽然这些属性中的每一个都存在很多模糊性和细微差别(例如,一致性实际上可以被视为应用级别的责任,而不是数据库属性,或者通常通过更强和更弱的隔离级别来保证隔离),但总体而言,它们可以作为开发者在考虑数据库事务时的期望的良好高级指南。

¥While there's a lot of ambiguity and nuance to each of these properties (for example, consistency could actually be considered an application-level responsibility rather than a database property or isolation is typically guaranteed in terms of stronger and weaker isolation levels), overall they serve as a good high-level guideline for expectations developers have when thinking about database transactions.

“事务是一个抽象层,它允许应用假装某些并发问题以及某些类型的硬件和软件故障不存在。一大类错误被简化为简单的事务中止,应用只需重试。” 设计数据密集型应用马丁·克莱普曼

¥"Transactions are an abstraction layer that allows an application to pretend that certain concurrency problems and certain kinds of hardware and software faults don’t exist. A large class of errors is reduced down to a simple transaction abort, and the application just needs to try again." Designing Data-Intensive Applications, Martin Kleppmann

Prisma Client 支持六种不同的事务处理方式,适用于三种不同的场景:

¥Prisma Client supports six different ways of handling transactions for three different scenarios:

设想可用技术
相关写入
  • Nested writes
独立写入
  • $transaction([]) API
  • Batch operations
读取、修改、写入
  • Idempotent operations
  • Optimistic concurrency control
  • Interactive transactions

你选择的技术取决于你的特定用例。

¥The technique you choose depends on your particular use case.

注意:就本指南而言,写入数据库包括创建、更新和删除数据。

¥Note: For the purposes of this guide, writing to a database encompasses creating, updating, and deleting data.

关于 Prisma 客户端中的事务

¥About transactions in Prisma Client

Prisma 客户端提供以下使用事务的选项:

¥Prisma Client provides the following options for using transactions:

  • 嵌套写入:使用 Prisma 客户端 API 处理同一事务中一个或多个相关记录的多个操作。

    ¥Nested writes: use the Prisma Client API to process multiple operations on one or more related records inside the same transaction.

  • 批量/批量事务:使用 updateManydeleteManycreateMany 批量处理一个或多个操作。

    ¥Batch / bulk transactions: process one or more operations in bulk with updateMany, deleteMany, and createMany.

  • Prisma 客户端中的 $transaction API:

    ¥The $transaction API in Prisma Client:

    • 顺序操作:使用 $transaction<R>(queries: PrismaPromise<R>[]): Promise<R[]> 传递要在事务内按顺序执行的 Prisma 客户端查询数组。

      ¥Sequential operations: pass an array of Prisma Client queries to be executed sequentially inside a transaction, using $transaction<R>(queries: PrismaPromise<R>[]): Promise<R[]>.

    • 互动事务:使用 $transaction<R>(fn: (prisma: PrismaClient) => R, options?: object): R 传递一个函数,该函数可以包含用户代码,包括 Prisma 客户端查询、非 Prisma 代码和要在事务中执行的其他控制流

      ¥Interactive transactions: pass a function that can contain user code including Prisma Client queries, non-Prisma code and other control flow to be executed in a transaction, using $transaction<R>(fn: (prisma: PrismaClient) => R, options?: object): R

嵌套写入

¥Nested writes

嵌套写入 允许你执行单个 Prisma 客户端 API 调用以及涉及多个 related 记录的多个操作。例如,与帖子一起创建用户或与发票一起更新订单。Prisma 客户端确保所有操作整体成功或失败。

¥A nested write lets you perform a single Prisma Client API call with multiple operations that touch multiple related records. For example, creating a user together with a post or updating an order together with an invoice. Prisma Client ensures that all operations succeed or fail as a whole.

以下示例演示了使用 create 的嵌套写入:

¥The following example demonstrates a nested write with create:

// Create a new user with two posts in a
// single transaction
const newUser: User = await prisma.user.create({
data: {
email: 'alice@prisma.io',
posts: {
create: [
{ title: 'Join the Prisma Discord at https://pris.ly/discord' },
{ title: 'Follow @prisma on Twitter' },
],
},
},
})

以下示例演示了使用 update 的嵌套写入:

¥The following example demonstrates a nested write with update:

// Change the author of a post in a single transaction
const updatedPost: Post = await prisma.post.update({
where: { id: 42 },
data: {
author: {
connect: { email: 'alice@prisma.io' },
},
},
})

批量/批量操作

¥Batch/bulk operations

以下批量操作作为事务运行:

¥The following bulk operations run as transactions:

  • createMany()

  • createManyAndReturn()

  • updateMany()

  • updateManyAndReturn()

  • deleteMany()

有关更多示例,请参阅有关 批量操作 的部分。

¥Refer to the section about bulk operations for more examples.

$transaction API

$transaction API 可以通过两种方式使用:

¥The $transaction API can be used in two ways:

  • 顺序操作:传递要在事务内按顺序执行的 Prisma 客户端查询数组。

    ¥Sequential operations: Pass an array of Prisma Client queries to be executed sequentially inside of a transaction.

    $transaction<R>(queries: PrismaPromise<R>[]): Promise<R[]>

  • 互动事务:传递一个可以包含用户代码的函数,包括 Prisma 客户端查询、非 Prisma 代码和要在事务中执行的其他控制流。

    ¥Interactive transactions: Pass a function that can contain user code including Prisma Client queries, non-Prisma code and other control flow to be executed in a transaction.

    $transaction<R>(fn: (prisma: PrismaClient) => R): R

顺序 Prisma 客户端操作

¥Sequential Prisma Client operations

以下查询返回与所提供的过滤器匹配的所有帖子以及所有帖子的计数:

¥The following query returns all posts that match the provided filter as well as a count of all posts:

const [posts, totalPosts] = await prisma.$transaction([
prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
prisma.post.count(),
])

你还可以在 $transaction 内使用原始查询:

¥You can also use raw queries inside of a $transaction:

import { selectUserTitles, updateUserName } from '@prisma/client/sql'

const [userList, updateUser] = await prisma.$transaction([
prisma.$queryRawTyped(selectUserTitles()),
prisma.$queryRawTyped(updateUserName(2)),
])

操作本身不是在执行时立即等待每个操作的结果,而是首先将其存储在变量中,然后使用名为 $transaction 的方法将其提交到数据库。Prisma 客户端将确保所有三个 create 操作都成功,或者都不成功。

¥Instead of immediately awaiting the result of each operation when it's performed, the operation itself is stored in a variable first which later is submitted to the database with a method called $transaction. Prisma Client will ensure that either all three create operations succeed or none of them succeed.

注意:操作按照它们在事务中放置的顺序执行。在事务中使用查询不会影响查询本身的操作顺序。

¥Note: Operations are executed according to the order they are placed in the transaction. Using a query in a transaction does not influence the order of operations in the query itself.

有关更多示例,请参阅有关 事务 API 的部分。

¥Refer to the section about the transactions API for more examples.

从版本 4.4.0 开始,顺序操作事务 API 有第二个参数。你可以在此参数中使用以下可选配置选项:

¥From version 4.4.0, the sequential operations transaction API has a second parameter. You can use the following optional configuration option in this parameter:

  • isolationLevel:设置 事务隔离级别。默认情况下,该值设置为数据库中当前配置的值。

    ¥isolationLevel: Sets the transaction isolation level. By default this is set to the value currently configured in your database.

例如:

¥For example:

await prisma.$transaction(
[
prisma.resource.deleteMany({ where: { name: 'name' } }),
prisma.resource.createMany({ data }),
],
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable, // optional, default defined by database configuration
}
)

互动事务

¥Interactive transactions

概述

¥Overview

有时你需要更多地控制事务中执行的查询。交互式事务旨在为你提供应急方案。

¥Sometimes you need more control over what queries execute within a transaction. Interactive transactions are meant to provide you with an escape hatch.

info

交互式事务从 4.7.0 版本开始普遍可用。

¥Interactive transactions have been generally available from version 4.7.0.

如果你在预览版中使用从 2.29.0 到 4.6.1(含)的交互式事务,则需要将 interactiveTransactions 预览功能添加到 Prisma 模式的生成器块中。

¥If you use interactive transactions in preview from version 2.29.0 to 4.6.1 (inclusive), you need to add the interactiveTransactions preview feature to the generator block of your Prisma schema.

要使用交互式事务,你可以将异步函数传递到 $transaction

¥To use interactive transactions, you can pass an async function into $transaction.

传递到该异步函数的第一个参数是 Prisma Client 的实例。下面,我们将该实例称为 tx。在此 tx 实例上调用的任何 Prisma 客户端调用都会封装到事务中。

¥The first argument passed into this async function is an instance of Prisma Client. Below, we will call this instance tx. Any Prisma Client call invoked on this tx instance is encapsulated into the transaction.

warning

谨慎使用交互式事务。长时间保持事务打开会损害数据库性能,甚至可能导致死锁。尽量避免在事务函数内执行网络请求和执行缓慢的查询。我们建议你尽快进出!

¥Use interactive transactions with caution. Keeping transactions open for a long time hurts database performance and can even cause deadlocks. Try to avoid performing network requests and executing slow queries inside your transaction functions. We recommend you get in and out as quick as possible!

示例

¥Example

让我们看一个例子:

¥Let's look at an example:

想象一下你正在构建一个网上银行系统。要执行的操作之一是将钱从一个人汇给另一个人。

¥Imagine that you are building an online banking system. One of the actions to perform is to send money from one person to another.

作为经验丰富的开发者,我们希望确保在转移过程中,

¥As experienced developers, we want to make sure that during the transfer,

  • 金额并没有消失

    ¥the amount doesn't disappear

  • 金额没有翻倍

    ¥the amount isn't doubled

这是交互式事务的一个很好的用例,因为我们需要在写入之间执行逻辑来检查余额。

¥This is a great use-case for interactive transactions because we need to perform logic in-between the writes to check the balance.

在下面的示例中,Alice 和 Bob 的账户中各有 100 美元。如果他们尝试汇出的资金多于实际金额,转账就会被拒绝。

¥In the example below, Alice and Bob each have $100 in their account. If they try to send more money than they have, the transfer is rejected.

Alice 预计能够以 100 美元进行一笔转账,而另一笔转账将被拒绝。这将导致 Alice 拥有 0 美元,Bob 拥有 200 美元。

¥Alice is expected to be able to make 1 transfer for $100 while the other transfer would be rejected. This would result in Alice having $0 and Bob having $200.

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

function transfer(from: string, to: string, amount: number) {
return prisma.$transaction(async (tx) => {
// 1. Decrement amount from the sender.
const sender = await tx.account.update({
data: {
balance: {
decrement: amount,
},
},
where: {
email: from,
},
})

// 2. Verify that the sender's balance didn't go below zero.
if (sender.balance < 0) {
throw new Error(`${from} doesn't have enough to send ${amount}`)
}

// 3. Increment the recipient's balance by amount
const recipient = await tx.account.update({
data: {
balance: {
increment: amount,
},
},
where: {
email: to,
},
})

return recipient
})
}

async function main() {
// This transfer is successful
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
// This transfer fails because Alice doesn't have enough funds in her account
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
}

main()

在上面的示例中,两个 update 查询都在数据库事务中运行。当应用到达函数末尾时,事务将提交到数据库。

¥In the example above, both update queries run within a database transaction. When the application reaches the end of the function, the transaction is committed to the database.

如果你的应用在此过程中遇到错误,异步函数将抛出异常并自动回滚事务。

¥If your application encounters an error along the way, the async function will throw an exception and automatically rollback the transaction.

要捕获异常,可以将 $transaction 封装在 try-catch 块中:

¥To catch the exception, you can wrap $transaction in a try-catch block:

try {
await prisma.$transaction(async (tx) => {
// Code running in a transaction...
})
} catch (err) {
// Handle the rollback...
}

事务选项

¥Transaction options

事务 API 有第二个参数。对于交互式事务,你可以在此参数中使用以下可选配置选项:

¥The transaction API has a second parameter. For interactive transactions, you can use the following optional configuration options in this parameter:

  • maxWait:Prisma 客户端等待从数据库获取事务的最长时间。默认值为 2 秒。

    ¥maxWait: The maximum amount of time Prisma Client will wait to acquire a transaction from the database. The default value is 2 seconds.

  • timeout:交互式事务在被取消和回滚之前可以运行的最长时间。默认值为 5 秒。

    ¥timeout: The maximum amount of time the interactive transaction can run before being canceled and rolled back. The default value is 5 seconds.

  • isolationLevel:设置 事务隔离级别。默认情况下,该值设置为数据库中当前配置的值。

    ¥isolationLevel: Sets the transaction isolation level. By default this is set to the value currently configured in your database.

例如:

¥For example:

await prisma.$transaction(
async (tx) => {
// Code running in a transaction...
},
{
maxWait: 5000, // default: 2000
timeout: 10000, // default: 5000
isolationLevel: Prisma.TransactionIsolationLevel.Serializable, // optional, default defined by database configuration
}
)

你还可以在构造函数级别全局设置这些:

¥You can also set these globally on the constructor-level:

const prisma = new PrismaClient({
transactionOptions: {
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 5000, // default: 2000
timeout: 10000, // default: 5000
},
})

事务隔离级别

¥Transaction isolation level

info

此功能在 MongoDB 上不可用,因为 MongoDB 不支持隔离级别。

¥This feature is not available on MongoDB, because MongoDB does not support isolation levels.

你可以设置事务 隔离级别 进行事务。

¥You can set the transaction isolation level for transactions.

info

这在以下 Prisma ORM 版本中可用,适用于版本 4.2.0 的交互式事务,适用于版本 4.4.0 的顺序操作。

¥This is available in the following Prisma ORM versions for interactive transactions from version 4.2.0, for sequential operations from version 4.4.0.

在 4.2.0(对于交互式事务)或 4.4.0(对于顺序操作)之前的版本中,你无法在 Prisma ORM 级别配置事务隔离级别。Prisma ORM 没有明确设置隔离级别,因此使用 在数据库中配置的隔离级别

¥In versions before 4.2.0 (for interactive transactions), or 4.4.0 (for sequential operations), you cannot configure the transaction isolation level at a Prisma ORM level. Prisma ORM does not explicitly set the isolation level, so the isolation level configured in your database is used.

设置隔离级别

¥Set the isolation level

要设置事务隔离级别,请在 API 的第二个参数中使用 isolationLevel 选项。

¥To set the transaction isolation level, use the isolationLevel option in the second parameter of the API.

对于顺序操作:

¥For sequential operations:

await prisma.$transaction(
[
// Prisma Client operations running in a transaction...
],
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable, // optional, default defined by database configuration
}
)

对于交互式事务:

¥For an interactive transaction:

await prisma.$transaction(
async (prisma) => {
// Code running in a transaction...
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable, // optional, default defined by database configuration
maxWait: 5000, // default: 2000
timeout: 10000, // default: 5000
}
)

支持的隔离级别

¥Supported isolation levels

Prisma Client 支持以下隔离级别(如果底层数据库中可用):

¥Prisma Client supports the following isolation levels if they are available in the underlying database:

  • ReadUncommitted

  • ReadCommitted

  • RepeatableRead

  • Snapshot

  • Serializable

每个数据库连接器可用的隔离级别如下:

¥The isolation levels available for each database connector are as follows:

数据库ReadUncommittedReadCommittedRepeatableReadSnapshotSerializable
PostgreSQL✔️✔️✔️✔️
MySQL✔️✔️✔️✔️
SQL Server✔️✔️✔️✔️✔️
CockroachDB✔️
SQLite✔️

默认情况下,Prisma 客户端将隔离级别设置为数据库中当前配置的值。

¥By default, Prisma Client sets the isolation level to the value currently configured in your database.

每个数据库默认配置的隔离级别如下:

¥The isolation levels configured by default in each database are as follows:

数据库默认
PostgreSQLReadCommitted
MySQLRepeatableRead
SQL ServerReadCommitted
CockroachDBSerializable
SQLiteSerializable

有关隔离级别的数据库特定信息

¥Database-specific information on isolation levels

请参阅以下资源:

¥See the following resources:

CockroachDB 和 SQLite 仅支持 Serializable 隔离级别。

¥CockroachDB and SQLite only support the Serializable isolation level.

事务时间问题

¥Transaction timing issues

info
  • 本节的解决方案不适用于 MongoDB,因为 MongoDB 不支持 隔离级别

    ¥The solution in this section does not apply to MongoDB, because MongoDB does not support isolation levels.

  • 本节讨论的时序问题不适用于 CockroachDB 和 SQLite,因为这些数据库仅支持最高的 Serializable 隔离级别。

    ¥The timing issues discussed in this section do not apply to CockroachDB and SQLite, because these databases only support the highest Serializable isolation level.

当两个或多个事务在某些 隔离级别 中并发运行时,时序问题可能会导致写入冲突或死锁,例如违反唯一约束。例如,考虑以下事件序列,其中事务 A 和事务 B 都尝试执行 deleteManycreateMany 操作:

¥When two or more transactions run concurrently in certain isolation levels, timing issues can cause write conflicts or deadlocks, such as the violation of unique constraints. For example, consider the following sequence of events where Transaction A and Transaction B both attempt to execute a deleteMany and a createMany operation:

  1. 事务 B:createMany 操作创建一组新的行。

    ¥Transaction B: createMany operation creates a new set of rows.

  2. 事务 B:应用提交事务 B。

    ¥Transaction B: The application commits transaction B.

  3. 事务 A:createMany 操作。

    ¥Transaction A: createMany operation.

  4. 事务 A:应用提交事务 A。新行与事务 B 在步骤 2 中添加的行冲突。

    ¥Transaction A: The application commits transaction A. The new rows conflict with the rows that transaction B added at step 2.

此冲突可能发生在隔离级别 ReadCommited 上,这是 PostgreSQL 和 Microsoft SQL Server 中的默认隔离级别。为了避免这个问题,你可以设置更高的隔离级别(RepeatableReadSerializable)。你可以设置事务的隔离级别。这会覆盖该事务的数据库隔离级别。

¥This conflict can occur at the isolation level ReadCommited, which is the default isolation level in PostgreSQL and Microsoft SQL Server. To avoid this problem, you can set a higher isolation level (RepeatableRead or Serializable). You can set the isolation level on a transaction. This overrides your database isolation level for that transaction.

为了避免事务写入冲突和事务死锁:

¥To avoid transaction write conflicts and deadlocks on a transaction:

  1. 在你的事务中,使用 isolationLevel 参数到 Prisma.TransactionIsolationLevel.Serializable

    ¥On your transaction, use the isolationLevel parameter to Prisma.TransactionIsolationLevel.Serializable.

    这可确保你的应用提交多个并发或并行事务,就像它们串行运行一样。当事务由于写入冲突或死锁而失败时,Prisma 客户端会返回 P2034 错误

    ¥This ensures that your application commits multiple concurrent or parallel transactions as if they were run serially. When a transaction fails due to a write conflict or deadlock, Prisma Client returns a P2034 error.

  2. 在你的应用代码中,在事务周围添加重试以处理任何 P2034 错误,如本示例所示:

    ¥In your application code, add a retry around your transaction to handle any P2034 errors, as shown in this example:

    import { Prisma, PrismaClient } from '@prisma/client'

    const prisma = new PrismaClient()
    async function main() {
    const MAX_RETRIES = 5
    let retries = 0

    let result
    while (retries < MAX_RETRIES) {
    try {
    result = await prisma.$transaction(
    [
    prisma.user.deleteMany({
    where: {
    /** args */
    },
    }),
    prisma.post.createMany({
    data: {
    /** args */
    },
    }),
    ],
    {
    isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
    }
    )
    break
    } catch (error) {
    if (error.code === 'P2034') {
    retries++
    continue
    }
    throw error
    }
    }
    }

Promise.all() 中使用 $transaction

¥Using $transaction within Promise.all()

如果你将 $transaction 封装在对 Promise.all() 的调用中,则事务内的查询将按顺序执行(即一个接一个):

¥If you wrap a $transaction inside a call to Promise.all(), the queries inside the transaction will be executed serially (i.e. one after another):

await prisma.$transaction(async (prisma) => {
await Promise.all([
prisma.user.findMany(),
prisma.user.findMany(),
prisma.user.findMany(),
prisma.user.findMany(),
prisma.user.findMany(),
prisma.user.findMany(),
prisma.user.findMany(),
prisma.user.findMany(),
prisma.user.findMany(),
prisma.user.findMany(),
])
})

这可能是违反直觉的,因为 Promise.all() 通常会并行化传递给它的调用。

¥This may be counterintuitive because Promise.all() usually parallelizes the calls passed into it.

这种行为的原因是:

¥The reason for this behaviour is that:

  • 一个事务意味着其中的所有查询都必须在同一连接上运行。

    ¥One transaction means that all queries inside it have to be run on the same connection.

  • 一个数据库连接一次只能执行一个查询。

    ¥A database connection can only ever execute one query at a time.

  • 由于一个查询在执行其工作时会阻止连接,因此将事务放入 Promise.all 中实际上意味着查询应该一个接一个地运行。

    ¥As one query blocks the connection while it is doing its work, putting a transaction into Promise.all effectively means that queries should be ran one after another.

相关写入

¥Dependent writes

在以下情况下,写入被视为相互依赖:

¥Writes are considered dependent on each other if:

  • 操作取决于前面操作的结果(例如,数据库生成 ID)

    ¥Operations depend on the result of a preceding operation (for example, the database generating an ID)

最常见的场景是创建记录并使用生成的 ID 创建或更新相关记录。示例包括:

¥The most common scenario is creating a record and using the generated ID to create or update a related record. Examples include:

  • 创建一个用户和两个相关的博客文章(一对多关系) - 创建博客文章之前必须知道作者 ID

    ¥Creating a user and two related blog posts (a one-to-many relationship) - the author ID must be known before creating blog posts

  • 创建团队并分配成员(多对多关系) - 在分配成员之前必须知道团队 ID

    ¥Creating a team and assigning members (a many-to-many relationship) - the team ID must be known before assigning members

相关写入必须同时成功,以保持数据一致性并防止意外行为,例如没有作者的博客文章或没有成员的团队。

¥Dependent writes must succeed together in order to maintain data consistency and prevent unexpected behavior, such as blog post without an author or a team without members.

嵌套写入

¥Nested writes

Prisma Client 针对依赖写入的解决方案是嵌套写入功能,createupdate 都支持该功能。以下嵌套写入创建一个用户和两篇博客文章:

¥Prisma Client's solution to dependent writes is the nested writes feature, which is supported by create and update. The following nested write creates one user and two blog posts:

const nestedWrite = await prisma.user.create({
data: {
email: 'imani@prisma.io',
posts: {
create: [
{ title: 'My first day at Prisma' },
{ title: 'How to configure a unique constraint in PostgreSQL' },
],
},
},
})

如果任何操作失败,Prisma 客户端将回滚整个事务。client.user.deleteManyclient.user.updateMany 等顶层批量操作当前不支持嵌套写入。

¥If any operation fails, Prisma Client rolls back the entire transaction. Nested writes are not currently supported by top-level bulk operations like client.user.deleteMany and client.user.updateMany.

何时使用嵌套写入

¥When to use nested writes

如果出现以下情况,请考虑使用嵌套写入:

¥Consider using nested writes if:

  • ✔ 你想要同时创建两个或多个通过 ID 关联的记录(例如,创建博客文章和用户)

    ¥✔ You want to create two or more records related by ID at the same time (for example, create a blog post and a user)

  • ✔ 你想要同时更新和创建按 ID 相关的记录(例如,更改用户名并创建新的博客文章)

    ¥✔ You want to update and create records related by ID at the same time (for example, change a user's name and create a new blog post)

设想:注册流程

¥Scenario: Sign-up flow

考虑 Slack 注册流程,其中:

¥Consider the Slack sign-up flow, which:

  1. 创建团队

    ¥Creates a team

  2. 将一名用户添加到该团队,该用户将自动成为该团队的管理员

    ¥Adds one user to that team, which automatically becomes that team's administrator

这种场景可以用下面的模式来表示 - 请注意,用户可以属于多个团队,并且团队可以拥有多个用户(多对多关系):

¥This scenario can be represented by the following schema - note that users can belong to many teams, and teams can have many users (a many-to-many relationship):

model Team {
id Int @id @default(autoincrement())
name String
members User[] // Many team members
}

model User {
id Int @id @default(autoincrement())
email String @unique
teams Team[] // Many teams
}

最直接的方法是创建一个团队,然后创建一个用户并将其附加到该团队:

¥The most straightforward approach is to create a team, then create and attach a user to that team:

// Create a team
const team = await prisma.team.create({
data: {
name: 'Aurora Adventures',
},
})

// Create a user and assign them to the team
const user = await prisma.user.create({
data: {
email: 'alice@prisma.io',
team: {
connect: {
id: team.id,
},
},
},
})

然而这段代码有一个问题 - 考虑以下场景:

¥However, this code has a problem - consider the following scenario:

  1. 创建团队成功 - "极光探险" 现已被占用

    ¥Creating the team succeeds - "Aurora Adventures" is now taken

  2. 创建并连接用户失败 - 团队 "极光探险" 存在,但没有用户

    ¥Creating and connecting the user fails - the team "Aurora Adventures" exists, but has no users

  3. 再次执行注册流程并尝试重新创建 "极光探险" 失败 - 该团队已经存在

    ¥Going through the sign-up flow again and attempting to recreate "Aurora Adventures" fails - the team already exists

创建团队和添加用户应该是一个原子操作,整体成功或失败。

¥Creating a team and adding a user should be one atomic operation that succeeds or fails as a whole.

要在底层数据库客户端中实现原子写入,你必须将插入封装在 BEGINCOMMITROLLBACK 语句中。Prisma Client 解决了 嵌套写入 的问题。以下查询创建一个团队、创建一个用户并在单个事务中连接记录:

¥To implement atomic writes in a low-level database clients, you must wrap your inserts in BEGIN, COMMIT and ROLLBACK statements. Prisma Client solves the problem with nested writes. The following query creates a team, creates a user, and connects the records in a single transaction:

const team = await prisma.team.create({
data: {
name: 'Aurora Adventures',
members: {
create: {
email: 'alice@prisma.io',
},
},
},
})

此外,如果任何时候发生错误,Prisma 客户端都会回滚整个事务。

¥Furthermore, if an error occurs at any point, Prisma Client rolls back the entire transaction.

嵌套写入常见问题解答

¥Nested writes FAQs

为什么我不能使用 $transaction([]) API 来解决同样的问题?

¥Why can't I use the $transaction([]) API to solve the same problem?

$transaction([]) API 不允许你在不同的操作之间传递 ID。在以下示例中,createUserOperation.id 尚不可用:

¥The $transaction([]) API does not allow you to pass IDs between distinct operations. In the following example, createUserOperation.id is not available yet:

const createUserOperation = prisma.user.create({
data: {
email: 'ebony@prisma.io',
},
})

const createTeamOperation = prisma.team.create({
data: {
name: 'Aurora Adventures',
members: {
connect: {
id: createUserOperation.id, // Not possible, ID not yet available
},
},
},
})

await prisma.$transaction([createUserOperation, createTeamOperation])
嵌套写入支持嵌套更新,但更新不依赖写入 - 我应该使用 $transaction([]) API 吗?

¥Nested writes support nested updates, but updates are not dependent writes - should I use the $transaction([]) API?

正确的说法是,因为你知道团队的 ID,所以你可以在 $transaction([]) 内独立更新团队及其团队成员。以下示例在 $transaction([]) 中执行这两个操作:

¥It is correct to say that because you know the ID of the team, you can update the team and its team members independently within a $transaction([]). The following example performs both operations in a $transaction([]):

const updateTeam = prisma.team.update({
where: {
id: 1,
},
data: {
name: 'Aurora Adventures Ltd',
},
})

const updateUsers = prisma.user.updateMany({
where: {
teams: {
some: {
id: 1,
},
},
name: {
equals: null,
},
},
data: {
name: 'Unknown User',
},
})

await prisma.$transaction([updateUsers, updateTeam])

但是,你可以通过嵌套写入获得相同的结果:

¥However, you can achieve the same result with a nested write:

const updateTeam = await prisma.team.update({
where: {
id: 1,
},
data: {
name: 'Aurora Adventures Ltd', // Update team name
members: {
updateMany: {
// Update team members that do not have a name
data: {
name: 'Unknown User',
},
where: {
name: {
equals: null,
},
},
},
},
},
})
我可以执行多个嵌套写入吗 - 例如,创建两个新团队并分配用户?

¥Can I perform multiple nested writes - for example, create two new teams and assign users?

是的,但这是场景和技术的组合:

¥Yes, but this is a combination of scenarios and techniques:

  • 创建团队和分配用户是依赖写入 - 使用嵌套写入

    ¥Creating a team and assigning users is a dependent write - use nested writes

  • 同时创建所有团队和用户是独立写入,因为团队/用户组合 #1 和团队/用户组合 #2 是不相关的写入 - 使用 $transaction([]) API

    ¥Creating all teams and users at the same time is an independent write because team/user combination #1 and team/user combination #2 are unrelated writes - use the $transaction([]) API

// Nested write
const createOne = prisma.team.create({
data: {
name: 'Aurora Adventures',
members: {
create: {
email: 'alice@prisma.io',
},
},
},
})

// Nested write
const createTwo = prisma.team.create({
data: {
name: 'Cool Crew',
members: {
create: {
email: 'elsa@prisma.io',
},
},
},
})

// $transaction([]) API
await prisma.$transaction([createTwo, createOne])

独立写入

¥Independent writes

如果写入不依赖于先前操作的结果,则写入被视为独立。以下几组独立写入可以按任何顺序发生:

¥Writes are considered independent if they do not rely on the result of a previous operation. The following groups of independent writes can occur in any order:

  • 将订单列表的状态字段更新为 "已派遣"

    ¥Updating the status field of a list of orders to "Dispatched"

  • 将电子邮件列表标记为 "读"

    ¥Marking a list of emails as "Read"

注意:如果存在约束,独立写入可能必须以特定顺序发生 - 例如,如果博客文章具有必填 authorId 字段,则你必须删除博客作者之前的博客文章。但是,它们仍然被视为独立写入,因为没有任何操作依赖于先前操作的结果,例如数据库返回生成的 ID。

¥Note: Independent writes may have to occur in a specific order if constraints are present - for example, you must delete blog posts before the blog author if the post have a mandatory authorId field. However, they are still considered independent writes because no operations depend on the result of a previous operation, such as the database returning a generated ID.

根据你的要求,Prisma 客户端有四个选项来处理应该一起成功或一起失败的独立写入。

¥Depending on your requirements, Prisma Client has four options for handling independent writes that should succeed or fail together.

批量操作

¥Bulk operations

批量写入允许你在单个事务中写入相同类型的多条记录 - 如果任何操作失败,Prisma 客户端将回滚整个事务。Prisma 客户端目前支持:

¥Bulk writes allow you to write multiple records of the same type in a single transaction - if any operation fails, Prisma Client rolls back the entire transaction. Prisma Client currently supports:

  • createMany()

  • createManyAndReturn()

  • updateMany()

  • updateManyAndReturn()

  • deleteMany()

何时使用批量操作

¥When to use bulk operations

如果出现以下情况,请考虑将批量操作作为解决方案:

¥Consider bulk operations as a solution if:

  • ✔ 你想要更新一批相同类型的记录,例如一批电子邮件

    ¥✔ You want to update a batch of the same type of record, like a batch of emails

设想:将电子邮件标记为已读

¥Scenario: Marking emails as read

你正在构建像 gmail.com 这样的服务,并且你的客户想要一个 "标记为已读" 功能,允许用户将所有电子邮件标记为已读。对电子邮件状态的每次更新都是一次独立写入,因为电子邮件彼此不依赖 - 例如,“生日快乐!🍰 你阿姨发来的电子邮件与宜家的促销电子邮件无关。

¥You are building a service like gmail.com, and your customer wants a "Mark as read" feature that allows users to mark all emails as read. Each update to the status of an email is an independent write because the emails do not depend on one another - for example, the "Happy Birthday! 🍰" email from your aunt is unrelated to the promotional email from IKEA.

在以下架构中,User 可以接收多封电子邮件(一对多关系):

¥In the following schema, a User can have many received emails (a one-to-many relationship):

model User {
id Int @id @default(autoincrement())
email String @unique
receivedEmails Email[] // Many emails
}

model Email {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int
subject String
body String
unread Boolean
}

基于此架构,你可以使用 updateMany 将所有未读电子邮件标记为已读:

¥Based on this schema, you can use updateMany to mark all unread emails as read:

await prisma.email.updateMany({
where: {
user: {
id: 10,
},
unread: true,
},
data: {
unread: false,
},
})

我可以在批量操作中使用嵌套写入吗?

¥Can I use nested writes with bulk operations?

不 - updateManydeleteMany 目前都不支持嵌套写入。例如,你不能删除多个团队及其所有成员(级联删除):

¥No - neither updateMany nor deleteMany currently supports nested writes. For example, you cannot delete multiple teams and all of their members (a cascading delete):

await prisma.team.deleteMany({
where: {
id: {
in: [2, 99, 2, 11],
},
},
data: {
members: {}, // Cannot access members here
},
})

我可以通过 $transaction([]) API 使用批量操作吗?

¥Can I use bulk operations with the $transaction([]) API?

是的 - 例如,你可以在 $transaction([]) 中包含多个 deleteMany 操作。

¥Yes — for example, you can include multiple deleteMany operations inside a $transaction([]).

$transaction([]) API

$transaction([]) API 是独立写入的通用解决方案,允许你将多个操作作为单个原子操作运行 - 如果任何操作失败,Prisma 客户端将回滚整个事务。

¥The $transaction([]) API is generic solution to independent writes that allows you to run multiple operations as a single, atomic operation - if any operation fails, Prisma Client rolls back the entire transaction.

还值得注意的是,操作是根据它们在事务中放置的顺序执行的。

¥Its also worth noting that operations are executed according to the order they are placed in the transaction.

await prisma.$transaction([iRunFirst, iRunSecond, iRunThird])

注意:在事务中使用查询不会影响查询本身的操作顺序。

¥Note: Using a query in a transaction does not influence the order of operations in the query itself.

随着 Prisma Client 的发展,$transaction([]) API 的用例将越来越多地被更专业的批量操作(例如 createMany)和嵌套写入所取代。

¥As Prisma Client evolves, use cases for the $transaction([]) API will increasingly be replaced by more specialized bulk operations (such as createMany) and nested writes.

何时使用 $transaction([]) API

¥When to use the $transaction([]) API

如果出现以下情况,请考虑使用 $transaction([]) API:

¥Consider the $transaction([]) API if:

  • ✔ 你想要更新包含不同类型记录(例如电子邮件和用户)的批次。这些记录不需要以任何方式相关。

    ¥✔ You want to update a batch that includes different types of records, such as emails and users. The records do not need to be related in any way.

  • ✔ 你想要批处理原始 SQL 查询 ($executeRaw) - 例如,Prisma Client 尚不支持的功能。

    ¥✔ You want to batch raw SQL queries ($executeRaw) - for example, for features that Prisma Client does not yet support.

设想:隐私立法

¥Scenario: Privacy legislation

GDPR 和其他隐私立法赋予用户权利要求组织删除其所有个人数据。在以下示例架构中,User 可以有许多帖子和私有消息:

¥GDPR and other privacy legislation give users the right to request that an organization deletes all of their personal data. In the following example schema, a User can have many posts and private messages:

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

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

model PrivateMessage {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int
message String
}

如果用户调用被遗忘权,我们必须删除三条记录:用户记录、私有消息和帖子。所有删除操作必须同时成功或根本不成功,这一点至关重要,这使得这成为事务的用例。然而,在这种情况下,使用像 deleteMany 这样的单个批量操作是不可能的,因为我们需要跨三个模型进行删除。相反,我们可以使用 $transaction([]) API 一起运行三个操作 - 两个 deleteMany 和一个 delete

¥If a user invokes the right to be forgotten, we must delete three records: the user record, private messages, and posts. It is critical that all delete operations succeed together or not at all, which makes this a use case for a transaction. However, using a single bulk operation like deleteMany is not possible in this scenario because we need to delete across three models. Instead, we can use the $transaction([]) API to run three operations together - two deleteMany and one delete:

const id = 9 // User to be deleted

const deletePosts = prisma.post.deleteMany({
where: {
userId: id,
},
})

const deleteMessages = prisma.privateMessage.deleteMany({
where: {
userId: id,
},
})

const deleteUser = prisma.user.delete({
where: {
id: id,
},
})

await prisma.$transaction([deletePosts, deleteMessages, deleteUser]) // Operations succeed or fail together

设想:预先计算的 ID 和 $transaction([]) API

¥Scenario: Pre-computed IDs and the $transaction([]) API

$transaction([]) API 不支持相关写入 - 如果操作 A 依赖于操作 B 生成的 ID,则使用 嵌套写入。但是,如果你预先计算了 ID(例如,通过生成 GUID),则你的写入将变得独立。考虑嵌套写入示例中的注册流程:

¥Dependent writes are not supported by the $transaction([]) API - if operation A relies on the ID generated by operation B, use nested writes. However, if you pre-computed IDs (for example, by generating GUIDs), your writes become independent. Consider the sign-up flow from the nested writes example:

await prisma.team.create({
data: {
name: 'Aurora Adventures',
members: {
create: {
email: 'alice@prisma.io',
},
},
},
})

TeamUserid 字段更改为 String,而不是自动生成 ID(如果你不提供值,则会自动生成 UUID)。此示例使用 UUID:

¥Instead of auto-generating IDs, change the id fields of Team and User to a String (if you do not provide a value, a UUID is generated automatically). This example uses UUIDs:

model Team {
id Int @id @default(autoincrement())
id String @id @default(uuid())
name String
members User[]
}

model User {
id Int @id @default(autoincrement())
id String @id @default(uuid())
email String @unique
teams Team[]
}

重构注册流程示例以使用 $transaction([]) API 而不是嵌套写入:

¥Refactor the sign-up flow example to use the $transaction([]) API instead of nested writes:

import { v4 } from 'uuid'

const teamID = v4()
const userID = v4()

await prisma.$transaction([
prisma.user.create({
data: {
id: userID,
email: 'alice@prisma.io',
team: {
id: teamID,
},
},
}),
prisma.team.create({
data: {
id: teamID,
name: 'Aurora Adventures',
},
}),
])

从技术上讲,如果你更喜欢这种语法,你仍然可以使用带有预先计算的 API 的嵌套写入:

¥Technically you can still use nested writes with pre-computed APIs if you prefer that syntax:

import { v4 } from 'uuid'

const teamID = v4()
const userID = v4()

await prisma.team.create({
data: {
id: teamID,
name: 'Aurora Adventures',
members: {
create: {
id: userID,
email: 'alice@prisma.io',
team: {
id: teamID,
},
},
},
},
})

如果你已经在使用自动生成的 ID 和嵌套写入,则没有令人信服的理由切换到手动生成的 ID 和 $transaction([]) API。

¥There's no compelling reason to switch to manually generated IDs and the $transaction([]) API if you are already using auto-generated IDs and nested writes.

读取、修改、写入

¥Read, modify, write

在某些情况下,你可能需要执行自定义逻辑作为原子操作的一部分 - 也称为 读-修改-写模式。以下是读取-修改-写入模式的示例:

¥In some cases you may need to perform custom logic as part of an atomic operation - also known as the read-modify-write pattern. The following is an example of the read-modify-write pattern:

  • 从数据库读取一个值

    ¥Read a value from the database

  • 运行一些逻辑来操纵该值(例如,联系外部 API)

    ¥Run some logic to manipulate that value (for example, contacting an external API)

  • 将值写回数据库

    ¥Write the value back to the database

所有操作都应该同时成功或失败,而不会对数据库进行不必要的更改,但你不一定需要使用实际的数据库事务。本指南的这一部分介绍了使用 Prisma Client 和读取-修改-写入模式的两种方法:

¥All operations should succeed or fail together without making unwanted changes to the database, but you do not necessarily need to use an actual database transaction. This section of the guide describes two ways to work with Prisma Client and the read-modify-write pattern:

  • 设计幂等 API

    ¥Designing idempotent APIs

  • 乐观并发控制

    ¥Optimistic concurrency control

幂等 API

¥Idempotent APIs

幂等性是指多次使用相同参数运行相同逻辑并获得相同结果的能力:无论运行逻辑一次还是一千次,对数据库的影响都是相同的。例如:

¥Idempotency is the ability to run the same logic with the same parameters multiple times with the same result: the effect on the database is the same whether you run the logic once or one thousand times. For example:

  • NOT IDEMPOTENT:在数据库中更新插入(更新或插入)电子邮件地址为 "letoya@prisma.io" 的用户。User 表不强制执行唯一的电子邮件地址。如果运行逻辑一次(创建一个用户)或十次(创建十个用户),对数据库的影响会有所不同。

    ¥NOT IDEMPOTENT: Upsert (update-or-insert) a user in the database with email address "letoya@prisma.io". The User table does not enforce unique email addresses. The effect on the database is different if you run the logic once (one user created) or ten times (ten users created).

  • IDEMPOTENT:在数据库中更新插入(更新或插入)电子邮件地址为 "letoya@prisma.io" 的用户。User 表确实强制执行唯一的电子邮件地址。如果运行逻辑一次(创建一个用户)或十次(使用相同的输入更新现有用户),对数据库的影响是相同的。

    ¥IDEMPOTENT: Upsert (update-or-insert) a user in the database with the email address "letoya@prisma.io". The User table does enforce unique email addresses. The effect on the database is the same if you run the logic once (one user created) or ten times (existing user is updated with the same input).

幂等性是你可以并且应该尽可能积极地设计到你的应用中的东西。

¥Idempotency is something you can and should actively design into your application wherever possible.

何时设计幂等 API

¥When to design an idempotent API

  • ✔ 你需要能够重试相同的逻辑,而不会在数据库中产生不需要的副作用

    ¥✔ You need to be able to retry the same logic without creating unwanted side-effects in the databases

设想:升级 Slack 团队

¥Scenario: Upgrading a Slack team

你正在为 Slack 创建一个升级流程,允许团队解锁付费功能。团队可以选择不同的计划并按用户每月付费。你使用 Stripe 作为支付网关,并扩展 Team 模型来存储 stripeCustomerId。订阅在 Stripe 中管理。

¥You are creating an upgrade flow for Slack that allows teams to unlock paid features. Teams can choose between different plans and pay per user, per month. You use Stripe as your payment gateway, and extend your Team model to store a stripeCustomerId. Subscriptions are managed in Stripe.

model Team {
id Int @id @default(autoincrement())
name String
User User[]
stripeCustomerId String?
}

升级流程如下所示:

¥The upgrade flow looks like this:

  1. 统计用户数量

    ¥Count the number of users

  2. 在 Stripe 中创建包含用户数量的订阅

    ¥Create a subscription in Stripe that includes the number of users

  3. 将团队与 Stripe 客户 ID 关联以解锁付费功能

    ¥Associate the team with the Stripe customer ID to unlock paid features

const teamId = 9
const planId = 'plan_id'

// Count team members
const numTeammates = await prisma.user.count({
where: {
teams: {
some: {
id: teamId,
},
},
},
})

// Create a customer in Stripe for plan-9454549
const customer = await stripe.customers.create({
externalId: teamId,
plan: planId,
quantity: numTeammates,
})

// Update the team with the customer id to indicate that they are a customer
// and support querying this customer in Stripe from our application code.
await prisma.team.update({
data: {
customerId: customer.id,
},
where: {
id: teamId,
},
})

这个例子有一个问题:你只能运行该逻辑一次。考虑以下场景:

¥This example has a problem: you can only run the logic once. Consider the following scenario:

  1. Stripe 创建新客户和订阅,并返回客户 ID

    ¥Stripe creates a new customer and subscription, and returns a customer ID

  2. 更新团队失败 - 该团队未在 Slack 数据库中标记为客户

    ¥Updating the team fails - the team is not marked as a customer in the Slack database

  3. 客户由 Stripe 收费,但付费功能在 Slack 中未解锁,因为团队缺乏有效的 customerId

    ¥The customer is charged by Stripe, but paid features are not unlocked in Slack because the team lacks a valid customerId

  4. 再次运行相同的代码:

    ¥Running the same code again either:

    • 导致错误,因为团队(由 externalId 定义)已存在 - Stripe 永远不会返回客户 ID

      ¥Results in an error because the team (defined by externalId) already exists - Stripe never returns a customer ID

    • 如果 externalId 不受唯一约束,Stripe 会创建另一个订阅(非幂等)

      ¥If externalId is not subject to a unique constraint, Stripe creates yet another subscription (not idempotent)

如果出现错误,你无法重新运行此代码,并且在不收取两次费用的情况下无法更改为其他计划。

¥You cannot re-run this code in case of an error and you cannot change to another plan without being charged twice.

以下重构(高亮)引入了一种机制,用于检查订阅是否已存在,并创建描述或更新现有订阅(如果输入相同,则订阅将保持不变):

¥The following refactor (highlighted) introduces a mechanism that checks if a subscription already exists, and either creates the description or updates the existing subscription (which will remain unchanged if the input is identical):

// Calculate the number of users times the cost per user
const numTeammates = await prisma.user.count({
where: {
teams: {
some: {
id: teamId,
},
},
},
})

// Find customer in Stripe
let customer = await stripe.customers.get({ externalId: teamID })

if (customer) {
// If team already exists, update
customer = await stripe.customers.update({
externalId: teamId,
plan: 'plan_id',
quantity: numTeammates,
})
} else {
customer = await stripe.customers.create({
// If team does not exist, create customer
externalId: teamId,
plan: 'plan_id',
quantity: numTeammates,
})
}

// Update the team with the customer id to indicate that they are a customer
// and support querying this customer in Stripe from our application code.
await prisma.team.update({
data: {
customerId: customer.id,
},
where: {
id: teamId,
},
})

现在,你可以使用相同的输入多次重试相同的逻辑,而不会产生不利影响。为了进一步增强此示例,你可以引入一种机制,如果在一定次数的尝试后更新未成功,则取消或暂时停用订阅。

¥You can now retry the same logic multiple times with the same input without adverse effect. To further enhance this example, you can introduce a mechanism whereby the subscription is cancelled or temporarily deactivated if the update does not succeed after a set number of attempts.

乐观并发控制

¥Optimistic concurrency control

乐观并发控制 (OCC) 是一种用于处理不依赖于 🔒 锁定的单个实体上的并发操作的模型。相反,我们乐观地假设记录在读取和写入之间保持不变,并使用并发令牌(时间戳或版本字段)来检测记录的更改。

¥Optimistic concurrency control (OCC) is a model for handling concurrent operations on a single entity that does not rely on 🔒 locking. Instead, we optimistically assume that a record will remain unchanged in between reading and writing, and use a concurrency token (a timestamp or version field) to detect changes to a record.

如果发生冲突(自你读取记录以来其他人已更改该记录),你可以取消事务。根据你的情况,你可以:

¥If a ❌ conflict occurs (someone else has changed the record since you read it), you cancel the transaction. Depending on your scenario, you can then:

  • 重试事务(预订另一个电影院座位)

    ¥Re-try the transaction (book another cinema seat)

  • 抛出错误(提醒用户他们将要覆盖其他人所做的更改)

    ¥Throw an error (alert the user that they are about to overwrite changes made by someone else)

本节介绍如何构建你自己的乐观并发控制。也可以看看:GitHub 上的应用级乐观并发控制 的计划

¥This section describes how to build your own optimistic concurrency control. See also: Plans for application-level optimistic concurrency control on GitHub

info
  • 如果你使用 4.4.0 或更早版本,则无法对 update 操作使用乐观并发控制,因为无法筛选非唯一字段。你需要与乐观并发控制一起使用的 version 字段是一个非唯一字段。

    ¥If you use version 4.4.0 or earlier, you cannot use optimistic concurrency control on update operations, because you cannot filter on non-unique fields. The version field you need to use with optimistic concurrency control is a non-unique field.

  • 从版本 5.0.0 开始,你可以 筛选 update 操作中的非唯一字段 以便使用乐观并发控制。该功能还可以通过版本 4.5.0 到 4.16.2 的预览标志 extendedWhereUnique 来使用。

    ¥Since version 5.0.0 you are able to filter on non-unique fields in update operations so that optimistic concurrency control is being used. The feature was also available via the Preview flag extendedWhereUnique from versions 4.5.0 to 4.16.2.

何时使用乐观并发控制

¥When to use optimistic concurrency control

  • ✔ 你预计会有大量并发请求(多人预订电影院座位)

    ¥✔ You anticipate a high number of concurrent requests (multiple people booking cinema seats)

  • ✔ 你预计这些并发请求之间的冲突很少见

    ¥✔ You anticipate that conflicts between those concurrent requests will be rare

避免在具有大量并发请求的应用中锁定可以使应用更能适应负载并具有更好的整体可扩展性。虽然锁定本身并不是坏事,但在高并发环境中锁定可能会导致意想不到的后果 - 即使你锁定单个行,并且只锁定很短的时间。有关更多信息,请参阅:

¥Avoiding locks in an application with a high number of concurrent requests makes the application more resilient to load and more scalable overall. Although locking is not inherently bad, locking in a high concurrency environment can lead to unintended consequences - even if you are locking individual rows, and only for a short amount of time. For more information, see:

设想:在电影院预订座位

¥Scenario: Reserving a seat at the cinema

你正在为电影院创建一个预订系统。每部电影都有固定数量的座位。以下架构对电影和座位进行建模:

¥You are creating a booking system for a cinema. Each movie has a set number of seats. The following schema models movies and seats:

model Seat {
id Int @id @default(autoincrement())
userId Int?
claimedBy User? @relation(fields: [userId], references: [id])
movieId Int
movie Movie @relation(fields: [movieId], references: [id])
}

model Movie {
id Int @id @default(autoincrement())
name String @unique
seats Seat[]
}

以下示例代码查找第一个可用席位并将该席位分配给用户:

¥The following sample code finds the first available seat and assigns that seat to a user:

const movieName = 'Hidden Figures'

// Find first available seat
const availableSeat = await prisma.seat.findFirst({
where: {
movie: {
name: movieName,
},
claimedBy: null,
},
})

// Throw an error if no seats are available
if (!availableSeat) {
throw new Error(`Oh no! ${movieName} is all booked.`)
}

// Claim the seat
await prisma.seat.update({
data: {
claimedBy: userId,
},
where: {
id: availableSeat.id,
},
})

然而,这段代码受到 "重复预订问题" 的影响 - 两个人可以预订相同的座位:

¥However, this code suffers from the "double-booking problem" - it is possible for two people to book the same seats:

  1. 座位 3A 返回索查 (findFirst)

    ¥Seat 3A returned to Sorcha (findFirst)

  2. 座位 3A 返回给 Ellen (findFirst)

    ¥Seat 3A returned to Ellen (findFirst)

  3. 座位 3A 由 Sorcha (update) 认领

    ¥Seat 3A claimed by Sorcha (update)

  4. 座位 3A 由 Ellen 认领(update - 覆盖 Sorcha 的认领)

    ¥Seat 3A claimed by Ellen (update - overwrites Sorcha's claim)

尽管 Sorcha 已成功预订座位,但系统最终还是存储了 Ellen 的申请。为了用乐观并发控制来解决这个问题,在席位上添加一个 version 字段:

¥Even though Sorcha has successfully booked the seat, the system ultimately stores Ellen's claim. To solve this problem with optimistic concurrency control, add a version field to the seat:

model Seat {
id Int @id @default(autoincrement())
userId Int?
claimedBy User? @relation(fields: [userId], references: [id])
movieId Int
movie Movie @relation(fields: [movieId], references: [id])
version Int
}

接下来,调整代码以在更新之前检查 version 字段:

¥Next, adjust the code to check the version field before updating:

const userEmail = 'alice@prisma.io'
const movieName = 'Hidden Figures'

// Find the first available seat
// availableSeat.version might be 0
const availableSeat = await client.seat.findFirst({
where: {
Movie: {
name: movieName,
},
claimedBy: null,
},
})

if (!availableSeat) {
throw new Error(`Oh no! ${movieName} is all booked.`)
}

// Only mark the seat as claimed if the availableSeat.version
// matches the version we're updating. Additionally, increment the
// version when we perform this update so all other clients trying
// to book this same seat will have an outdated version.
const seats = await client.seat.updateMany({
data: {
claimedBy: userEmail,
version: {
increment: 1,
},
},
where: {
id: availableSeat.id,
version: availableSeat.version, // This version field is the key; only claim seat if in-memory version matches database version, indicating that the field has not been updated
},
})

if (seats.count === 0) {
throw new Error(`That seat is already booked! Please try again.`)
}

现在两个人不可能预订同一个座位:

¥It is now impossible for two people to book the same seat:

  1. 座位 3A 返回 Sorcha(version 为 0)

    ¥Seat 3A returned to Sorcha (version is 0)

  2. 座位 3A 返回给 Ellen(version 为 0)

    ¥Seat 3A returned to Ellen (version is 0)

  3. Sorcha 认领座位 3A(version 增加为 1,预订成功)

    ¥Seat 3A claimed by Sorcha (version is incremented to 1, booking succeeds)

  4. Ellen 声明的座位 3A(内存中 version (0) 与数据库 version (1) 不匹配) - 预订不成功)

    ¥Seat 3A claimed by Ellen (in-memory version (0) does not match database version (1) - booking does not succeed)

互动事务

¥Interactive transactions

如果你有一个现有的应用,那么重构你的应用以使用乐观并发控制可能是一项艰巨的任务。交互式事务为此类情况提供了一个有用的应急方案。

¥If you have an existing application, it can be a significant undertaking to refactor your application to use optimistic concurrency control. Interactive Transactions offers a useful escape hatch for cases like this.

要创建交互式事务,请将异步函数传递到 $事务

¥To create an interactive transaction, pass an async function into $transaction.

传递到该异步函数的第一个参数是 Prisma Client 的实例。下面,我们将该实例称为 tx。在此 tx 实例上调用的任何 Prisma 客户端调用都会封装到事务中。

¥The first argument passed into this async function is an instance of Prisma Client. Below, we will call this instance tx. Any Prisma Client call invoked on this tx instance is encapsulated into the transaction.

在下面的示例中,Alice 和 Bob 的账户中各有 100 美元。如果他们尝试汇出的资金多于实际金额,转账就会被拒绝。

¥In the example below, Alice and Bob each have $100 in their account. If they try to send more money than they have, the transfer is rejected.

预期结果是 Alice 进行一笔 100 美元的转账,而另一笔转账将被拒绝。这将导致 Alice 拥有 0 美元,Bob 拥有 200 美元。

¥The expected outcome would be for Alice to make 1 transfer for $100 and the other transfer would be rejected. This would result in Alice having $0 and Bob having $200.

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

async function transfer(from: string, to: string, amount: number) {
return await prisma.$transaction(async (tx) => {
// 1. Decrement amount from the sender.
const sender = await tx.account.update({
data: {
balance: {
decrement: amount,
},
},
where: {
email: from,
},
})

// 2. Verify that the sender's balance didn't go below zero.
if (sender.balance < 0) {
throw new Error(`${from} doesn't have enough to send ${amount}`)
}

// 3. Increment the recipient's balance by amount
const recipient = tx.account.update({
data: {
balance: {
increment: amount,
},
},
where: {
email: to,
},
})

return recipient
})
}

async function main() {
// This transfer is successful
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
// This transfer fails because Alice doesn't have enough funds in her account
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
}

main()

在上面的示例中,两个 update 查询都在数据库事务中运行。当应用到达函数末尾时,事务将提交到数据库。

¥In the example above, both update queries run within a database transaction. When the application reaches the end of the function, the transaction is committed to the database.

如果应用在此过程中遇到错误,异步函数将抛出异常并自动回滚事务。

¥If the application encounters an error along the way, the async function will throw an exception and automatically rollback the transaction.

你可以在本 section 中了解有关交互式事务的更多信息。

¥You can learn more about interactive transactions in this section.

warning

谨慎使用交互式事务。长时间保持事务打开会损害数据库性能,甚至可能导致死锁。尽量避免在事务函数内执行网络请求和执行缓慢的查询。我们建议你尽快进出!

¥Use interactive transactions with caution. Keeping transactions open for a long time hurts database performance and can even cause deadlocks. Try to avoid performing network requests and executing slow queries inside your transaction functions. We recommend you get in and out as quick as possible!

结论

¥Conclusion

Prisma Client 支持多种处理事务的方式,可以直接通过 API,也可以支持你在应用中引入乐观并发控制和幂等性的能力。如果你觉得你的应用中存在任何建议选项未涵盖的用例,请打开 GitHub 问题 开始讨论。

¥Prisma Client supports multiple ways of handling transactions, either directly through the API or by supporting your ability to introduce optimistic concurrency control and idempotency into your application. If you feel like you have use cases in your application that are not covered by any of the suggested options, please open a GitHub issue to start a discussion.