Hanzo

Lifecycle Hooks

Run custom logic before and after CRUD operations.

Lifecycle Hooks

Hooks let you run validation, enrichment, or side-effect logic at specific points in an entity's lifecycle. Implement the hook interface on your struct -- the ORM calls it automatically.

Available Hooks

InterfaceMethodCalled When
BeforeCreatorBeforeCreate() errorBefore a new entity is persisted
AfterCreatorAfterCreate() errorAfter a new entity is persisted
BeforeUpdaterBeforeUpdate() errorBefore an existing entity is updated
AfterUpdaterAfterUpdate() errorAfter an existing entity is updated
BeforeDeleterBeforeDelete() errorBefore an entity is deleted
AfterDeleterAfterDelete() errorAfter an entity is deleted

Execution Order

Create

1. ApplyDefaults (struct tags)
2. BeforeCreate()
3. SerializeFields (orm:"serialize")
4. DB Put
5. AfterCreate()

Update

1. BeforeUpdate()
2. SerializeFields
3. DB Put
4. AfterUpdate()

Delete

1. BeforeDelete()
2. DB Delete
3. AfterDelete()

Example: Validation Hook

type User struct {
    orm.Model[User]
    Name  string `json:"name"`
    Email string `json:"email"`
}

func (u *User) BeforeCreate() error {
    if u.Name == "" {
        return fmt.Errorf("name is required")
    }
    if !strings.Contains(u.Email, "@") {
        return fmt.Errorf("invalid email: %s", u.Email)
    }
    return nil
}

If BeforeCreate returns an error, the entity is not persisted and Create() returns that error.

Example: Timestamps

type AuditedEntity struct {
    orm.Model[AuditedEntity]
    Name      string    `json:"name"`
    CreatedBy string    `json:"createdBy"`
    UpdatedBy string    `json:"updatedBy"`
    TouchedAt time.Time `json:"touchedAt"`
}

func (a *AuditedEntity) BeforeCreate() error {
    a.TouchedAt = time.Now()
    return nil
}

func (a *AuditedEntity) BeforeUpdate() error {
    a.TouchedAt = time.Now()
    return nil
}

Example: Side Effects

type Order struct {
    orm.Model[Order]
    CustomerID string `json:"customerId"`
    Total      int64  `json:"total"`
    Status     string `json:"status" orm:"default:pending"`
}

func (o *Order) AfterCreate() error {
    // Send confirmation email, emit event, etc.
    log.Printf("Order created: %s for customer %s ($%.2f)",
        o.Id(), o.CustomerID, float64(o.Total)/100)
    return nil
}

func (o *Order) AfterUpdate() error {
    if o.Status == "paid" {
        // Trigger fulfillment
        log.Printf("Order %s paid — starting fulfillment", o.Id())
    }
    return nil
}

Error Handling

  • Before hooks: returning an error aborts the operation. The database is not touched.
  • After hooks: returning an error is propagated to the caller, but the database write has already committed. Use after-hooks for non-critical side effects, or wrap the full operation in a transaction.
err := db.RunInTransaction(func(tx orm.DB) {
    order := orm.New[Order](tx)
    order.Total = 5000
    if err := order.Create(); err != nil {
        // If AfterCreate fails inside a transaction,
        // the entire transaction rolls back
        return
    }
})

Hook Interfaces

The hook interfaces are defined in hooks.go:

type BeforeCreator interface {
    BeforeCreate() error
}

type AfterCreator interface {
    AfterCreate() error
}

type BeforeUpdater interface {
    BeforeUpdate() error
}

type AfterUpdater interface {
    AfterUpdate() error
}

type BeforeDeleter interface {
    BeforeDelete() error
}

type AfterDeleter interface {
    AfterDelete() error
}

Any struct embedding Model[T] can implement any combination of these interfaces. The ORM uses interface type assertion at runtime -- no registration or configuration needed.

How is this guide?

Last updated on

On this page