Overview
The .db() method binds a model to a specific database or transaction instance. This enables atomic multi-operation workflows where all changes succeed together or fail together.
Syntax
model.db(dbInstance) => Model
Parameters
dbInstance
DrizzleDB | Transaction
required
A Drizzle database instance or transaction object.Typically used within db.transaction() callback to bind models to the transaction.
Return Value
Returns a new model instance bound to the provided database/transaction. The original model is unchanged.
Basic Transaction
import { drizzle } from "drizzle-orm/node-postgres";
import { modelBuilder } from "@apisr/drizzle-model";
import * as schema from "./schema";
import { relations } from "./relations";
const db = drizzle(process.env.DATABASE_URL!, { schema, relations });
const model = modelBuilder({ db, schema, relations, dialect: "PostgreSQL" });
const userModel = model("user", {});
const postModel = model("post", {});
// Execute multiple operations atomically
await db.transaction(async (tx) => {
// Bind models to transaction
const txUser = userModel.db(tx);
const txPost = postModel.db(tx);
// All operations use the transaction
const user = await txUser.insert({
name: "Alice",
email: "alice@example.com",
age: 25,
}).returnFirst();
await txPost.insert({
title: "First Post",
content: "Hello world",
authorId: user.id,
});
// If any operation fails, everything rolls back
});
Examples
Transfer Operation
await db.transaction(async (tx) => {
const txAccount = accountModel.db(tx);
// Deduct from source account
const source = await txAccount
.where({ id: esc(sourceId) })
.update({ balance: sql`balance - ${amount}` })
.returnFirst();
if (!source || source.balance < 0) {
throw new Error("Insufficient funds");
}
// Add to destination account
await txAccount
.where({ id: esc(destId) })
.update({ balance: sql`balance + ${amount}` });
});
Create User with Profile
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
const txProfile = profileModel.db(tx);
const user = await txUser.insert({
email: "user@example.com",
name: "John Doe",
}).returnFirst();
await txProfile.insert({
userId: user.id,
bio: "Software developer",
avatar: "https://example.com/avatar.jpg",
});
});
Conditional Rollback
try {
await db.transaction(async (tx) => {
const txOrder = orderModel.db(tx);
const txInventory = inventoryModel.db(tx);
const order = await txOrder.insert({
userId: 123,
productId: 456,
quantity: 5,
}).returnFirst();
const inventory = await txInventory
.where({ productId: esc(456) })
.update({ stock: sql`stock - 5` })
.returnFirst();
if (inventory.stock < 0) {
throw new Error("Out of stock");
}
});
} catch (error) {
console.error("Transaction failed:", error);
// All changes are rolled back
}
Multiple Model Operations
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
const txPost = postModel.db(tx);
const txComment = commentModel.db(tx);
// Create user
const user = await txUser.insert({
name: "Alice",
email: "alice@example.com",
age: 25,
}).returnFirst();
// Create post
const post = await txPost.insert({
title: "My First Post",
content: "Hello world",
authorId: user.id,
}).returnFirst();
// Create comment
await txComment.insert({
content: "Great post!",
postId: post.id,
authorId: user.id,
});
// All succeed or all fail together
});
Safe Transaction
const result = await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
const user = await txUser
.insert({ ... })
.returnFirst()
.safe();
if (user.error) {
throw user.error; // Rollback transaction
}
return user.data;
});
Nested Transactions
Drizzle ORM supports savepoints for nested transactions:
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
const user = await txUser.insert({ ... }).returnFirst();
// Nested transaction (savepoint)
await tx.transaction(async (tx2) => {
const txPost = postModel.db(tx2);
await txPost.insert({
authorId: user.id,
title: "Post",
});
// Can rollback just this nested transaction
});
});
Read-Only Transactions
Some databases support read-only transaction mode:
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
const txPost = postModel.db(tx);
const users = await txUser.findMany();
const posts = await txPost.findMany();
// Consistent snapshot - both queries see same data state
return { users, posts };
}, {
accessMode: "read only", // PostgreSQL-specific
});
Transaction Isolation
await db.transaction(async (tx) => {
const txAccount = accountModel.db(tx);
// Your operations here
}, {
isolationLevel: "serializable", // Highest isolation
});
Available isolation levels (database-dependent):
"read uncommitted"
"read committed" (default for most DBs)
"repeatable read"
"serializable"
Error Handling
Automatic Rollback
try {
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
await txUser.insert({ ... });
throw new Error("Something went wrong");
// Transaction automatically rolls back
});
} catch (error) {
console.error("Transaction failed:", error);
}
Manual Rollback
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
const user = await txUser.insert({ ... }).returnFirst();
if (someCondition) {
tx.rollback(); // Explicit rollback
}
});
Safe Execution
const result = await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
const txPost = postModel.db(tx);
const userResult = await txUser
.insert({ ... })
.returnFirst()
.safe();
if (userResult.error) {
throw userResult.error;
}
const postResult = await txPost
.insert({ authorId: userResult.data.id })
.returnFirst()
.safe();
if (postResult.error) {
throw postResult.error;
}
return { user: userResult.data, post: postResult.data };
}).catch(error => ({ error }));
Transaction-Bound Models
The .db() method returns a new model instance. The original model is unchanged:
const userModel = model("user", {});
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
// txUser uses the transaction
await txUser.insert({ ... });
// Original userModel still uses main db
// await userModel.insert({ ... }); // ❌ Don't do this
});
// Outside transaction, use original model
await userModel.findMany(); // ✅ Uses main db
Keep Transactions Short
// ❌ Bad - long-running transaction holds locks
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
const user = await txUser.insert({ ... }).returnFirst();
// Expensive external API call
await fetch("https://slow-api.com/notify");
await txUser.update({ notified: true });
});
// ✅ Good - minimize transaction scope
const user = await db.transaction(async (tx) => {
return await userModel.db(tx)
.insert({ ... })
.returnFirst();
});
// Do slow operations outside transaction
await fetch("https://slow-api.com/notify");
await userModel
.where({ id: esc(user.id) })
.update({ notified: true });
Batch Operations
// ✅ Good - single transaction for related operations
await db.transaction(async (tx) => {
const txPost = postModel.db(tx);
await txPost.insert([
{ title: "Post 1", content: "..." },
{ title: "Post 2", content: "..." },
{ title: "Post 3", content: "..." },
]);
});
Type Safety
Transaction-bound models preserve all type information:
await db.transaction(async (tx) => {
const txUser = userModel.db(tx);
// Full type safety maintained
const user = await txUser.insert({
name: "Alice", // ✅ Valid
email: "alice@example.com", // ✅ Valid
// invalid: 123 // ❌ Type error
}).returnFirst();
// user type is correctly inferred
console.log(user.id); // ✅ TypeScript knows about 'id'
});
Notes
Always bind models to the transaction using .db(tx) at the start of the transaction callback. Don’t use the original model instance inside a transaction.
Keep transactions short and focused. Long-running transactions can cause database lock contention and performance issues.
Transaction isolation levels are database-specific. Check your database documentation for supported levels.