本来应该写Docker的,但是想了下,还是决定先做好开发,最后部署Docker。
这里先提出问题,我们models目录下的 tag.go、 article.go 文件,它们有两个方法:AfterCreate、AfterUpdate。
每个文件都要实现这两个办法?
目标
通过GORM的 Callbacks 来实现功能,不需要编写每个文件。
功能
- 每次 create 更新 CreatedOn 字段。
- 每次 update 更新 ModifiedOn 字段。
实现Callbacks
参考文档还是 gorm.io .
参考地址1:https://gorm.io/zh_CN/docs/write_plugins.html
参考地址2:https://gorm.io/docs/update.html#Change-Updating-Values
参考地址3:https://github.com/go-gorm/gorm/issues/3441
参考地址4:https://gorm.io/zh_CN/docs/hooks.html
这里我们需要针对 before_create 和 before_update 两个Callback实现
原因可以参考:GORM HOOK章节
在models/models.go 文件中增加两个函数:
1 2 3 4 5 6 7 |
func updateTimeStampForBeforeCreateCallback(db *gorm.DB) { db.Statement.SetColumn("CreatedOn", time.Now().Unix()) } func updateTimeStampForBeforeUpdateCallback(db *gorm.DB) { db.Statement.SetColumn("ModifiedOn", time.Now().Unix()) } |
SetColumn函数实际上是GORM包实现的一个辅助函数,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Helpers // SetColumn set column's value func (stmt *Statement) SetColumn(name string, value interface{}) { if v, ok := stmt.Dest.(map[string]interface{}); ok { v[name] = value } else if stmt.Schema != nil { if field := stmt.Schema.LookUpField(name); field != nil { switch stmt.ReflectValue.Kind() { case reflect.Slice, reflect.Array: field.Set(stmt.ReflectValue.Index(stmt.CurDestIndex), value) case reflect.Struct: field.Set(stmt.ReflectValue, value) } } else { stmt.AddError(ErrInvalidField) } } else { stmt.AddError(ErrInvalidField) } } |
注册Callback
在 models.go 的 init 函数中增加以下语句:
1 2 3 |
// 用新函数替换GORM Create、Update流程自带的回调函数 db.Callback().Create().Replace("gorm:before_create", updateTimeStampForBeforeCreateCallback) db.Callback().Update().Replace("gorm:before_update", updateTimeStampForBeforeUpdateCallback) |
验证
访问AddTag接口,成功之后检查数据库,可以发现 created_on 字段为当前执行时间。
访问EditTag接口,可发现 modified_on 为最后一次执行更新的实际。
拓展
P.S. 其实我们实现的 CreatedOn、 ModifiedOn 以及这里要实现的 DeletedOn 都是GORM已经实现好的。
实际项目中,硬删除是比较少的,那么是否可以通过Callbacks来完成这个功能呢。
我们在先前 Model struct 增加 DeletedOn 变量。
1 2 3 4 5 6 7 |
//Model ... type Model struct { ID uint `gorm:"primarykey" json:"id"` CreatedOn int `json:"created_on"` ModifiedOn int `json:"modified_on"` DeletedOn int `json:"deleted_on"` } |
在 init 函数里面替换删除的回调
db.Callback().Delete().Replace(“gorm:delete”, deleteCallback)
然后实现 deleteCallback 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
//deleteCallback 实现软删除 func deleteCallback(db *gorm.DB) { if db.Error == nil { if db.Statement.Schema != nil { db.Statement.SQL.Grow(100) deleteField := db.Statement.Schema.LookUpField("DeletedOn") if !db.Statement.Unscoped && deleteField != nil { //Soft Delete if db.Statement.SQL.String() == "" { nowTime := time.Now().Unix() db.Statement.AddClause( clause.Set{{ Column: clause.Column{Name: deleteField.DBName}, Value: nowTime, }}, ) db.Statement.AddClauseIfNotExists(clause.Update{}) db.Statement.Build("UPDATE", "SET", "WHERE") } } else { //Delete if db.Statement.SQL.String() == "" { db.Statement.AddClauseIfNotExists(clause.Delete{}) db.Statement.AddClauseIfNotExists(clause.From{}) db.Statement.Build("DELETE", "FROM", "WHERE") } } fmt.Println(db.Statement.SQL.String()) fmt.Println(db.Statement.Vars) //Must Need WHERE if _, ok := db.Statement.Clauses["WHERE"]; !db.AllowGlobalUpdate && !ok { db.AddError(gorm.ErrMissingWhereClause) return } db.Exec(db.Statement.SQL.String(), db.Statement.Vars...) } } } |
P.S. 记得数据库表加上 deleted_on 字段
看起来目前实现了一个有bug的版本,不慌,我们可以慢慢修复。
UPDATE
blog_tag
SETdeleted_on
=? WHERE id = ?
[1599765813 9]2020/09/11 03:23:33 /home/xiong/Code/ginBlog/models/models.go:130 sql: expected 2 arguments, got 4
[0.209ms] [rows:0] UPDATEblog_tag
SETdeleted_on
=1599765813 WHERE id = 92020/09/11 03:23:33 /home/xiong/Code/ginBlog/models/tag.go:76 sql: expected 2 arguments, got 4
[0.569ms] [rows:0]
之前考虑的一种情况是:类型为[]interface {}的变量具有特定的内存布局,在编译时已知:每一个interface{}类型占用两个字(一个用于存储interface包含的类型,另一个用于存储类型的值或指针)。因此,长度为N并且类型为[]interface{}的切片大小为N*2个字长。猜测这是为什么期望两个参数,最终得到四个参数的原因。
说实话我没太理解这里导致的具体问题是什么,去网上搜索了一些,看起来五花八门…
甚至我当时觉得可能问题并不出在db.Exec(db.Statement.SQL.String(), db.Statement.Vars…)这里。
因为当我把对应的SQL、参数拉出来依旧报错。
但是当我把SQL语句放到tag.go文件中,Callback外面时…它神奇的…好了。
另一个思路:去看了下源码 Callbacks/delete.go 里面的Delete实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
func Delete(db *gorm.DB) { if db.Error == nil { if db.Statement.Schema != nil && !db.Statement.Unscoped { for _, c := range db.Statement.Schema.DeleteClauses { db.Statement.AddClause(c) } } if db.Statement.SQL.String() == "" { db.Statement.SQL.Grow(100) db.Statement.AddClauseIfNotExists(clause.Delete{}) if db.Statement.Schema != nil { _, queryValues := schema.GetIdentityFieldValuesMap(db.Statement.ReflectValue, db.Statement.Schema.PrimaryFields) column, values := schema.ToQueryValues(db.Statement.Table, db.Statement.Schema.PrimaryFieldDBNames, queryValues) if len(values) > 0 { db.Statement.AddClause(clause.Where{Exprs: []clause.Expression{clause.IN{Column: column, Values: values}}}) } if db.Statement.ReflectValue.CanAddr() && db.Statement.Dest != db.Statement.Model && db.Statement.Model != nil { _, queryValues = schema.GetIdentityFieldValuesMap(reflect.ValueOf(db.Statement.Model), db.Statement.Schema.PrimaryFields) column, values = schema.ToQueryValues(db.Statement.Table, db.Statement.Schema.PrimaryFieldDBNames, queryValues) if len(values) > 0 { db.Statement.AddClause(clause.Where{Exprs: []clause.Expression{clause.IN{Column: column, Values: values}}}) } } } db.Statement.AddClauseIfNotExists(clause.From{}) db.Statement.Build("DELETE", "FROM", "WHERE") } if _, ok := db.Statement.Clauses["WHERE"]; !db.AllowGlobalUpdate && !ok { db.AddError(gorm.ErrMissingWhereClause) return } if !db.DryRun && db.Error == nil { result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) if err == nil { db.RowsAffected, _ = result.RowsAffected() } else { db.AddError(err) } } } } |
理解了这段代码,它主要分为四部分:
- 获取软删除SQL
- 获取删除SQL(如果前一步没获取到)
- 检查WHERE子句
- 执行SQL
软删除相关的代码在soft_delete.go文件中, 关注一下它的 func (sd SoftDeleteDeleteClause) ModifyStatement(stmt *Statement) 即可。
意味着GORM已经实现好了软删除的流程,我只需要把我需要的代码Copy过来…嘿嘿
于是最终版代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
//deleteCallback 实现软删除 func deleteCallback(db *gorm.DB) { if db.Error == nil { if db.Statement.Schema != nil { db.Statement.SQL.Grow(100) deleteField := db.Statement.Schema.LookUpField("DeletedOn") if !db.Statement.Unscoped && deleteField != nil { //Soft Delete if db.Statement.SQL.String() == "" { nowTime := time.Now().Unix() db.Statement.AddClause( clause.Set{{ Column: clause.Column{Name: deleteField.DBName}, Value: nowTime, }}, ) db.Statement.AddClauseIfNotExists(clause.Update{}) db.Statement.Build("UPDATE", "SET", "WHERE") } } else { //Delete if db.Statement.SQL.String() == "" { db.Statement.AddClauseIfNotExists(clause.Delete{}) db.Statement.AddClauseIfNotExists(clause.From{}) db.Statement.Build("DELETE", "FROM", "WHERE") } } //Must Need WHERE if _, ok := db.Statement.Clauses["WHERE"]; !db.AllowGlobalUpdate && !ok { db.AddError(gorm.ErrMissingWhereClause) return } //执行SQL if !db.DryRun && db.Error == nil { result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) if err == nil { db.RowsAffected, _ = result.RowsAffected() } else { db.AddError(err) } } } } } |
测试了一下软删除,OK了。
1 2 3 4 5 6 7 8 9 10 |
mysql> SELECT * FROM blog_tag; +----+---------+------------+------------+-------------+-------------+------------+-------+ | id | name | created_on | created_by | modified_on | modified_by | deleted_on | state | +----+---------+------------+------------+-------------+-------------+------------+-------+ | 7 | 3 | 1599022723 | root | 0 | | 0 | 1 | | 8 | create1 | 1599026312 | root | 0 | | 0 | 1 | | 10 | edit12 | 1599671217 | root | 1599721426 | root | 1599796305 | 1 | | 11 | 6 | 1599671277 | root | 0 | | 0 | 1 | +----+---------+------------+------------+-------------+-------------+------------+-------+ 4 rows in set (0.00 sec) |
测试删除
将 models/tag.go 删除标签函数修改:加上了Unscoped()
参考文档:https://gorm.io/zh_CN/docs/delete.html
1 2 3 4 5 6 7 |
//DeleteTag 删除标签 func DeleteTag( id int, ) bool { err := db.Unscoped().Where("id = ?", id).Delete(&Tag{}).Error return err == nil } |
然后再测试了一下删除:
1 2 3 4 5 6 7 8 9 |
mysql> SELECT * FROM blog_tag; +----+---------+------------+------------+-------------+-------------+------------+-------+ | id | name | created_on | created_by | modified_on | modified_by | deleted_on | state | +----+---------+------------+------------+-------------+-------------+------------+-------+ | 7 | 3 | 1599022723 | root | 0 | | 0 | 1 | | 8 | create1 | 1599026312 | root | 0 | | 0 | 1 | | 11 | 6 | 1599671277 | root | 0 | | 0 | 1 | +----+---------+------------+------------+-------------+-------------+------------+-------+ 3 rows in set (0.00 sec) |
算是搞定了,这里记得把 DeleteTag 函数改回来,不需要 Unscoped() , 这只是我们需要测试一下。
小结
我们使用 GORM 完成了创建、更新、删除的Callbacks。
理解了GORM软删除方案。
完善软删除需要做的事情如下:
- 给Article数据库表增加deleted_on字段
- 修改其他代码,增加deleted_on判断
附:
一般开发来说,GORM已经实现了 CreatedOn、 ModifiedOn 以及 DeletedOn。
实在是没必要自己实现…我就不继续搞了。
请问,这个软删除之后的查询怎么处理,还要自己写一个callback吗?