gorm icon indicating copy to clipboard operation
gorm copied to clipboard

How to target a different model (in other word, table) once a global hook is triggered?

Open tigerinus opened this issue 3 years ago • 2 comments

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?

tigerinus avatar Sep 20 '22 16:09 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!

tigerinus avatar Sep 20 '22 17:09 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.

tigerinus avatar Sep 20 '22 17:09 tigerinus

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

github-actions[bot] avatar Oct 20 '23 02:10 github-actions[bot]