Transactions

Starting A Transaction

  1. // GenTx generates group of entities in a transaction.
  2. func GenTx(ctx context.Context, client *ent.Client) error {
  3. tx, err := client.Tx(ctx)
  4. if err != nil {
  5. return fmt.Errorf("starting a transaction: %w", err)
  6. }
  7. hub, err := tx.Group.
  8. Create().
  9. SetName("Github").
  10. Save(ctx)
  11. if err != nil {
  12. return rollback(tx, fmt.Errorf("failed creating the group: %w", err))
  13. }
  14. // Create the admin of the group.
  15. dan, err := tx.User.
  16. Create().
  17. SetAge(29).
  18. SetName("Dan").
  19. AddManage(hub).
  20. Save(ctx)
  21. if err != nil {
  22. return rollback(tx, err)
  23. }
  24. // Create user "Ariel".
  25. a8m, err := tx.User.
  26. Create().
  27. SetAge(30).
  28. SetName("Ariel").
  29. AddGroups(hub).
  30. AddFriends(dan).
  31. Save(ctx)
  32. if err != nil {
  33. return rollback(tx, err)
  34. }
  35. fmt.Println(a8m)
  36. // Output:
  37. // User(id=2, age=30, name=Ariel)
  38. // Commit the transaction.
  39. return tx.Commit()
  40. }
  41. // rollback calls to tx.Rollback and wraps the given error
  42. // with the rollback error if occurred.
  43. func rollback(tx *ent.Tx, err error) error {
  44. if rerr := tx.Rollback(); rerr != nil {
  45. err = fmt.Errorf("%w: %v", err, rerr)
  46. }
  47. return err
  48. }

You must call Unwrap() if you are querying edges off of a created entity after a successful transaction (example: a8m.QueryGroups()). Unwrap restores the state of the underlying client embedded within the entity to a non-transactable version.

The full example exists in GitHub.

Transactional Client

Sometimes, you have an existing code that already works with *ent.Client, and you want to change it (or wrap it) to interact with transactions. For these use cases, you have a transactional client. An *ent.Client that you can get from an existing transaction.

  1. // WrapGen wraps the existing "Gen" function in a transaction.
  2. func WrapGen(ctx context.Context, client *ent.Client) error {
  3. tx, err := client.Tx(ctx)
  4. if err != nil {
  5. return err
  6. }
  7. txClient := tx.Client()
  8. // Use the "Gen" below, but give it the transactional client; no code changes to "Gen".
  9. if err := Gen(ctx, txClient); err != nil {
  10. return rollback(tx, err)
  11. }
  12. return tx.Commit()
  13. }
  14. // Gen generates a group of entities.
  15. func Gen(ctx context.Context, client *ent.Client) error {
  16. // ...
  17. return nil
  18. }

The full example exists in GitHub.

Best Practices

Reusable function that runs callbacks in a transaction:

  1. func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
  2. tx, err := client.Tx(ctx)
  3. if err != nil {
  4. return err
  5. }
  6. defer func() {
  7. if v := recover(); v != nil {
  8. tx.Rollback()
  9. panic(v)
  10. }
  11. }()
  12. if err := fn(tx); err != nil {
  13. if rerr := tx.Rollback(); rerr != nil {
  14. err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr)
  15. }
  16. return err
  17. }
  18. if err := tx.Commit(); err != nil {
  19. return fmt.Errorf("committing transaction: %w", err)
  20. }
  21. return nil
  22. }

Its usage:

  1. func Do(ctx context.Context, client *ent.Client) {
  2. // WithTx helper.
  3. if err := WithTx(ctx, client, func(tx *ent.Tx) error {
  4. return Gen(ctx, tx.Client())
  5. }); err != nil {
  6. log.Fatal(err)
  7. }
  8. }

Hooks

Same as schema hooks and runtime hooks, hooks can be registered on active transactions, and will be executed on Tx.Commit or Tx.Rollback:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. tx, err := client.Tx(ctx)
  3. if err != nil {
  4. return err
  5. }
  6. // Add a hook on Tx.Commit.
  7. tx.OnCommit(func(next ent.Committer) ent.Committer {
  8. return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
  9. // Code before the actual commit.
  10. err := next.Commit(ctx, tx)
  11. // Code after the transaction was committed.
  12. return err
  13. })
  14. })
  15. // Add a hook on Tx.Rollback.
  16. tx.OnRollback(func(next ent.Rollbacker) ent.Rollbacker {
  17. return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error {
  18. // Code before the actual rollback.
  19. err := next.Rollback(ctx, tx)
  20. // Code after the transaction was rolled back.
  21. return err
  22. })
  23. })
  24. //
  25. // <Code goes here>
  26. //
  27. return err
  28. }

Isolation Levels

Some drivers support tweaking a transaction’s isolation level. For example, with the sql driver, you can do so with the BeginTx method.

  1. tx, err := client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})