or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

associations.mdclause.mddatabase-operations.mdhooks.mdindex.mdlogger.mdmigrations.mdquery-building.mdschema.mdtransactions.md
tile.json

hooks.mddocs/

Hooks and Callbacks

GORM provides lifecycle hooks that allow you to execute custom logic before or after database operations. Hooks are triggered automatically during Create, Update, Delete, and Query operations.

Hook Interfaces

Implement these interfaces from gorm.io/gorm/callbacks to add lifecycle hooks to your models.

import "gorm.io/gorm/callbacks"

// Create hooks
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}

type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}

// Update hooks
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}

type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}

// Save hooks (Create or Update)
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}

type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}

// Delete hooks
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}

type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}

// Find hooks
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

Create Hooks

BeforeCreate

Called before inserting a new record into the database.

Usage:

type User struct {
    gorm.Model
    Name     string
    Email    string
    Password string
}

func (u *User) BeforeCreate(tx *gorm.DB) error {
    // Hash password before creating
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    u.Password = string(hashedPassword)

    // Validate email
    if !strings.Contains(u.Email, "@") {
        return errors.New("invalid email format")
    }

    // Generate UUID if needed
    if u.ID == 0 {
        u.ID = generateUUID()
    }

    return nil
}

// Hook is automatically called
db.Create(&User{Name: "Alice", Email: "alice@example.com", Password: "secret"})

AfterCreate

Called after a new record is inserted into the database.

Usage:

func (u *User) AfterCreate(tx *gorm.DB) error {
    // Send welcome email
    if err := sendWelcomeEmail(u.Email); err != nil {
        return err
    }

    // Create default profile
    return tx.Create(&Profile{
        UserID: u.ID,
        Bio:    "New user",
    }).Error
}

Update Hooks

BeforeUpdate

Called before updating a record.

Usage:

type User struct {
    gorm.Model
    Name     string
    Email    string
    Version  int
}

func (u *User) BeforeUpdate(tx *gorm.DB) error {
    // Increment version for optimistic locking
    u.Version++

    // Validate data
    if u.Name == "" {
        return errors.New("name cannot be empty")
    }

    // Log the change
    tx.Create(&AuditLog{
        UserID:    u.ID,
        Action:    "update",
        Timestamp: time.Now(),
    })

    return nil
}

// Hook is automatically called
db.Model(&user).Update("Name", "Alice")

AfterUpdate

Called after a record is updated.

Usage:

func (u *User) AfterUpdate(tx *gorm.DB) error {
    // Clear cache
    cache.Delete(fmt.Sprintf("user:%d", u.ID))

    // Notify subscribers
    notifySubscribers(u.ID, "user_updated")

    return nil
}

Save Hooks

Save hooks are triggered for both Create and Update operations when using Save().

BeforeSave

Called before creating or updating a record.

Usage:

type User struct {
    gorm.Model
    Name      string
    Email     string
    UpdatedBy string
}

func (u *User) BeforeSave(tx *gorm.DB) error {
    // Normalize email
    u.Email = strings.ToLower(strings.TrimSpace(u.Email))

    // Set updated by from context
    if userID, ok := tx.Statement.Context.Value("user_id").(string); ok {
        u.UpdatedBy = userID
    }

    return nil
}

AfterSave

Called after creating or updating a record.

Usage:

func (u *User) AfterSave(tx *gorm.DB) error {
    // Invalidate cache
    cache.Invalidate("users")

    // Update search index
    searchIndex.Update(u)

    return nil
}

Delete Hooks

BeforeDelete

Called before deleting a record.

Usage:

type User struct {
    gorm.Model
    Name   string
    Orders []Order
}

func (u *User) BeforeDelete(tx *gorm.DB) error {
    // Check if user has orders
    var count int64
    tx.Model(&Order{}).Where("user_id = ?", u.ID).Count(&count)

    if count > 0 {
        return errors.New("cannot delete user with existing orders")
    }

    // Archive user data
    tx.Create(&ArchivedUser{
        UserID:    u.ID,
        Name:      u.Name,
        DeletedAt: time.Now(),
    })

    return nil
}

AfterDelete

Called after deleting a record.

Usage:

func (u *User) AfterDelete(tx *gorm.DB) error {
    // Delete associated files
    os.RemoveAll(fmt.Sprintf("/uploads/users/%d", u.ID))

    // Clear cache
    cache.Delete(fmt.Sprintf("user:%d", u.ID))

    // Log deletion
    tx.Create(&AuditLog{
        Action:    "delete",
        UserID:    u.ID,
        Timestamp: time.Now(),
    })

    return nil
}

Query Hooks

AfterFind

Called after querying records from the database.

Usage:

type User struct {
    gorm.Model
    Name        string
    Password    string `gorm:"column:password" json:"-"`
    DecryptedData string `gorm:"-"`
}

func (u *User) AfterFind(tx *gorm.DB) error {
    // Decrypt sensitive data
    if u.DecryptedData != "" {
        decrypted, err := decrypt(u.DecryptedData)
        if err != nil {
            return err
        }
        u.DecryptedData = decrypted
    }

    // Hide password
    u.Password = ""

    // Update last accessed time
    go updateLastAccessed(u.ID)

    return nil
}

// Hook is automatically called
var user User
db.First(&user, 1)  // AfterFind is called after loading

Hook Execution Order

The hooks are executed in this order:

Create:

  1. BeginTransaction
  2. BeforeSave
  3. BeforeCreate
  4. Save associations (before)
  5. Insert into database
  6. Save associations (after)
  7. AfterCreate
  8. AfterSave
  9. CommitOrRollback

Update:

  1. BeginTransaction
  2. BeforeSave
  3. BeforeUpdate
  4. Save associations (before)
  5. Update database
  6. Save associations (after)
  7. AfterUpdate
  8. AfterSave
  9. CommitOrRollback

Delete:

  1. BeginTransaction
  2. BeforeDelete
  3. Delete from database
  4. AfterDelete
  5. CommitOrRollback

Query:

  1. Load from database
  2. AfterFind

Skipping Hooks

Skip hooks for specific operations.

// Skip all hooks
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)

// Skip default transaction (which skips some hooks)
db.Session(&gorm.Session{SkipDefaultTransaction: true}).Create(&user)

Error Handling in Hooks

Return an error from a hook to stop the operation and rollback the transaction.

func (u *User) BeforeCreate(tx *gorm.DB) error {
    // Validate age
    if u.Age < 18 {
        return errors.New("user must be 18 or older")
    }

    // Check for duplicate email
    var count int64
    tx.Model(&User{}).Where("email = ?", u.Email).Count(&count)
    if count > 0 {
        return errors.New("email already exists")
    }

    return nil
}

// If BeforeCreate returns error, Create operation will fail
err := db.Create(&user).Error
if err != nil {
    // Handle error from hook
}

Modifying Values in Hooks

You can modify the model values in hooks.

func (u *User) BeforeSave(tx *gorm.DB) error {
    // Trim whitespace
    u.Name = strings.TrimSpace(u.Name)
    u.Email = strings.TrimSpace(u.Email)

    // Normalize phone number
    u.Phone = normalizePhone(u.Phone)

    return nil
}

Using Transaction in Hooks

The tx parameter is the current transaction. You can use it to perform additional database operations.

func (u *User) AfterCreate(tx *gorm.DB) error {
    // Create related records within the same transaction
    profile := Profile{
        UserID: u.ID,
        Bio:    "New user",
    }

    if err := tx.Create(&profile).Error; err != nil {
        return err  // This will rollback the entire transaction
    }

    // Create notification
    notification := Notification{
        UserID:  u.ID,
        Message: "Welcome!",
    }

    return tx.Create(&notification).Error
}

Context in Hooks

Access context values in hooks.

func (u *User) BeforeCreate(tx *gorm.DB) error {
    // Get value from context
    if requestID, ok := tx.Statement.Context.Value("request_id").(string); ok {
        u.RequestID = requestID
    }

    return nil
}

// Set context when creating
ctx := context.WithValue(context.Background(), "request_id", "abc-123")
db.WithContext(ctx).Create(&user)

Conditional Hooks

Execute different logic based on conditions.

func (u *User) BeforeSave(tx *gorm.DB) error {
    // Check if this is create or update
    if u.ID == 0 {
        // This is a create operation
        u.CreatedBy = getCurrentUser()
    } else {
        // This is an update operation
        u.UpdatedBy = getCurrentUser()
    }

    return nil
}

func (u *User) BeforeUpdate(tx *gorm.DB) error {
    // Only execute if specific field changed
    if tx.Statement.Changed("Email") {
        // Send email verification
        sendVerificationEmail(u.Email)
    }

    return nil
}

Bulk Operations and Hooks

Hooks are called for each record in bulk operations.

users := []User{
    {Name: "Alice"},
    {Name: "Bob"},
    {Name: "Carol"},
}

// BeforeCreate and AfterCreate called for each user
db.Create(&users)

// To skip hooks in bulk operations
db.Session(&gorm.Session{SkipHooks: true}).Create(&users)

Registering Custom Callbacks

For advanced use cases, you can register custom callbacks that run at specific points in the lifecycle.

import "gorm.io/gorm/callbacks"

// Register custom callbacks
func RegisterDefaultCallbacks(db *gorm.DB, config *Config)

Usage:

// Register a custom callback
db.Callback().Create().Before("gorm:before_create").Register("my_plugin:before_create", func(db *gorm.DB) {
    // Custom logic here
})

// Register callback after specific point
db.Callback().Create().After("gorm:after_create").Register("my_plugin:after_create", func(db *gorm.DB) {
    // Custom logic here
})

// Replace existing callback
db.Callback().Create().Replace("gorm:create", func(db *gorm.DB) {
    // Custom create logic
})

// Remove callback
db.Callback().Create().Remove("gorm:create")

Callback Points

GORM provides these callback points for each operation:

Create callbacks:

  • gorm:begin_transaction
  • gorm:before_create
  • gorm:save_before_associations
  • gorm:create
  • gorm:save_after_associations
  • gorm:after_create
  • gorm:commit_or_rollback_transaction

Query callbacks:

  • gorm:query
  • gorm:preload
  • gorm:after_query

Update callbacks:

  • gorm:begin_transaction
  • gorm:setup_reflect_value
  • gorm:before_update
  • gorm:save_before_associations
  • gorm:update
  • gorm:save_after_associations
  • gorm:after_update
  • gorm:commit_or_rollback_transaction

Delete callbacks:

  • gorm:begin_transaction
  • gorm:before_delete
  • gorm:delete
  • gorm:after_delete
  • gorm:commit_or_rollback_transaction

Best Practices

Keep Hooks Simple

// Good: Simple validation
func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.Email == "" {
        return errors.New("email is required")
    }
    return nil
}

// Bad: Complex external operations
func (u *User) BeforeCreate(tx *gorm.DB) error {
    // Avoid slow external API calls in hooks
    response, err := http.Get("https://api.example.com/validate")
    // ...
    return err
}

Don't Modify Non-Related Data

// Good: Modify the model being saved
func (u *User) BeforeSave(tx *gorm.DB) error {
    u.Email = strings.ToLower(u.Email)
    return nil
}

// Bad: Modify unrelated global state
var globalCounter int
func (u *User) BeforeSave(tx *gorm.DB) error {
    globalCounter++  // Avoid modifying global state
    return nil
}

Handle Errors Appropriately

func (u *User) AfterCreate(tx *gorm.DB) error {
    // Critical operation: return error to rollback
    if err := tx.Create(&Profile{UserID: u.ID}).Error; err != nil {
        return err
    }

    // Non-critical operation: log but don't fail
    if err := sendWelcomeEmail(u.Email); err != nil {
        log.Printf("Failed to send welcome email: %v", err)
        // Don't return error
    }

    return nil
}

Use Goroutines Carefully

func (u *User) AfterCreate(tx *gorm.DB) error {
    // Safe: Database operation in transaction
    tx.Create(&Profile{UserID: u.ID})

    // Safe: Non-critical async operation
    go func() {
        sendWelcomeEmail(u.Email)
    }()

    // Unsafe: Don't use tx in goroutine after hook returns
    // The transaction will be committed/closed
    // go func() {
    //     tx.Create(&Something{})  // BAD!
    // }()

    return nil
}