关于go:使用gorm写入timeTime的类型时的问题

3次阅读

共计 3747 个字符,预计需要花费 10 分钟才能阅读完成。

概述

很大一部分 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 类型,其实也是没有时区的概念的,能够认为是一个字符串。

正文完
 0