--- title: Transactions description: Run writes in lagoon.Transaction, defer side effects with lagoon.AfterCommit until the commit, and install GORM callbacks from Boot with lagoon.OnDatabase. section: database order: 70 --- # Transactions Laravel's `DB::transaction` runs a closure in a transaction, and `DB::afterCommit` defers work until it commits. [lagoon](../../modules/lagoon/README.md) has the same pair, `lagoon.Transaction` and `lagoon.AfterCommit`, and the rest of the framework relies on them: realtime broadcasts, search index updates and blob deletions wait for the commit, so no client hears about a row that was rolled back. ## Running a transaction `lagoon.Transaction` runs a function in a transaction and commits when it returns `nil`. Inside the function, use the `ctx` and `tx` it receives for every write. Work registered with `lagoon.AfterCommit` runs, in registration order, only after the commit succeeds; when the function returns an error, the transaction rolls back and the work is dropped: ```go src=modules/lagoon/example_test.go#publish return lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error { if err := tx.Model(&Post{}).Where("id = ?", id).Update("title", "Published").Error; err != nil { return err } lagoon.AfterCommit(ctx, tx, func(ctx context.Context, db *gorm.DB) { *log = append(*log, fmt.Sprintf("post %d published", id)) // broadcast, index, send mail... }) if fail { return errors.New("rolled back") // the AfterCommit work never runs } return nil }) ``` The callback receives a database handle with an empty statement, so a query it runs never continues from the written model's statement. A panicking callback is logged and does not turn a committed write into an error. `lagoon.AfterCommit` behaves differently depending on where it is called: | Called | The work runs | |--------|---------------| | Inside `lagoon.Transaction` | After the outermost transaction commits; never after a rollback. | | In a GORM callback of a single-statement write (GORM's own implicit transaction) | After GORM commits that write; never when the write fails. | | Inside a plain `gorm.DB.Transaction` or another transaction lagoon did not open | Never. lagoon cannot see whether that transaction commits, so it logs a warning and skips the work. | | Outside any transaction | Immediately. | The third row is deliberate: running the work early could announce a write that later rolls back. When code in a transaction needs after-commit work, open the transaction with `lagoon.Transaction`. ## Nested transactions A `lagoon.Transaction` inside another becomes a savepoint. Its after-commit work joins the outer transaction's only when its own function succeeds, so work dropped with a failed savepoint never runs: ```go src=modules/lagoon/example_test.go#nested return lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error { lagoon.AfterCommit(ctx, tx, func(context.Context, *gorm.DB) { *log = append(*log, "outer") }) // A nested Transaction is a savepoint. Pass it the outer tx: given // the root db handle it returns an error instead. _ = lagoon.Transaction(ctx, tx, func(ctx context.Context, tx *gorm.DB) error { lagoon.AfterCommit(ctx, tx, func(context.Context, *gorm.DB) { *log = append(*log, "dropped") }) return errors.New("savepoint rolled back") }) return lagoon.Transaction(ctx, tx, func(ctx context.Context, tx *gorm.DB) error { lagoon.AfterCommit(ctx, tx, func(context.Context, *gorm.DB) { *log = append(*log, "inner") }) return nil }) }) ``` Pass the nested call the outer transaction's `tx`. Given the root database handle instead, the nested `lagoon.Transaction` returns an error without running its function. Otherwise it would open a second, independent transaction whose after-commit work would wait for the outer one. ## Callbacks registered at boot A hook that calls a service, such as a broadcast or a job dispatch, is a GORM callback rather than a model method (see [Models](models.md)). A plugin registers it from `Boot`, but `Boot` runs before the `serve` command opens the database. `lagoon.OnDatabase` bridges the gap: it runs your function as soon as the database is published, immediately when it already is. Register the callback before GORM's `gorm:commit_or_rollback_transaction` step and defer its side effect with `lagoon.AfterCommit`: ```go src=modules/lagoon/example_test.go#on-database return lagoon.OnDatabase(app, func(_ *sql.DB, gdb *gorm.DB) error { return gdb.Callback().Create().After("gorm:create").Before("gorm:commit_or_rollback_transaction").Register("acme:post_created", func(db *gorm.DB) { post, ok := db.Statement.Dest.(*Post) if db.Error != nil || !ok { return } lagoon.AfterCommit(db.Statement.Context, db, func(ctx context.Context, db *gorm.DB) { *log = append(*log, "created "+post.Slug) // runs only once the insert is committed }) }) }) ``` > [!WARNING] > Register such a callback with a `Before("gorm:commit_or_rollback_transaction")` constraint, as above. lagoon runs a single-statement write's after-commit work from its own callback right after that commit step; a callback that GORM sorts after it buffers work that is never run, without an error. ## Boot-time work that needs the database `lagoon.OnDatabase` is also the place for anything else a plugin must do with the database handle at start-up, such as registering a join table with `lagoon.RegisterJoinTable`. The error of a queued function is returned by `lagoon.Publish`, so a failing callback stops the start-up.