Register, replace, and remove hooks in the GORM callback pipeline
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.
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}
Use .Register(name, fn) on a processor to add a new callback:
// Register a callback that runs during every create operationdb.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.
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 callbackdb.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 callbackdb.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 pipelinedb.Callback().Create(). Before("*"). Register("my-plugin:first", myFirstCallback)// Always run last in the create pipelinedb.Callback().Create(). After("*"). Register("my-plugin:last", myLastCallback)
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 modedb.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).
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:
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.
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()) } }}