GORM自定义Gorm.Model实现自动添加时间戳

废话不说直接开始

官网(http://gorm.io)有给出一套默认的gorm.Model模型,定义如下

package gorm

import "time"

// Model base model definition, including fields `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`, which could be embedded in your models
//    type User struct {
//      gorm.Model
//    }
type Model struct {
    ID        uint `gorm:"primary_key"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time `sql:"index"`
}

包含四个属性,ID,创建时间,更新时间,删除时间,当操作数据时会自动更改相应的时间,删除时会将删除改成软删除并添加删除时间。

为什么官网已经有了还要自己写一套呢?理由有二:

1.我在做的是项目重构,原有数据库里已经有了GUID格式的主键,和此模型冲突(虽然不知为啥但官方明确指出支持复合主键但不建议这样做)

2.三个时间字段存储的是时间,而原项目存储的时间戳

既然决定重写就得先搞懂他是如何运作的,源码里找到DB对象定义如下

db相关信息,每次绑定不同的value,操作对象例如product{}
type DB struct {
    Value        interface{}
    Error        error
    RowsAffected int64

    // single db
    db                SQLCommon //原生db.sql对象,包含query相关的原生方法
    blockGlobalUpdate bool
    logMode           int
    logger            logger
    search            *search //保存搜索的条件where, limit, group,比如调用db.clone()时,会指定search
    values            map[string]interface{}

    // global db
    parent        *DB
    callbacks     *Callback //当前sql绑定的函数调用链
    dialect       Dialect //不同数据库适配注册sql.db
    singularTable bool
}

// 保存当前sql执行相关信息
type Scope struct {
    Search          *search // 检索条件
    Value           interface{}
    SQL             string //sql
    SQLVars         []interface{}
    db              *DB //sql.db
    instanceID      string
    primaryKeyField *Field
    skipLeft        bool
    fields          *[]*Field //字段
    selectAttrs     *[]string
}

// 保存各种操作需要执行的调用链,例如create函数,需要调用creates数组中所有的函数
type Callback struct {
    creates    []*func(scope *Scope)
    updates    []*func(scope *Scope)
    deletes    []*func(scope *Scope)
    queries    []*func(scope *Scope)
    rowQueries []*func(scope *Scope)
    processors []*CallbackProcessor
}

代码引用自: https://blog.csdn.net/qq_17612199/article/details/79437795

(官方源码里没有注释)

好现在知道了这些钩子方法是通过回调DB对象的Callback实现的而Callback下又是一个个的切片说明回调并不只是一个,找到官方的Creates回调

// Define callbacks for creating
func init() {
    DefaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback)
    DefaultCallback.Create().Register("gorm:before_create", beforeCreateCallback)
    DefaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback)
    DefaultCallback.Create().Register("gorm:update_time_stamp", updateTimeStampForCreateCallback)
    DefaultCallback.Create().Register("gorm:create", createCallback)
    DefaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback)
    DefaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback)
    DefaultCallback.Create().Register("gorm:after_create", afterCreateCallback)
    DefaultCallback.Create().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback)
}

找到‘update_time_stamp’对应的方法‘updateTimeStampForCreateCallback’

// updateTimeStampForCreateCallback will set `CreatedAt`, `UpdatedAt` when creating
func updateTimeStampForCreateCallback(scope *Scope) {
    if !scope.HasError() {
        now := NowFunc()

        if createdAtField, ok := scope.FieldByName("CreatedAt"); ok {
            if createdAtField.IsBlank {
                createdAtField.Set(now)
            }
        }

        if updatedAtField, ok := scope.FieldByName("UpdatedAt"); ok {
            if updatedAtField.IsBlank {
                updatedAtField.Set(now)
            }
        }
    }
}
// NowFunc returns current time, this function is exported in order to be able
// to give the flexibility to the developer to customize it according to their
// needs, e.g:
// gorm.NowFunc = func() time.Time {
// return time.Now().UTC()
// }
var NowFunc = func() time.Time {
    return time.Now()
}
 

好了,到了这一步相信各位已经知道该如何重写了,需要注意的是上边DefaultCallback.Create()用到的是Register()方法,Create()定义如下

// Register a new callback, refer `Callbacks.Create`
func (cp *CallbackProcessor) Register(callbackName string, callback func(scope *Scope)) {
    if cp.kind == "row_query" {
        if cp.before == "" && cp.after == "" && callbackName != "gorm:row_query" {
            log.Printf("Registing RowQuery callback %v without specify order with Before(), After(), applying Before('gorm:row_query') by default for compatibility...\n", callbackName)
            cp.before = "gorm:row_query"
        }
    }

    cp.name = callbackName
    cp.processor = &callback
    cp.parent.processors = append(cp.parent.processors, cp)
    cp.parent.reorder()
}

既然已经注册过了那重写的话就不能用Register了,源码中找到一个Replace方法用于替换原有的回调,其定义如下

// Replace a registered callback with new callback
//     db.Callback().Create().Replace("gorm:update_time_stamp_when_create", func(*Scope) {
//           scope.SetColumn("Created", now)
//           scope.SetColumn("Updated", now)
//     })
func (cp *CallbackProcessor) Replace(callbackName string, callback func(scope *Scope)) {
    log.Printf("[info] replacing callback `%v` from %v\n", callbackName, fileWithLineNum())
    cp.name = callbackName
    cp.processor = &callback
    cp.replace = true
    cp.parent.processors = append(cp.parent.processors, cp)
    cp.parent.reorder()
}

好了,该掌握的都掌握了现在开始实际搞一下试一试

项目中model文件夹用于存放映射数据库模型,新建changemodel.go文件

package model

type ChangeModel struct{
    CreatedTime int32 
    UpdatedTime int32
    DeletedTime int32
}

在需要用到事件记录的模型里引入ChangeModel如

package model

//User
type User struct{
    ChangeModel
    UserID string `gorm:"primary_key;size:36"`
    Name string `gorm:"size:20"`
    PassWord string `gorm:"size:40"`
}

好了到此为止自定义的公共字段已经弄好了,接下来是最重要的重写回调并注入

打开自己的数据库连接文件

package mysqltools

import (
    "fmt"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "log"
    "sync"
    "time"
)

type MysqlConnectiPool struct {
}

var instance *MysqlConnectiPool
var once sync.Once

var db *gorm.DB
var err_db error

func GetInstance() *MysqlConnectiPool {
    once.Do(func() {
        instance = &MysqlConnectiPool{}
    })
    return instance
}

/*
* @fuc 初始化数据库连接(可在main()适当位置调用)
*/
func (m *MysqlConnectiPool) InitDataPool() (issucc bool) {
    db, err_db = gorm.Open("mysql", "name:password@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local")
    fmt.Println(err_db)
    if err_db != nil {
        log.Fatal(err_db)
        return false
    }
    db.DB().SetMaxIdleConns(10)
    db.DB().SetMaxOpenConns(100)
    db.DB().SetConnMaxLifetime(time.Hour)
    db.Callback().Create().Replace("gorm:update_time_stamp",updateTimeStampForCreateCallback)
    db.Callback().Update().Replace("gorm:update_time_stamp",updateTimeStampForUpdateCallback)
    db.Callback().Delete().Replace("gorm:delete", deleteCallback)
    return true
}
func (m *MysqlConnectiPool) GetMysqlDB() (db_con *gorm.DB) {
    return db
}


// // 注册新建钩子在持久化之前
func updateTimeStampForCreateCallback(scope *gorm.Scope) {
   
    if !scope.HasError() {
        nowTime := time.Now().Unix()
        if createTimeField, ok := scope.FieldByName("CreatedTime"); ok {
            if createTimeField.IsBlank {
                createTimeField.Set(nowTime)
            }
        }

        if modifyTimeField, ok := scope.FieldByName("UpdatedTime"); ok {
            if modifyTimeField.IsBlank {
                modifyTimeField.Set(nowTime)
            }
        }
    }
}
// 注册更新钩子在持久化之前
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
    if _, ok := scope.Get("gorm:update_column"); !ok {
        scope.SetColumn("UpdatedTime", time.Now().Unix())
    }
}
// 注册删除钩子在删除之前
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("DeletedTime")

        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 ""
}

可以看到三个对应的回调已经写好并替换掉默认的钩子回调

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

注意

钩子并不是万能的,当使用Raw()写原生SQL时钩子会失效,批量操作会导致钩子无效,另外一些特殊的方法也会,所以重要数据千万不要偷懒用钩子操作

posted @ 2018-12-21 11:10  石冠易  阅读(19582)  评论(1编辑  收藏  举报