Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/go-gorm/gorm/llms.txt

Use this file to discover all available pages before exploring further.

GORM executes every database operation through an ordered pipeline of callback functions. You can insert your own functions at any point in that pipeline — before or after any built-in step — without modifying GORM itself.

Callback processors

GORM maintains six callback processors, one per operation type:
ProcessorTriggered by
Create()db.Create(), db.Save()
Query()db.Find(), db.First(), db.Scan()
Update()db.Update(), db.Updates(), db.Save()
Delete()db.Delete()
Row()db.Row(), db.Rows()
Raw()db.Raw(), db.Exec()
Access any processor through db.Callback():
db.Callback().Create()  // create processor
db.Callback().Query()   // query processor
db.Callback().Update()  // update processor
db.Callback().Delete()  // delete processor
db.Callback().Row()     // row processor
db.Callback().Raw()     // raw processor

Callback function signature

Every callback is a plain Go function that receives a *gorm.DB:
func(db *gorm.DB)
The function can read and modify db.Statement to inspect or alter the current operation. To signal a failure, call db.AddError(err) — GORM will stop executing subsequent callbacks in the chain and return the error to the caller.
func myCallback(db *gorm.DB) {
    if someConditionFails {
        db.AddError(errors.New("condition not met"))
        return
    }
    // proceed with logic
}

Registering a callback

Use .Register(name, fn) on a processor to add a new callback:
// Register a callback that runs during every create operation
db.Callback().Create().Register("my-plugin:set_created_by", func(db *gorm.DB) {
    if db.Statement.Schema != nil {
        // set a CreatedBy field if it exists
        if field := db.Statement.Schema.LookUpField("CreatedBy"); field != nil {
            field.Set(db.Statement.Context, db.Statement.ReflectValue, currentUserID())
        }
    }
})
Callback names must be unique within a processor. Registering two callbacks with the same name on the same processor produces a warning in the GORM log.

Ordering callbacks

Use .Before("other-name") or .After("other-name") to control where your callback runs relative to an existing one:
// Run before GORM's built-in create callback
db.Callback().Create().
    Before("gorm:create").
    Register("my-plugin:before_create", func(db *gorm.DB) {
        fmt.Println("before create:", db.Statement.Table)
    })

// Run after GORM's built-in create callback
db.Callback().Create().
    After("gorm:create").
    Register("my-plugin:after_create", func(db *gorm.DB) {
        fmt.Println("after create:", db.Statement.Table)
    })
You can also use "*" as a wildcard to force a callback to run before or after all others:
// Always run first in the create pipeline
db.Callback().Create().
    Before("*").
    Register("my-plugin:first", myFirstCallback)

// Always run last in the create pipeline
db.Callback().Create().
    After("*").
    Register("my-plugin:last", myLastCallback)

Conditional callbacks

Use .Match(func(*gorm.DB) bool) to register a callback that only runs when a condition is true. The match function is evaluated once at compile time (when Register is called), not on every operation:
// Only register the callback when running in debug mode
db.Callback().Query().
    Match(func(db *gorm.DB) bool {
        return db.Config.DryRun
    }).
    Register("debug:log_query", func(db *gorm.DB) {
        fmt.Println("[dry-run] query:", db.Statement.SQL.String())
    })
Use db.Statement.Context inside a callback to access request-scoped values (such as a user identity or trace ID) that were passed via db.WithContext(ctx).

Replacing a callback

Use .Replace(name, fn) to swap out an existing callback — including GORM’s built-in ones — with your own implementation. The replacement runs in the same position as the original:
// Replace the built-in create callback entirely
db.Callback().Create().Replace("gorm:create", func(db *gorm.DB) {
    // custom SQL execution logic
    result, err := db.Statement.ConnPool.ExecContext(
        db.Statement.Context,
        db.Statement.SQL.String(),
        db.Statement.Vars...,
    )
    if err != nil {
        db.AddError(err)
        return
    }
    rowsAffected, _ := result.RowsAffected()
    db.RowsAffected = rowsAffected
})
Replacing built-in GORM callbacks (those prefixed with "gorm:") can break expected behavior. Make sure your replacement handles all edge cases that the original covers, or call the original inside your replacement.

Removing a callback

Use .Remove(name) to delete a callback from the pipeline. GORM logs a warning when a callback is removed:
// Remove the built-in update_time callback so GORM won't auto-set UpdatedAt
db.Callback().Update().Remove("gorm:update_time_unix_nano")

Checking the current SQL statement

Inside a callback, db.Statement exposes the full context of the operation:
db.Callback().Create().After("gorm:create").Register("audit:log", func(db *gorm.DB) {
    if db.Error != nil {
        return // don't log failed operations
    }

    // Access the table name
    table := db.Statement.Table

    // Access the number of rows affected
    rows := db.RowsAffected

    log.Printf("inserted %d row(s) into %s", rows, table)
})

Registering callbacks from a plugin

The recommended pattern is to register all callbacks inside a plugin’s Initialize method so that they are grouped together and tied to the plugin lifecycle:
type TimestampPlugin struct{}

func (p *TimestampPlugin) Name() string { return "timestamp-plugin" }

func (p *TimestampPlugin) Initialize(db *gorm.DB) error {
    db.Callback().Create().
        Before("gorm:create").
        Register("timestamp-plugin:set_created_at", setCreatedAt)

    db.Callback().Update().
        Before("gorm:update").
        Register("timestamp-plugin:set_updated_at", setUpdatedAt)

    return nil
}

func setCreatedAt(db *gorm.DB) {
    if db.Statement.Schema != nil {
        if f := db.Statement.Schema.LookUpField("CreatedAt"); f != nil {
            f.Set(db.Statement.Context, db.Statement.ReflectValue, time.Now())
        }
    }
}

func setUpdatedAt(db *gorm.DB) {
    if db.Statement.Schema != nil {
        if f := db.Statement.Schema.LookUpField("UpdatedAt"); f != nil {
            f.Set(db.Statement.Context, db.Statement.ReflectValue, time.Now())
        }
    }
}

Built-in callback names

GORM’s own callbacks follow the "gorm:<name>" convention. Common names you may want to target with Before/After:
  • gorm:begin_transaction
  • gorm:before_create
  • gorm:save_before_associations
  • gorm:create
  • gorm:save_after_associations
  • gorm:after_create
  • gorm:commit_or_rollback_transaction
  • gorm:query
  • gorm:preload
  • gorm:after_query
  • gorm:begin_transaction
  • gorm:before_update
  • gorm:save_before_associations
  • gorm:update
  • gorm:save_after_associations
  • gorm:after_update
  • gorm:commit_or_rollback_transaction
  • gorm:begin_transaction
  • gorm:before_delete
  • gorm:delete_before_associations
  • gorm:delete
  • gorm:after_delete
  • gorm:commit_or_rollback_transaction

Build docs developers (and LLMs) love