NestJS‑TypeORM Integration: Should Migrations Execute Inside Automatic Transactions?
0 reputation · 17 Jan 2022, 01:32 UTC
0 reputation · 17 Jan 2022, 01:32 UTC
The goal is to guarantee that a schema‑change migration can be rolled back completely when it fails, preserving database consistency in a NestJS application that relies on TypeORM for data access.
By default TypeORM’s Migration interface runs SQL statements outside any NestJS‑managed transaction, so developers must manually start, commit, or roll back a QueryRunner transaction inside the up/down methods. The NestJS documentation does not define a default behavior, leaving the decision to the implementer and creating variability in error‑handling practices.
An unresolved design question is whether NestJS should provide a higher‑level wrapper (e.g., a MigrationService) that automatically encloses migrations in a transaction and handles compensation on failure, thereby reducing boilerplate and the risk of missed error handling.
Should NestJS ship a built‑in MigrationService that wraps migrations in a transaction? What are the trade‑offs of automatic transaction wrapping versus manual handling? How would such a wrapper interact with existing custom QueryRunner usage and third‑party migration libraries?
34775 reputation · 17 Jan 2022, 04:47 UTC
NestJS should not ship a built-in MigrationService to wrap migrations in transactions because TypeORM already handles this behavior by default. The assumption that migrations execute outside a transaction is generally incorrect for standard database configurations, provided the database supports transactional DDL.
By default, TypeORM uses a migrationsTransactionMode setting set to each. This means every individual migration's up method is executed within its own database transaction. If any SQL statement within that migration file fails, TypeORM automatically rolls back the transaction for that specific migration, ensuring the database schema remains in its state prior to that migration starting.
While a high-level NestJS wrapper might reduce boilerplate, there are significant trade-offs to consider:
CREATE TABLE statement once it has executed.QueryRunner logic. An automated wrapper might conflict with manual transaction management when used for complex data transformations, leading to nested transaction errors or "transaction already started" exceptions.migrationsTransactionMode: 'all') can cause long-held table locks, potentially blocking the application's startup sequence or deployment cycles.To ensure consistency in a NestJS environment without unnecessary wrappers, follow these steps:
DataSource options do not have migrationsTransactionMode set to none unless explicitly required.queryRunner passed into the up and down methods rather than creating a new connection. This ensures your statements are part of the transaction managed by the runner.down method or use pre-checks to ensure idempotency.// Example of a safe migration implementation
export async up(queryRunner: QueryRunner): Promise {
// The queryRunner is already inside a transaction started by TypeORM
await queryRunner.query(`CREATE TABLE "user" ("id" int PRIMARY KEY, "name" varchar)`);
n // If an error occurs here, TypeORM rolls back the table creation in PostgreSQL
}
To provide a more specific recommendation, are you using a database that supports transactional DDL (like PostgreSQL) or one that performs implicit commits on DDL (like MySQL)?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.