Skip to main content

Transactions

db.Tx runs a function in a transaction. It commits if the function returns nil, and rolls back on an error or a panic. tx is your own context type, bound to that transaction.

Tx​

err := db.Tx(ctx, func(tx *AppDB) error {
bob := &User{Name: "Bob", Email: "bob@example.com"}

if err := tx.Users.Insert(ctx, bob); err != nil {
return err
}

return tx.Posts.Insert(ctx, &Post{UserID: bob.ID, Title: "First"})
})

The error comes back to you unchanged. Reads inside see the transaction's own writes, and a panic rolls back and then keeps panicking.

Use tx inside the function: anything you do on db runs outside the transaction. Cancelling the context rolls it back.

SaveChanges in a transaction​

SaveChanges has its own implicit transaction, or joins the one it's in. If that transaction rolls back, the changes are pending again, and a rolled-back insert isn't tracked.

err := db.Tx(ctx, func(tx *AppDB) error {
user, err := tx.Users.Find(ctx, id)
if err != nil {
return err
}

user.Age++

return tx.SaveChanges(ctx)
})

ant.WithoutAutoTx() stops SaveChanges and Insert from opening their own, so a failure partway leaves earlier statements in place. Use it only to control transactions yourself.

Nesting​

A Tx inside a Tx becomes a savepoint, so only the inner part rolls back. That's also how you recover from a failed statement, which PostgreSQL otherwise won't let you do.

err := db.Tx(ctx, func(tx *AppDB) error {
if err := tx.Users.Insert(ctx, alice); err != nil {
return err
}

inner := tx.Tx(ctx, func(tx2 *AppDB) error {
return tx2.Users.Insert(ctx, bob)
})
if inner != nil {
log.Println("skipped bob:", inner)
}

return nil // alice commits either way
})

Begin​

ant.Begin hands you the transaction to finish yourself. Prefer Tx where you can, since it can't leave one open by accident.

tx, err := ant.Begin(ctx, db)
if err != nil {
return err
}

defer tx.Rollback()

if err := tx.Users.Insert(ctx, alice); err != nil {
return err
}

return tx.Commit()

The second end is ant.ErrTxDone, which makes that deferred Rollback harmless. db.Commit() outside a transaction is ant.ErrNotInTx.

Savepoints​

err := tx.Savepoint(ctx, "before")
err = tx.RollbackTo(ctx, "before")
err = tx.Release(ctx, "before")

Isolation and read-only​

Options go after the function, on Tx and Begin.

err := db.Tx(
ctx,
func(tx *AppDB) error {
return nil
},
ant.Isolation(sql.LevelSerializable),
)

err = db.Tx(
ctx,
func(tx *AppDB) error {
return nil
},
ant.ReadOnly(),
)