Bulk operations
massCreate, massUpdate, massSoftDelete, massRestore, massDelete: all transactional with rollback on failure.
Bulk operations run inside a single database transaction. If any row fails, the entire operation is rolled back and a BulkOperationException is thrown, carrying the per-row errors.
The five methods
await service.massCreate([dto1, dto2, dto3]);
await service.massUpdate([1, 2, 3], { status: 'archived' });
await service.massSoftDelete([1, 2, 3]);
await service.massRestore([1, 2, 3]);
await service.massDelete([1, 2, 3]);| Method | Returns | Failure mode |
|---|---|---|
massCreate(data[], options?) | Promise<T[]> | Throws BulkOperationException on the first failing row. |
massUpdate(ids[], data, options?) | Promise<T[]> | Same. |
massSoftDelete(ids[], options?) | Promise<boolean> | Same. |
massRestore(ids[], options?) | Promise<T[]> | Same. |
massDelete(ids[], options?) | Promise<boolean> | Same. |
Transactional semantics
All bulk methods wrap their work in a single transaction. If any row fails (constraint violation, FK error, validation, etc.), the transaction is rolled back. No partial writes are visible to subsequent reads.
A bulk operation that succeeds returns its full result. A bulk operation
that fails throws BulkOperationException and none of the rows are
modified. Use this as a guarantee, not just an optimization.
BulkOperationException
import { BulkOperationException } from 'nestjs-drizzle-crud';
try {
await users.massCreate([{ name: 'a' }, { name: 'b' /* missing required email */ }]);
} catch (err) {
if (err instanceof BulkOperationException) {
// err.errors: per-row errors
// err.index: index of the failing row
console.error(`Row ${err.index} failed:`, err.errors);
}
throw err;
}Catch BulkOperationException to surface per-row errors to the client. Otherwise let it propagate to your exception filter for a generic 500.
Lifecycle hooks during bulk operations
The standard beforeCreate / afterCreate (and the corresponding update / delete / softDelete / restore hooks) fire once per row, in array order. A massCreate([a, b, c]) calls beforeCreate three times before the insert, then the insert, then afterCreate three times.
If any row's beforeCreate throws, the operation is aborted before the insert. No rows are written.
Passing options
All bulk methods accept the same SqlOperationOptions as their non-bulk counterparts. In particular, you can pass a custom transaction:
await service.executeSqlTransaction(async (tx) => {
await service.massCreate(dtos, { transaction: tx });
await otherService.massUpdate(otherIds, { flag: true }, { transaction: tx });
});See Transactions for the full pattern.
Limits and pagination
Bulk methods don't paginate. They accept the full array in memory and execute it as a single transaction. For very large bulk operations (thousands of rows), consider:
- Chunking on the caller side. Process arrays in batches of 100–500 rows and call
massCreateper batch. - Switching to a streaming insert. Write a custom method that uses Drizzle's
db.insert(...).values(chunk).execute()chain with explicit chunking.
The cap is on the database side, not the package. Single transactions holding thousands of row locks can starve other connections.