Transactions
Run several operations atomically with the driver's withSession and withTransaction, passing the session to each operation.
MongoZod has no transaction helper of its own: a transaction is the driver's. Start a session with
client.withSession(), run the operations in session.withTransaction(), and pass { session }
to each operation of a model, as you would to a collection of the driver.
import { ObjectId } from 'mongodb'
import { mongozod } from 'mongozod'
import * as mz from 'mongozod/mzod'
import * as z from 'zod'
const postSchema = z.object({
_id: mz.objectId()
.default(() => new ObjectId()),
title: z.string()
.min(1),
commentCount: z.number()
.int()
.min(0)
.default(0)
})
const PostsModel = mongozod({
collection: 'posts',
db,
schema: postSchema
})
const commentSchema = z.object({
_id: mz.objectId()
.default(() => new ObjectId()),
postId: mz.objectId(),
body: z.string()
.min(1)
})
const CommentsModel = mongozod({
collection: 'comments',
db,
schema: commentSchema
})
export async function addComment(postId: ObjectId, body: string) {
return client.withSession((session) =>
session.withTransaction(async () => {
const { document } = await CommentsModel.insertOne({ postId, body }, { session })
await PostsModel.updateOne({ _id: postId }, { $inc: { commentCount: 1 } }, { session })
return document
})
)
}withTransaction commits when the callback resolves, and returns what it returns. When the callback
throws, the transaction is aborted and the error is rethrown: a ZodError from the second write
undoes the first one. See the driver's
transactions guide.
Transactions need a replica set or a sharded cluster, as the MongoDB manual says.
Every operation takes the session
An operation without { session } runs outside the transaction, without an error: it doesn't see
the transaction's writes, and its own writes stay even when the transaction aborts. The session
option goes next to the driver's other options and MongoZod's:
await client.withSession((session) =>
session.withTransaction(async () => {
const post = await PostsModel.findOne({ title: 'Hello, MongoDB' }, { session })
if (!post) throw new Error('the post does not exist')
await CommentsModel.deleteMany({ postId: post._id }, { session })
await PostsModel.deleteOne({ _id: post._id }, { session })
})
)findandaggregate: a cursor keeps the session it was created with, so pass it in the options of the call,find(filter, { session }), not later.populate: its reads share the session of the read that asks for them, so a populated document written earlier in the transaction is found.
The client of a model
Code that has a model but not the client, such as a service, reaches it through the model's
database, the driver's Db, with db.client:
export async function renamePost(postId: ObjectId, title: string) {
await PostsModel.db.client.withSession((session) =>
session.withTransaction(async () => {
await PostsModel.updateOne({ _id: postId }, { $set: { title } }, { session })
})
)
}When the model's db is a function, PostsModel.db is the database it returns at that moment. See
Models.
Validation and the session
The schema's async refinements run without the session: a refinement that queries the database
reads outside the transaction, so it doesn't see what the transaction wrote. A check that has to
see it goes in a before hook of a plugin, which gets the session in the options of the
operation. A plugin's middleware can also run the whole operation in a transaction, with
next({ session }). See Writing plugins.
Common mistakes
- Leaving out
{ session }in one operation: it runs outside the transaction. A read doesn't see the transaction's writes, and a write is kept when the transaction aborts. Pass the session to every operation in the callback. - Catching an error inside the callback without rethrowing it: the driver can't tell whether the
transaction was aborted, and per the documentation of
withTransactionit may retry it without end. Let errors reachwithTransaction, or rethrow them. - Running operations in parallel with
Promise.all: the driver doesn't support parallel operations in one transaction. Await them one after the other. - Side effects in the callback, such as an email: the driver may call the callback more than
once to retry the transaction. Send it after
withTransactionresolves. - Checking uniqueness or balances with an async refinement: it reads outside the transaction.
Use a plugin's
beforehook with the session, or a unique index. - Running transactions against a standalone server: they need a replica set. In tests, run MongoDB as a single-node replica set.
Last updated on
MongoZod is an independent open-source project. It is not affiliated with, endorsed by or sponsored by MongoDB, Inc. MongoDB® is a registered trademark of MongoDB, Inc.