Check for an active transaction via DB.getTransactionContext()
If a transaction exists, reuse it
If no transaction exists, create a new PuriWrapper
Copy
// Internal logic of getPuri (for reference)async getPuri(which: DBPreset): PuriWrapper { // 1. Attempt to get transaction from transaction context const trx = DB.getTransactionContext().getTransaction(which); if (trx) { // 2. If transaction exists, reuse it return trx; } // 3. If no transaction, return a new PuriWrapper const db = this.getDB(which); return new PuriWrapper(db, new UpsertBuilder());}
Thanks to this feature, it’s safe to call other methods within a transaction. All getPuri calls use the same transaction.
Copy
class UserModelClass extends BaseModelClass { @transactional() async createUser(email: string, name: string) { const wdb = this.getPuri("w"); const [user] = await wdb.table("users") .insert({ email, name }) .returning("*"); // Call another method - uses the same transaction await this.createDefaultProfile(user.id); return user; } // No @transactional, but uses the same transaction when called from above async createDefaultProfile(userId: number) { const wdb = this.getPuri("w"); // Automatically reuses the transaction from createUser await wdb.table("profiles") .insert({ user_id: userId, bio: "Welcome!" }); }}
The transaction context set by the @transactional decorator is automatically shared across all sub-method calls. This is implemented via AsyncLocalStorage.
A PuriWrapper obtained via getPuri within a transaction and a new transaction started with wdb.transaction() are different transactions. If you need nested transactions, you must manage them explicitly.
import { BaseModelClass } from "sonamu";class ReportModelClass extends BaseModelClass { async getComplexReport() { const rdb = this.getPuri("r"); // Complex query with raw SQL const [result] = await rdb.raw(` WITH monthly_sales AS ( SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as order_count, SUM(total_amount) as total FROM orders WHERE status = 'completed' GROUP BY month ) SELECT month, order_count, total, LAG(total) OVER (ORDER BY month) as prev_month_total, (total - LAG(total) OVER (ORDER BY month)) / LAG(total) OVER (ORDER BY month) * 100 as growth_rate FROM monthly_sales ORDER BY month DESC LIMIT 12 `); return result; } async searchFullText(keyword: string) { const rdb = this.getPuri("r"); // Full-text search const posts = await rdb.raw(` SELECT id, title, content, MATCH(title, content) AGAINST(? IN BOOLEAN MODE) as relevance FROM posts WHERE MATCH(title, content) AGAINST(? IN BOOLEAN MODE) ORDER BY relevance DESC LIMIT 20 `, [keyword, keyword]); return posts[0]; }}
Copy
import { BaseModelClass } from "sonamu";class BatchModelClass extends BaseModelClass { async batchUpdateStatus( userIds: number[], status: string ) { const wdb = this.getPuri("w"); // Process in batches of 500 const chunkSize = 500; let updated = 0; for (let i = 0; i < userIds.length; i += chunkSize) { const chunk = userIds.slice(i, i + chunkSize); const count = await wdb.table("users") .whereIn("id", chunk) .update({ status, updated_at: new Date() }); updated += count; } return { updated }; } async batchInsert(users: Array<{ email: string; name: string }>) { const wdb = this.getPuri("w"); return wdb.transaction(async (trx) => { // Register batches with UpsertBuilder users.forEach(user => { trx.ubRegister("users", { email: user.email, name: user.name, status: "active", created_at: new Date() }); }); // Upsert all at once const ids = await trx.ubUpsert("users", { chunkSize: 500 }); return { count: ids.length, ids }; }); }}
Copy
import { BaseModelClass } from "sonamu";class OrderModelClass extends BaseModelClass { async processOrder(orderId: number) { const wdb = this.getPuri("w"); return wdb.transaction(async (trx) => { // 1. Fetch order const [order] = await trx.table("orders") .where("id", orderId) .forUpdate() // Lock row .select(); if (!order) { throw new Error("Order not found"); } // 2. Fetch order items const items = await trx.table("order_items") .where("order_id", orderId) .select(); // 3. Check stock and deduct for (const item of items) { const [product] = await trx.table("products") .where("id", item.product_id) .forUpdate() .select(); if (product.stock < item.quantity) { throw new Error(`Insufficient stock for ${product.name}`); } await trx.table("products") .where("id", item.product_id) .decrement("stock", item.quantity); } // 4. Update order status await trx.table("orders") .where("id", orderId) .update({ status: "processing", processed_at: new Date() }); // 5. Record history await trx.table("order_history").insert({ order_id: orderId, status: "processing", created_at: new Date() }); return { success: true }; }); }}
// Use indexes in WHERE conditionsconst users = await rdb.table("users") .where("status", "active") // Index needed on status .where("created_at", ">", startDate) // Index needed on created_at .select();
// Process large data in batchesfor (let i = 0; i < ids.length; i += 500) { const chunk = ids.slice(i, i + 500); await wdb.table("users").whereIn("id", chunk).update({ ... });}