github.com/go-sql-driver/mysql で datetime型のカラムのタイムゾーンを適切に扱う
MySQLのdatetime
型はタイムゾーンを保持しないため、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
型の値はタイムゾーンがJSTのtime.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側からプレースホルダー経由でMySQLに time.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)) ...
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 にある。