由GORM的Updates语法糖"bug"引发的思考

前言

在最近的项目中,同事使用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 use Select 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,这门表达能力极为丰富的老牌静态类型语言。就拿整型这个数据类型来讲,它有Integerint,看了一下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这样的高级类型吗?或者说已经有第三方库在做这个事情了呢?


转载请注明原文地址:https://blog.keepchen.com/a/reflections-triggered-by-gorm-updates-syntactic-sugar-bug.html


980_120.png(via stardots.io)