Transactions and batch queries
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.
For more in-depth examples and use cases, refer to the π transactions guide.
Transactions overviewβ
Before Prisma ORM version 4.4.0, you could not set isolation levels on transactions. The isolation level in your database configuration always applied.
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 supports six different ways of handling transactions for three different scenarios:
| Scenario | Available techniques |
|---|---|
| Dependent writes |
|
| Independent writes |
|
| Read, modify, write |
|
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.
About transactions in Prisma Clientβ
Prisma Client provides the following options for using transactions:
- Nested writes: use the Prisma Client API to process multiple operations on one or more related records inside the same transaction.
- Batch / bulk transactions: process one or more operations in bulk with
updateMany,deleteMany, andcreateMany. - The
$transactionAPI in Prisma Client:- Sequential operations: pass an array of Prisma Client queries to be executed sequentially inside a transaction, using
$transaction<R>(queries: PrismaPromise<R>[]): Promise<R[]>. - 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
- Sequential operations: pass an array of Prisma Client queries to be executed sequentially inside a transaction, using
Nested writesβ
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.
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 Slack on https://slack.prisma.io' },
{ title: 'Follow @prisma on Twitter' },
],
},
},
})
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' },
},
},
})
Refer to the π transactions guide for more examples.
Batch/bulk operationsβ
The following bulk operations run as transactions:
deleteManyupdateManycreateMany
Refer to the π transactions guide for more examples.
The $transaction APIβ
The $transaction API can be used in two ways:
-
Sequential operations: Pass an array of Prisma Client queries to be executed sequentially inside of a transaction.
$transaction<R>(queries: PrismaPromise<R>[]): Promise<R[]> -
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
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(),
])
You can also use raw queries inside of a $transaction:
- Relational databases
- MongoDB
const [userList, updateUser] = await prisma.$transaction([
prisma.$queryRaw`SELECT 'title' FROM User`,
prisma.$executeRaw`UPDATE User SET name = 'Hello' WHERE id = 2;`,
])
const [findRawData, aggregateRawData, commandRawData] =
await prisma.$transaction([
prisma.user.findRaw({
filter: { age: { $gt: 25 } },
}),
prisma.user.aggregateRaw({
pipeline: [
{ $match: { status: 'registered' } },
{ $group: { _id: '$country', total: { $sum: 1 } } },
],
}),
prisma.$runCommandRaw({
aggregate: 'User',
pipeline: [
{ $match: { name: 'Bob' } },
{ $project: { email: true, _id: false } },
],
explain: false,
}),
])
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.
Refer to the π transactions guide for more examples.
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: 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.
Interactive transactions have been generally available from version 4.7.0.
If you use interactive transactions in preview from version 2.29.0 to 4.6.1 (included), you need to add the interactiveTransactions preview feature to the generator block of your Prisma schema.
To use interactive transactions, you can pass an async function into $transaction.
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.
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.
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 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()
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.
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β
The transaction API has a second parameter. For interactive transactions, you can use the following optional configuration options in this parameter:
maxWait: The maximum amount of time Prisma Client will wait to acquire a transaction from the database. The default value is 2 seconds.timeout: The maximum amount of time the interactive transaction can run before being canceled and rolled back. The default value is 5 seconds.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β
This feature is not available on MongoDB, because MongoDB does not support isolation levels.
You can set the transaction isolation level for transactions.
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.
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β
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 supports the following isolation levels if they are available in the underlying database:
ReadUncommittedReadCommittedRepeatableReadSnapshotSerializable
The isolation levels available for each database connector are as follows:
| Database | ReadUncommitted | ReadCommitted | RepeatableRead | Snapshot | Serializable |
|---|---|---|---|---|---|
| PostgreSQL | βοΈ | βοΈ | βοΈ | No | βοΈ |
| MySQL | βοΈ | βοΈ | βοΈ | No | βοΈ |
| SQL Server | βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
| CockroachDB | No | No | No | No | βοΈ |
| SQLite | No | No | No | No | βοΈ |
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:
| Database | Default |
|---|---|
| PostgreSQL | ReadCommitted |
| MySQL | RepeatableRead |
| SQL Server | ReadCommitted |
| CockroachDB | Serializable |
| SQLite | Serializable |
Database-specific information on isolation levelsβ
See the following resources:
- Transaction isolation levels in PostgreSQL
- Transaction isolation levels in Microsoft SQL Server
- Transaction isolation levels in MySQL
CockroachDB and SQLite only support the Serializable isolation level.
Transaction timing issuesβ
- The solution in this section does not apply to MongoDB, because MongoDB does not support isolation levels.
- The timing issues discussed in this section do not apply to CockroachDB and SQLite, because these databases only support the highest
Serializableisolation level.
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:
- Transaction B:
createManyoperation creates a new set of rows. - Transaction B: The application commits transaction B.
- Transaction A:
createManyoperation. - Transaction A: The application commits transaction A. The new rows conflict with the rows that transaction B added at step 2.
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:
-
On your transaction, use the
isolationLevelparameter toPrisma.TransactionIsolationLevel.Serializable.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.
-
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
}
}
}
Dependent writesβ
Writes are considered dependent on each other if:
- Operations depend on the result of a preceding operation (for example, the database generating an ID)
The most common scenario is creating a record and using the generated ID to create or update a related record. Examples include:
- Creating a user and two related blog posts (a one-to-many relationship) - the author ID must be known before creating blog posts
- 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'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' },
],
},
},
})
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:
- β You want to create two or more records related by ID at the same time (for example, create a blog post and a user)
- β 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β
Consider the Slack sign-up flow, which:
- Creates a team
- 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:
- Creating the team succeeds - "Aurora Adventures" is now taken
- Creating and connecting the user fails - the team "Aurora Adventures" exists, but has no users
- 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.
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',
},
},
},
})
Furthermore, if an error occurs at any point, Prisma Client rolls back the entire transaction.
Nested writes FAQsβ
Why can't I use the $transaction([]) API to solve the same problem?β
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])
Nested writes support nested updates, but updates are not dependent writes - should I use the $transaction([]) API?β
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
- 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"
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
authorIdfield. 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.
Depending on your requirements, Prisma Client has four options for handling independent writes that should succeed or fail together.
Bulk operationsβ
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:
updateManydeleteManycreateMany
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β
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.
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
}
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,
},
})