ThreadLocal Transactions

A common pain point in Go is passing database transaction objects (*gorm.DB) explicitly as arguments to every repository function.

Rays ORM utilizes github.com/timandy/routine to establish ThreadLocal state for your goroutines. When you start a transaction via the GormTxManager, the transaction is stored in the local thread context.

When your auto-generated repositories execute queries, they transparently check the ThreadLocal context. If a transaction exists, they use it. If not, they use the global *gorm.DB.

Using the GormTxManager Closure

Inject the *GormTxManager into your business logic services to orchestrate complex database operations safely. The Transactional closure accepts a context.Context (which is passed to GORM’s WithContext) and manages commits and rollbacks automatically based on the returned error.

package services

import (
    "context"
    . "github.com/BeamFoundry/rays-orm"
    . "github.com/BeamFoundry/rays/pkg/core"
    . "github.com/BeamFoundry/rays/pkg/lang"
)

type UserService struct {
    Service

    txManager *GormTxManager  `@:"Inject"`
    userRepo  *UserRepository `@:"Inject"`
    auditRepo *AuditRepository`@:"Inject"`
}

func (s *UserService) CreateUserWithAudit(ctx context.Context, name string) Error {

    // Execute a block of code within a ThreadLocal transaction
    return s.txManager.Transactional(ctx, func() Error {

        newUser := &User{Name: name}

        // 1. This save natively participates in the active transaction
        _, err := s.userRepo.Save(newUser)
        if err != nil {
            return err // Triggers rollback
        }

        // 2. This custom query also participates in the transaction
        auditLog := &Audit{Action: "USER_CREATED", TargetID: newUser.Id()}
        _, err = s.auditRepo.Save(auditLog)
        if err != nil {
            return err // Triggers rollback
        }

        return nil // Triggers commit
    })
}

Manual Transaction Management

If you prefer or require granular control over the transaction lifecycle, you can manually start, commit, and rollback transactions.

When using this approach, you must call defer this.transactionManager.End() immediately after starting the transaction to ensure the ThreadLocal state is properly cleaned up, even in the event of a panic.

package services

import (
    "context"
    "errors"
    . "github.com/BeamFoundry/rays-orm"
    . "github.com/BeamFoundry/rays/pkg/core"
)

type UserServiceImpl struct {
    Service
    transactionManager *GormTxManager  `@:"Inject"`
    userRepository     *UserRepository `@:"Inject"`
}

func (this *UserServiceImpl) AddUser(ctx context.Context, username string) (*User, error) {
    // Start the transaction and bind it to the current thread
    tx := this.transactionManager.Start(ctx)

    // ALWAYS defer End() to clean up the ThreadLocal storage
    defer this.transactionManager.End()

    if _, ok := this.userRepository.FindByUsername(username).Get(); ok {
        tx.Rollback()
        return nil, errors.New("User already exists")
    }

    user := &User{ Name: username }

    u, err := this.userRepository.Save(user)
    if err != nil {
        tx.Rollback()
        return nil, err
    }

    tx.Commit()
    return u, nil
}