概述
很大一部分gopher都是用过gorm,而time.Time类型也是大家罕用的类型,明天我就次要介绍一个在应用过程中遇到的一个比拟奇怪的问题。
问题景象形容
gorm有一个debug模式,大家并不生疏,开启之后执行的每个sql都能够输入到终端。然而我却遇到了一个奇怪的问题,终端打印的sql显示扫描到0行数据,然而实际上把这个sql拿到数据库中执行是有后果的。
起因假如
遇到这种问题,其实一开始会很莫名其妙,而后可能就会想到,是不是数据库执行的sql基本不是控制台打印进去的sql呢。因为where条件上是有工夫过滤条件的,过后其实曾经有一点狐疑是工夫的问题了,基于这个猜想,去验证并不艰难。
验证环节
因为前面其实我曾经确定了是工夫的问题,咱们这里的验证改为一个简略的场景,不拿我过后的业务数据在这里阐明。我会从新构建一个最简略的场景(这样也不便大家亲自实际),而后捋分明这个过程。
筹备工作
golang环境 我本地是1.18.2 实践上这个没什么影响,不要太低就好
mysql环境(我过后理论业务是用的tidb,为了不便验证,间接应用docker运行一个mysql服务)
代码
package mainimport ( "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.2902701012022/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/IP2022-09-30T14:00:57.719717Z 12 Query SET NAMES utf8mb42022-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类型,其实也是没有时区的概念的,能够认为是一个字符串。