github.com/go-sql-driver/mysql で date型のカラムをtime.Time型で扱うと日付がズレるのを回避する
以下の記事で示したように、github.com/go-sql-driver/mysql
で parseTime
、loc
オプションを適切に設定することで、Go の time.Time
型とMySQLの datetime
型をタイムゾーンを考慮して適切に相互変換できる。
しかし、同様にMySQLのdate
型をtime.Time
型と相互変換しようとすると日付がズレる(ここではMySQL側のタイムゾーンをJSTとする)。
const RFC3339Date = "2006-01-02" ... db.Exec("create table test_date(dt date)") ... date1 := time.Date(2022, 11, 2, 0, 0, 0, 0, time.UTC) db.Exec("insert into test_date(dt) values (?)", date1) rows, err := db.Query("select dt from test_date") var date2 time.Time for rows.Next() { rows.Scan(&date2) break } // date1: 2022-11-02(2022-11-02T00:00:00Z), date2: 2022-11-01(2022-11-01T15:00:00Z) fmt.Printf("date1: %s(%s), date2: %s(%s)\n", date1.UTC().Format(RFC3339Date), date1.UTC().Format(time.RFC3339), date2.UTC().Format(RFC3339Date), date2.UTC().Format(time.RFC3339), )
これは、Goでは日付をtime.Time
型(時刻、タイムゾーンあり)で扱うので、Go => MySQL/MySQL => Goでのタイムゾーンの変換と、時刻/タイムゾーンの情報が欠落してしまうことに起因する。具体的には以下のようになる。
2022-11-02
をタイムゾーンがUTCのtime.Time
型に格納:2022-11-02T00:00:00Z
- MySQLのタイムゾーンJSTに変換:
2022-11-02T09:00:00+09:00
- MySQLの
date
型に格納:2022-11-02
- タイムゾーンがJSTの
time.Time
型に格納:2022-11-02T00:00:00+09:00
- タイムゾーンをUTCに変換:
2022-11-01T15:00:00Z
- 日付を抽出:
2022-11-01
ここで 4. のようになるのは、github.com/go-sql-driver/mysql
ではMySQLのdate
型からtime.Time
型の値を生成するときは、以下のように時刻が00:00:00
でタイムゾーンがloc
オプションで示したものとなるからである。
func parseDateTime(b []byte, loc *time.Location) (time.Time, error) { ... if len(b) == 10 { return time.Date(year, month, day, 0, 0, 0, 0, loc), nil }
https://github.com/go-sql-driver/mysql/blob/fa1e4ed592daa59bcd70003263b5fc72e3de0137/utils.go#L139
この問題を防ぐには、date
型を文字列でやり取りすればいいのだが、いちいちtime.Time
型に変換するのは面倒である。そのため、タイムゾーンなしの日付型を新たにつくり、database/sql
で直接扱えるようにする。これには対象の型にdriver.Valuer
およびsql.Scanner
インタフェースを満たすようにValue() (driver.Value, error)
/Scan(value interface{}) error
メソッドを定義する。
type Date struct { year int month time.Month day int } func NewDate(year int, month time.Month, day int) *Date { t := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) return &Date{ year: t.Year(), month: t.Month(), day: t.Day(), } } func (d *Date) Year() int { return d.year } func (d *Date) Month() time.Month { return d.month } func (d *Date) Day() int { return d.day } func (d *Date) Format(layout string) string { return d.Time(nil).Format(layout) } func (d *Date) Time(loc *time.Location) time.Time { if loc == nil { loc = time.Local } return time.Date(d.year, d.month, d.day, 0, 0, 0, 0, loc) } // Go => MySQL func (d Date) Value() (driver.Value, error) { return driver.Value(d.Format(RFC3339Date)), nil } // MySQL => Go func (d *Date) Scan(value interface{}) error { var t mysql.NullTime err := t.Scan(value) if err != nil { return err } d.day = t.Time.Day() d.month = t.Time.Month() d.year = t.Time.Year() return nil } // interface check var _ driver.Valuer = (*Date)(nil) var _ sql.Scanner = (*Date)(nil)
そうすると、以下のようにdatabase/sql
で直接扱え、日付がズレることもなくなる。
date1 := NewDate(2022, 11, 2) _, err = db.Exec("insert into test_date(dt) values (?)", date1) if err != nil { log.Fatal(err) } rows, err := db.Query("select dt from test_date") if err != nil { log.Fatal(err) } var date2 Date for rows.Next() { rows.Scan(&date2) break } // date1: 2022-11-02, date2: 2022-11-02 fmt.Printf("date1: %s, date2: %s\n", date1.Format(RFC3339Date), date2.Format(RFC3339Date), )
なおここで示したサンプルコードは以下にある。