How to insert new record to db using gorm plugins/hooks

Viewed 769

While trying to insert logs to detect my models' values changes using gorm in golang, I'm doing it using plugins:

type MyModel struct {
 Id
 Name
}

type Log struct {
 Id
 NewValue
 OldValue
 CreatedAt
}

My plugin definition is something like this:

func ChangelogCreatePlugin(db *gorm.DB) {
    log := &Log{NewValue: "the new value", OldValue: "the old value", CreatedAt: "current time"}
    // Here is the problem
    db.Save(log) <- this is not acceptable
}

Inserting a different data model using the db *gorm.DB argument in the plugin is not acceptable as that argument is initialized to accept data from the same model which triggered the plugin.

My requirement is to store my log in the same db transaction, so if one of them failed they should both fail. How to do such thing in gorm?

I'm aware of hooks. Hooks are not useful in my case as I want my logs to track different models, so I'm looking for more "reusable" solution, rather than copy/paste hooks implementations in my models.

3 Answers

After a lot of digging and debugging, I came up with this solution:

You first need to register your plugin execution in the right order, in my case it should be something like this:

gormDb.Callback().Create().Before("gorm:commit_or_rollback_transaction").Register("changelog_create", ChangelogCreatePlugin)

This order will guarantee that your plugin will be activated or triggered before the transaction commit of any insert clause of your models.

To have more insights about where you can register your plugins, have a look inside the default callbacks registered in gorm

After doing so, I needed to adjust the plugin code:

func ChangelogCreatePlugin(db *gorm.DB) {
    // first make a check that the model insert transaction doesn't have any error
    if db.Error != nil {
        return
    }

    
    log := &Log{NewValue: "the new value", OldValue: "the old value", CreatedAt: "current time"}
    
    // get a new db session for the new model to work
    logDb := db.Session(&gorm.Session{})


    // if an error ocurred while saving the log
    // push it into original model db instance errors, so it will be rolledback eventually
    logErr := logDb.Save(log)
    if logErr != nil {
        db.AddError(logErr)
    }
}

Hope this may help someone someday!

Try create transactional decorator witch use func(d *gorm.DB) or f ...func(d *gorm.DB) as argument. It should look like

type TransactionalDecorator struct {
    DB *gorm.DB
}

func (t *TransactionalDecorator) Transaction(f ...func(d *gorm.DB) error) error  {
    return t.DB.Transaction(func(tx *gorm.DB) error {
        for _, fn := range f {
            if err := fn(tx); err != nil {
                return err
            }
        }
        return nil
    })
}

All database interaction function will be execute in the same transaction

repo , _ := gorm.Open(...)

tdb := &TransactionalDecorator{DB: repo}

saveModel := func(d *gorm.DB) error {
    // save model
}
saveLog := func(d *gorm.DB) error {
    // save log
}

err := tdb.Transaction(saveModel, saveLog)

How about:

func ChangelogCreatePlugin(db *gorm.DB) {
    log := &Log{NewValue: "the new value", OldValue: "the old value", CreatedAt: "current time"}
    // Here is the problem
    db.Session(&gorm.Session{}).Save(log)
}

This should give you a "fresh" db/tx to work with

Related