github.com/go-sql-driver/mysql で datetime型のカラムのタイムゾーンを適切に扱う

MySQLdatetime型はタイムゾーンを保持しないため、MySQL側でJSTで取り扱うと決めたら、クライアント側で都度適切にタイムゾーンを変換する必要がある。

Go言語でこれを適切に行うためには、DB接続時に github.com/go-sql-driver/mysql のDSNで以下のようにする必要がある。なお、MySQL側ではJSTで保持するとする。

locale, _ := time.LoadLocation("Asia/Tokyo")
c := mysql.Config{
    DBName:    "dbname",
    User:      "root",
    Passwd:    "",
    Addr:      "db:3306",
    Net:       "tcp",
    Collation: "utf8mb4_bin",
    ParseTime: true,
    Loc:       locale,
}
dsn := c.FormatDSN()
db, _ := sql.Open("mysql", dsn)

まず、loc オプションで MySQL側のタイムゾーンを設定する(この場合はAsia/Tokyo)。そして、parseTime オプションを true にして Scan() 時に time.Time で受け取れるようにする。

このようにすることによって、MySQL側から得たdatetime型の値はタイムゾーンJSTtime.Time型に変換される。

db.Query("select cast('2022-11-01 10:00:00' as datetime)")
...
var t time.Time
rows.Scan(&t)
fmt.Println(t.Format(time.RFC3339)) // 2022-11-01T10:00:00+09:00

また、Go側からプレースホルダー経由でMySQLtime.Time型の値を渡したときは渡したtime.Time型のタイムゾーンがいずれであっても、MySQL側のタイムゾーンJSTに変換されようになる。

t, _ := time.Parse(time.RFC3339, "2022-11-01T10:00:00Z")
db.Query("select ?", t)
...
var tt string
rows.Scan(&tt)
fmt.Println(tt) // 2022-11-01 19:00:00

まとめると以下のようになる。

package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    "github.com/go-sql-driver/mysql"
)

func main() {
    locale, err := time.LoadLocation("Asia/Tokyo")
    if err != nil {
        panic(err)
    }
    c := mysql.Config{
        DBName:    "dbname",
        User:      "root",
        Passwd:    "",
        Addr:      "db:3306",
        Net:       "tcp",
        ParseTime: true,
        Loc:       locale,
    }
    dsn := c.FormatDSN()
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    {
        rows, err := db.Query("select cast('2022-11-01 10:00:00' as datetime)")
        if err != nil {
            log.Fatal(err)
        }
        for rows.Next() {
            var t time.Time
            err := rows.Scan(&t)
            if err != nil {
                log.Fatal(err)
            }
            fmt.Println(t.Format(time.RFC3339)) // 2022-11-01T10:00:00+09:00
        }
    }
    {
        t, err := time.Parse(time.RFC3339, "2022-11-01T10:00:00Z")
        if err != nil {
            log.Fatal(err)
        }
        rows, err := db.Query("select ?", t)
        if err != nil {
            log.Fatal(err)
        }
        for rows.Next() {
            var tt string
            err := rows.Scan(&tt)
            if err != nil {
                log.Fatal(err)
            }
            fmt.Println(tt) // 2022-11-01 19:00:00
        }
    }
}

ちなみに、github.com/go-sql-driver/mysql側では以下のようになっており、DSNで渡したlocオプションで指定したタイムゾーンに変換されるようになっている。

Go => MySQL:

switch v := arg.(type) {
...
case time.Time:
    ...
        b, err = appendDateTime(b, v.In(mc.cfg.Loc))
    ...

https://github.com/go-sql-driver/mysql/blob/fa1e4ed592daa59bcd70003263b5fc72e3de0137/packets.go#L1119

MySQL => Go:

func parseDateTime(b []byte, loc *time.Location) (time.Time, error) {
    ...
            return time.Date(year, month, day, hour, min, sec, 0, loc), nil
    ...
}

https://github.com/go-sql-driver/mysql/blob/fa1e4ed592daa59bcd70003263b5fc72e3de0137/utils.go#L167

なお、この記事で示したサンプルコードは sandbox/go-sql-driver-mysql-timezone at master · mrk21/sandbox にある。

環境

参考