Gin Practice Serial Ten Customized GORM Callbacks

  golang, gorm, mysql

Custom GORM Callbacks

GORM itself is powered by Callbacks, so you could fully customize GORM as you want

Project address:https://github.com/EDDYCJY/go …

GORM itself is callback driven, so we can completely customize GORM as needed to achieve our goal.

  • Register a new callback
  • Delete existing callbacks
  • Replace an existing callback
  • The order in which callbacks are registered

GORM includes the above four types of Callbacks, and we use “replace existing callbacks” to solve a minor pain point in combination with the project.

Problem

In the models directory, we include tag.go and article.go. they have a problem, that is, BeforeCreate and BeforeUpdate appear repeatedly. is it necessary to write 100 times for 100 files?

1、tag.go

image

2、article.go

image

Obviously, this is impossible. If you have realized this problem before, it is OK, but if not, it will be changed from now on.

Solve

Here we use Callbacks to implement the function, and do not need to write files one by one.

Implement Callbacks

Open the models. Go file in the Models directory to implement the following two methods:

1、updateTimeStampForCreateCallback

// updateTimeStampForCreateCallback will set `CreatedOn`, `ModifiedOn` when creating
func updateTimeStampForCreateCallback(scope *gorm.Scope) {
    if !scope.HasError() {
        nowTime := time.Now().Unix()
        if createTimeField, ok := scope.FieldByName("CreatedOn"); ok {
            if createTimeField.IsBlank {
                createTimeField.Set(nowTime)
            }
        }

        if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok {
            if modifyTimeField.IsBlank {
                modifyTimeField.Set(nowTime)
            }
        }
    }
}

In this method, the following functions will be completed

  • Check for errors (db.Error)
  • scope.FieldByNameviascope.Fields()Acquiring all fields and judging whether the required fields are currently contained or not
for _, field := range scope.Fields() {
    if field.Name == name || field.DBName == name {
        return field, true
    }
    if field.DBName == dbName {
        mostMatchedField = field
    }
}
  • field.IsBlankIt can be judged whether the value of this field is empty or not.
func isBlank(value reflect.Value) bool {
    switch value.Kind() {
    case reflect.String:
        return value.Len() == 0
    case reflect.Bool:
        return !value.Bool()
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        return value.Int() == 0
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
        return value.Uint() == 0
    case reflect.Float32, reflect.Float64:
        return value.Float() == 0
    case reflect.Interface, reflect.Ptr:
        return value.IsNil()
    }

    return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}
  • If it is emptyfield.SetUsed to set a value for this field, the parameter isinterface{}

2、updateTimeStampForUpdateCallback

// updateTimeStampForUpdateCallback will set `ModifyTime` when updating
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
    if _, ok := scope.Get("gorm:update_column"); !ok {
        scope.SetColumn("ModifiedOn", time.Now().Unix())
    }
}
  • scope.Get(...)Parameters with literal values set are obtained according to the input parameters, for example, in this articlegorm:update_columnWhich looks for the field attribute with this literal value
  • scope.SetColumn(...)Assuming no designationupdate_columnWe are updating callback settings by defaultModifiedOnThe value of

Register Callbacks

I have already written the callback method in the above section. Next, I need to register it in GORM’s hook, but it has its own Create and Update callbacks, so I can call for replacement.

In the init function of models.go, add the following statement

db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback)
db.Callback().Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback)

Verification

After accessing the AddTag interface and checking the database successfully, it can be found thatcreated_onAndmodified_onAll fields are the current execution time.

Access EditTag interface to discovermodified_onWhen was the last update performed

Expand

We think that hard deletions are rare in actual projects, so can Callbacks be used to complete this function?

The answer is yes, we in the previousModel structIncreaseDeletedOnVariable

type Model struct {
    ID int `gorm:"primary_key" json:"id"`
    CreatedOn int `json:"created_on"`
    ModifiedOn int `json:"modified_on"`
    DeletedOn int `json:"deleted_on"`
}

Implement Callbacks

Open the models.go file in the models directory to implement the following methods:

func deleteCallback(scope *gorm.Scope) {
    if !scope.HasError() {
        var extraOption string
        if str, ok := scope.Get("gorm:delete_option"); ok {
            extraOption = fmt.Sprint(str)
        }

        deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn")

        if !scope.Search.Unscoped && hasDeletedOnField {
            scope.Raw(fmt.Sprintf(
                "UPDATE %v SET %v=%v%v%v",
                scope.QuotedTableName(),
                scope.Quote(deletedOnField.DBName),
                scope.AddToVars(time.Now().Unix()),
                addExtraSpaceIfExist(scope.CombinedConditionSql()),
                addExtraSpaceIfExist(extraOption),
            )).Exec()
        } else {
            scope.Raw(fmt.Sprintf(
                "DELETE FROM %v%v%v",
                scope.QuotedTableName(),
                addExtraSpaceIfExist(scope.CombinedConditionSql()),
                addExtraSpaceIfExist(extraOption),
            )).Exec()
        }
    }
}

func addExtraSpaceIfExist(str string) string {
    if str != "" {
        return " " + str
    }
    return ""
}
  • scope.Get("gorm:delete_option")Check if delete_option is specified manually
  • scope.FieldByName("DeletedOn")Get the delete field we agreed upon, if it existsUPDATESoft delete, if it does not existDELETEHard delete
  • scope.QuotedTableName()Returns the referenced table name. This method GORM performs some processing on the table name according to its own logic
  • scope.CombinedConditionSql()Return to the combined conditional SQL and look at the prototype of the method
func (scope *Scope) CombinedConditionSql() string {
    joinSQL := scope.joinsSQL()
    whereSQL := scope.whereSQL()
    if scope.Search.raw {
        whereSQL = strings.TrimSuffix(strings.TrimPrefix(whereSQL, "WHERE ("), ")")
    }
    return joinSQL + whereSQL + scope.groupSQL() +
        scope.havingSQL() + scope.orderSQL() + scope.limitAndOffsetSQL()
}
  • scope.AddToVarsThis method can add values as SQL parameters and can also be used to prevent SQL injection.
func (scope *Scope) AddToVars(value interface{}) string {
    _, skipBindVar := scope.InstanceGet("skip_bindvar")

    if expr, ok := value.(*expr); ok {
        exp := expr.expr
        for _, arg := range expr.args {
            if skipBindVar {
                scope.AddToVars(arg)
            } else {
                exp = strings.Replace(exp, "?", scope.AddToVars(arg), 1)
            }
        }
        return exp
    }

    scope.SQLVars = append(scope.SQLVars, value)

    if skipBindVar {
        return "?"
    }
    return scope.Dialect().BindVar(len(scope.SQLVars))
}

Register Callbacks

In the init function of models.go, add the following deleted callback

db.Callback().Delete().Replace("gorm:delete", deleteCallback)

Verification

Restart the service and access the DeleteTag interface. after success, the deleted_on field will be found to have a value.

Summary

In this chapter, we have completed Callbacks for adding, updating and querying in combination with GORM, which is often used in actual projects.

After all, for a hook, there is no need to write too much unnecessary code yourself.

(Note that after adding soft deletions, the previous code needs to be added.)deleted_onThe judgment of the)

References

This series of sample codes

This series of catalogues

Document