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.
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
}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"})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
}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")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 are triggered for both Create and Update operations when using Save().
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
}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
}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
}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
}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 loadingThe hooks are executed in this order:
Create:
Update:
Delete:
Query:
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)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
}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
}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(¬ification).Error
}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)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
}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)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")GORM provides these callback points for each operation:
Create callbacks:
gorm:begin_transactiongorm:before_creategorm:save_before_associationsgorm:creategorm:save_after_associationsgorm:after_creategorm:commit_or_rollback_transactionQuery callbacks:
gorm:querygorm:preloadgorm:after_queryUpdate callbacks:
gorm:begin_transactiongorm:setup_reflect_valuegorm:before_updategorm:save_before_associationsgorm:updategorm:save_after_associationsgorm:after_updategorm:commit_or_rollback_transactionDelete callbacks:
gorm:begin_transactiongorm:before_deletegorm:deletegorm:after_deletegorm:commit_or_rollback_transaction// 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
}// 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
}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
}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
}