前言
在最近的项目中,同事使用GORM操作数据库,在更新数据的时候,发生了一些期望之外的效果,称之为“异常”或“bug”。经过对GORM官方文档的研读,发现了特有的提示,进而引发了我更进一步的思考。
经过
同事在进行常规的业务开发的时候,对数据库表记录进行更新,在操作的过程中,一个看似常规的操作,引起了数据被异常更新了。
下面是数据库表的模型字段定义
package models
import (
"github.com/keepchen/go-sail/v3/orm"
"github.com/shopspring/decimal"
)
// TransactionInfoList 流水表
type TransactionInfoList struct {
orm.BaseModel
RequestNo string `gorm:"column:request_no;type:varchar(64);NOT NULL;uniqueIndex:idx_requestNo;comment:请求订单号"`
TradeNo string `gorm:"column:trade_no;type:varchar(64);default:'';comment:钱包订单号"`
OrderNo string `gorm:"column:order_no;type:varchar(64);NOT NULL;index:idx_orderNo;comment:这笔交易对于的业务订单号"`
Uid string `gorm:"column:uid;type:varchar(150);NOT NULL;index:idx_uid;comment:用户ID"`
Message string `gorm:"column:message;type:varchar(128);default:'';comment:返回信息"`
State string `gorm:"column:state;type:varchar(128);default:'';comment:回调状态"`
其他字段已省略...
}
func (o *TransactionInfoList) TableName() string {
return "transaction_info_list"
}
以下是go-sail的orm.BaseModel定义
package orm
import (
"time"
)
// BaseModel 基础模型字段
//
// docs: https://gorm.io/docs/models.html
type BaseModel struct {
ID uint64 `gorm:"column:id;type:bigint UNSIGNED;primaryKey;AUTO_INCREMENT;comment:主键ID"`
CreatedAt time.Time `gorm:"column:created_at;type:datetime;comment:创建时间"` //创建时间
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;comment:更新时间"` //更新时间
DeletedAt *time.Time `gorm:"column:deleted_at;type:datetime;comment:(软)删除时间"` //(软)删除时间
}
// NoneID 空ID
const NoneID = uint64(0)
var nowTimeFunc = func() time.Time {
return time.Now()
}
// SetHookTimeFunc 设置时间勾子函数
func SetHookTimeFunc(ntf func() time.Time) {
nowTimeFunc = ntf
}
同事执行的sql操作也是非常的简单,就是根据业务逻辑对交易状态进行更新:
...
tansactionInfo := &models.TransactionInfoList{State: "SUCCESS"}
err := sail.GetDBW().WithContext(ctx).Updates(tansactionInfo).Error
...
起初还觉得一切正常,后面到深入自测以及测试同学介入的时候,发现创建时间字段被意外更新了,这显然是不合理的也是不应该出现的。同事本以为这是gorm的bug,去查阅gorm官方文档之后,发现官方文档有这么一句提示:
NOTE When updating with struct, GORM will only update non-zero fields. You might want to use
map
to update attributes or useSelect
to specify fields to update
也就是说,如果用结构体(struct)的方式进行数据更新操作,那么GORM只更新非零值。
这里就引发了我的深入思考了,什么是非零值?
回过头去看orm.BaseModel中的定义,created_at
字段是time.Time
类型,当没有对其进行赋值时,它的值是数据类型的默认值,也就是time.Time{}
对象,此值是非零值。也就会触发GORM的更新行为。
那么再进一步思考,如何界定默认值与赋予的默认值呢?
比如:
var age int
与
var age int = 0
前者是对变量的初始化行为,用户并没有赋值,后者是初始化并赋予用户值0
。两者的值是相等的,这样的情况下,是无法区分默认值与赋予的默认值的。
谈到这里,让我不禁想到了Java
,这门表达能力极为丰富的老牌静态类型语言。就拿整型这个数据类型来讲,它有Integer
和int
,看了一下Java对Integer的源码定义:
package java.lang;
import java.lang.annotation.Native;
public final class Integer extends Number implements Comparable<Integer> {
...
}
发现Intege是一个类
,那么对于变量声明的时候,意义就全然不同的了:
private Integer age;
private int age;
一个是基础数据类型,一个是类类型。
我对Java并不了解,据我身边熟悉Java的朋友介绍,Integer
是高级数据类型,会有装箱
和拆箱
的操作。如果age没有被赋值,那么它将是null
而不是0
,因此可以通过这样的方式界定究竟是仅初始化(定义)还是赋予的默认值。
另外,在Java领域,有一个思想是变化是由行为触发的,动作/行为引发了变化,因此推崇setXX
来触发值的变化,比如:
dto.setPage(1)
这样一来,是不是还能在setPage
语法糖里面做更多的事情,比如代理、通知还有其他?
言归正传
要解决上面出现的问题,GORM官方文档也指出了解决方案,那就是使用Omit()
显式指定忽略对某字段的更新:
...
tansactionInfo := &models.TransactionInfoList{State: "SUCCESS"}
err := sail.GetDBW().WithContext(ctx).Omit("created_at").Updates(tansactionInfo).Error
...
这样问题是解决了,但是看起来总是不太优雅,还有别的方案吗?答案是肯定的。GORM在模型定义的时候,支持设置仅更新
、仅创建
的权限操作。👉 文档
那么,我们对orm.BaseModel
进行调整:
// BaseModel 基础模型字段
//
// docs: https://gorm.io/docs/models.html
type BaseModel struct {
ID uint64 `gorm:"column:id;type:bigint UNSIGNED;primaryKey;AUTO_INCREMENT;comment:主键ID"`
CreatedAt time.Time `gorm:"column:created_at;type:datetime;comment:创建时间;autoCreateTime;<-:create"` //创建时间
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;comment:更新时间;autoUpdateTime"` //更新时间
DeletedAt *time.Time `gorm:"column:deleted_at;type:datetime;comment:(软)删除时间"` //(软)删除时间
}
这样一来,就不用在每一处调用的地方都写Omit
了。
胡思乱想
你们说,Golang有一天会出类似Integer
这样的高级类型吗?或者说已经有第三方库在做这个事情了呢?