概述
很大一部分 gopher 都是用过 gorm,而 time.Time 类型也是大家罕用的类型,明天我就次要介绍一个在应用过程中遇到的一个比拟奇怪的问题。
问题景象形容
gorm 有一个 debug 模式,大家并不生疏,开启之后执行的每个 sql 都能够输入到终端。然而我却遇到了一个奇怪的问题,终端打印的 sql 显示扫描到 0 行数据,然而实际上把这个 sql 拿到数据库中执行是有后果的。
起因假如
遇到这种问题,其实一开始会很莫名其妙,而后可能就会想到,是不是数据库执行的 sql 基本不是控制台打印进去的 sql 呢。因为 where 条件上是有工夫过滤条件的,过后其实曾经有一点狐疑是工夫的问题了,基于这个猜想,去验证并不艰难。
验证环节
因为前面其实我曾经确定了是工夫的问题,咱们这里的验证改为一个简略的场景,不拿我过后的业务数据在这里阐明。我会从新构建一个最简略的场景(这样也不便大家亲自实际),而后捋分明这个过程。
筹备工作
golang 环境 我本地是 1.18.2 实践上这个没什么影响,不要太低就好
mysql 环境(我过后理论业务是用的 tidb,为了不便验证,间接应用 docker 运行一个 mysql 服务)
代码
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"time"
)
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:[email protected](127.0.0.1:3306)/lv_test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {panic(err)
}
db = db.Debug()
var result []*map[string]interface{}
t := time.Now()
fmt.Println(t)
t = t.UTC()
err = db.Table("test").Where("created_at>?", t).Find(&result).Error
if err != nil {panic(err)
}
}
下面的代码是一个最小的验证单元
能够通过上面的命令开启通用日志以及查看日志地位
set global general_log=on;
show variables like ‘general_log_file’;
执行
执行 golang 代码
看到输入后果如下,因为工夫被转换成 utc 工夫,所以输入的 sql 看起来如同是失常的
2022-09-30 22:00:57.7214462 +0800 CST m=+1.290270101
2022/09/30 22:00:57 C:/work/go/code/first/mysql.go:24
[2.653ms] [rows:0] SELECT * FROM `test` WHERE created_at>'2022-09-30 14:00:57.721'
然而通过查看 mysql 的 general log 并不是这样
2022-09-30T14:00:57.718992Z 12 Connect [email protected] on lv_test using TCP/IP
2022-09-30T14:00:57.719717Z 12 Query SET NAMES utf8mb4
2022-09-30T14:00:57.720416Z 12 Query SELECT VERSION()
2022-09-30T14:00:57.735894Z 12 Prepare SELECT * FROM `test` WHERE created_at>?
2022-09-30T14:00:57.736664Z 12 Execute SELECT * FROM `test` WHERE created_at>'2022-09-30 22:00:57.7214462'
2022-09-30T14:00:57.737528Z 12 Close stmt
能够看到数据库外面实在执行的时候的工夫参数跟 gorm 打印的并不一样,这样的状况就会给排查问题造成比拟大的困扰。
源码剖析
通过浏览 go-sql-driver 源码外面的 Exec 办法,发现调用了 interpolateParams 办法解决参数(在 github.com/go-sql-dirver/mysql 目录的 connection.go)
case time.Time:
if v.IsZero() {buf = append(buf, "'0000-00-00'"...)
} else {buf = append(buf, '\'')
buf, err = appendDateTime(buf, v.In(mc.cfg.Loc))
if err != nil {return "", err}
buf = append(buf, '\'')
}
我简略截取了对传入的 time.Time 对象的解决办法,发现是把 time.Time 对象解决成 cfg.Loc 中的时区,那么这个 cfg 是哪里来的呢
浏览 connector.go,发现是在 Connect 办法中对 mc 进行赋值操作
func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
var err error
// New mysqlConn
mc := &mysqlConn{
maxAllowedPacket: maxPacketSize,
maxWriteSize: maxPacketSize - 1,
closech: make(chan struct{}),
cfg: c.cfg,
}
mc.parseTime = mc.cfg.ParseTime
//......... 之后的代码省略
}
connector 外面的 cfg 又是哪里来的呢
在 driver.go 中的 Open 函数赋值的
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {cfg, err := ParseDSN(dsn)
if err != nil {return nil, err}
c := &connector{cfg: cfg,}
return c.Connect(context.Background())
}
这时候咱们就要看 ParseDSN 中的解决了
通过查看不难找出
// Time Location
case "loc":
if value, err = url.QueryUnescape(value); err != nil {return}
cfg.Loc, err = time.LoadLocation(value)
if err != nil {return}
通过下面,能够看到,如果传入了 loc=Local 的参数,入库的工夫会被 fomat 为东八区的工夫字符串
咱们再看看 gorm 打印 sql 时候的解决
gorm 源码中的 logger 目录,sql.go 文件
func ExplainSQL(sql string, numericPlaceholder *regexp.Regexp, escaper string, avars ...interface{}) string {
var (convertParams func(interface{}, int)
vars = make([]string, len(avars))
)
convertParams = func(v interface{}, idx int) {switch v := v.(type) {
case bool:
vars[idx] = strconv.FormatBool(v)
case time.Time:
if v.IsZero() {vars[idx] = escaper + tmFmtZero + escaper
} else {vars[idx] = escaper + v.Format(tmFmtWithMS) + escaper
}
//...... 略去前面的代码
}
能够看到 在打印日志的时候,工夫是被依照 tmFmtWithMS 这个格局格式化的
不难找出这个的定义
const (
tmFmtWithMS = "2006-01-02 15:04:05.999"
tmFmtZero = "0000-00-00 00:00:00"
nullStr = "NULL"
)
能够看到,是间接 Format 的,没有思考时区的状况
总结与论断
golang 中的 time.Time 类型实质上也是由工夫戳和时区形成的,不论是什么时区,工夫戳是一样的,而后依据不同的时区体现出不同的字符串模式的值。
咱们一开始连贯数据库传入的 loc=Local 参数,驱动就在数据交互操作中把所有的 time.Time 类型变成本地工夫(东八区),这种状况下 utc 工夫的 2022-09-30 14:00:57.721 就变成了东八区的 2022-09-30 22:00:57.721。然而 gorm 在进行日志打印的时候,并没有去思考时区的状况,而是间接打印出了 2022-09-30 14:00:57.721 这个字符串。这就导致看到的 gorm 日志和理论执行的 sql 不统一。其实实际上对数据库来说,即使是 datatime 类型,其实也是没有时区的概念的,能够认为是一个字符串。