Your Question

When a hook registered at global is triggered, it seems the db object is scoped to the impacted model only.

In my case, I need Mom.Kids []Kid updated whenever a Kid is deleted from its own table.

Assume that I cannot change the model of Kid, the best way I can think of is to have a hook for AfterDelete registered on the Kid model, to achieve my goal above.

package main

import (
    "os"
    "time"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Kid struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    Name      string
}

type Mom struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    Name      string
    Kids      []*Kid `gorm:"many2many:mom_kids;"`
}

const (
    HookBeforeCreate = "before_create"
    HookAfterCreate  = "after_create"
    HookBeforeSave   = "before_save"
    HookAfterSave    = "after_save"
    HookBeforeUpdate = "before_update"
    HookAfterUpdate  = "after_update"
    HookBeforeDelete = "before_delete"
    HookAfterDelete  = "after_delete"
    HookAfterFind    = "after_find"
)

var Hooks map[string][]func(*gorm.DB, interface{})

func init() {
    Hooks = make(map[string][]func(*gorm.DB, interface{}))

    for _, v := range []string{
        HookBeforeCreate, HookAfterCreate,
        HookBeforeSave, HookAfterSave,
        HookBeforeUpdate, HookAfterUpdate,
        HookBeforeDelete, HookAfterDelete,
        HookAfterFind,
    } {
        Hooks[v] = make([]func(*gorm.DB, interface{}), 0)
    }
}

func registerHook(db *gorm.DB) error {
    if err := db.Callback().Create().Before("gorm:create").Register(HookBeforeCreate, hookFunc(HookBeforeCreate)); err != nil {
        return err
    }

    if err := db.Callback().Create().After("gorm:create").Register(HookAfterCreate, hookFunc(HookAfterCreate)); err != nil {
        return err
    }

    if err := db.Callback().Update().Before("gorm:save").Register(HookBeforeSave, hookFunc(HookBeforeSave)); err != nil {
        return err
    }

    if err := db.Callback().Update().After("gorm:save").Register(HookAfterSave, hookFunc(HookAfterSave)); err != nil {
        return err
    }

    if err := db.Callback().Update().Before("gorm:update").Register(HookBeforeUpdate, hookFunc(HookBeforeUpdate)); err != nil {
        return err
    }

    if err := db.Callback().Update().After("gorm:update").Register(HookAfterUpdate, hookFunc(HookAfterUpdate)); err != nil {
        return err
    }

    if err := db.Callback().Delete().Before("gorm:delete").Register(HookBeforeDelete, hookFunc(HookBeforeDelete)); err != nil {
        return err
    }

    if err := db.Callback().Delete().After("gorm:delete").Register(HookAfterDelete, hookFunc(HookAfterDelete)); err != nil {
        return err
    }

    if err := db.Callback().Query().After("gorm:find").Register(HookAfterFind, hookFunc(HookAfterFind)); err != nil {
        return err
    }

    return nil
}

func hookFunc(name string) func(d *gorm.DB) {
    return func(d *gorm.DB) {
        if d == nil || d.Statement == nil || d.Statement.Schema == nil || d.Statement.SkipHooks {
            return
        }

        for _, f := range Hooks[name] {
            f(d, d.Statement.Model)
        }
    }
}

func main() {
    if _, err := os.Stat("test.db"); err == nil {
        os.Remove("test.db")
    }

    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic(err)
    }

    if err := db.AutoMigrate(&Mom{}, &Kid{}); err != nil {
        panic(err)
    }

    if err := registerHook(db); err != nil {
        panic(err)
    }

    mom := Mom{Name: "Mom"}
    kid1 := Kid{Name: "Kid1"}
    kid2 := Kid{Name: "Kid2"}
    mom.Kids = []*Kid{&kid1, &kid2}

    if err := db.Create(&mom).Error; err != nil {
        panic(err)
    }

    Hooks[HookAfterDelete] = append(Hooks[HookAfterDelete], func(d *gorm.DB, model interface{}) {
        if kid, ok := model.(*Kid); ok {

            println("after delete: Kid Name =", kid.Name)

            var moms []Mom
            if err := d.Preload("Kids").Find(&moms).Error; err != nil {
                panic(err)
            }

            for i := range moms {

                println("after delete: Mom Name =", moms[i].Name) // <-- this is not printed

                updatedKids := make([]*Kid, 0)
                for j := range moms[i].Kids {
                    if moms[i].Kids[j].ID != kid.ID {
                        updatedKids = append(updatedKids, moms[i].Kids[j])
                    }
                }

                if err := d.Model(&moms[i]).Association("Kids").Error; err != nil {
                    panic(err)
                }

                if err := d.Model(&moms[i]).Association("Kids").Replace(updatedKids); err != nil {
                    panic(err)
                }
            }
        }
    })

    if err := db.Delete(&kid1).Error; err != nil {
        panic(err)
    }
}

The result of above code is that after delete: Mom Name = ... is never printed.

The document you expected this should be explained

https://gorm.io/docs/hooks.html

Expected answer

How do I update a different model (table) when a registered global hook is triggered?

Comment From: tigerinus

My current workaround is to pass the global DB object to the hook

    if err := db.InstanceSet("gdb", db).Delete(&kid1).Error; err != nil {
        panic(err)
    }

But this involves changing hundred lines of code to have InstanceSet(...) method called. Any better solution is appreciated!

Comment From: tigerinus

Alright, I found a better workaround:

Some type and const:

type ContextKey string
const ContextKeyGlobalDB = ContextKey("gdb")

somewhere in main:

    db = db.WithContext(context.WithValue(context.Background(), ContextKeyGlobalDB, db))

somewhere in the hook:

gdb := d.Statement.Context.Value(ContextKeyGlobalDB)
if gdb, ok := gdb.(*gorm.DB); ok {
    /// do my stuff with gdb
}

Still, not sure if this is the best solution yet.

Comment From: github-actions[bot]

This issue has been automatically marked as stale because it has been open 360 days with no activity. Remove stale label or comment or this will be closed in 180 days