goのTimeは日時とタイムゾーンの情報を持っている。日本標準時のタイムゾーンを持ったTimeを作ってみる。
location, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Fatal(err)
}
t := time.Date(2019, 1, 2, 3, 4, 5, 0, location) // 2019年1月2日3時4分5秒
これをmysqlに突っ込む。mysqlとのコネクションはこんな感じ。
cfg := mysql.Config{
User: "root",
Passwd: "password",
Net: "tcp",
Addr: "127.0.0.1:4306",
DBName: "time",
ParseTime: true,
}
Locは指定していない。その場合UTCになる。
https://github.com/go-sql-driver/mysql/blob/191a7c4c519ef60cf3e8656fde8728eee9194308/dsn.go#L73
// NewConfig creates a new Config and sets default values.
func NewConfig() *Config {
return &Config{
Collation: defaultCollation,
Loc: time.UTC,
MaxAllowedPacket: defaultMaxAllowedPacket,
Logger: defaultLogger,
AllowNativePasswords: true,
CheckConnLiveness: true,
}
}
go-mysql-driverで生のSQLを書いてmysqlにINSERTする
db.Exec("INSERT INTO `time` (`id`, `time`) VALUES (?, ?);", "Asia/Tokyo 2019-01-02T03:04:05", t)
このとき発行されるクエリをgeneral_logで確認すると、この段階でUTCの 2019-01-01 18:04:05
に変換されている。
2023-05-06T14:33:27.581647Z 9 Execute INSERT INTO `time` (`id`, `time`) VALUES ('Asia/Tokyo 2019-01-02T03:04:05', '2019-01-01 18:04:05')
これはgo-mysql-driverがTimeをシリアライズする前にInでタイムゾーンをUTCに変換しているからだ。
https://github.com/go-sql-driver/mysql/blob/191a7c4c519ef60cf3e8656fde8728eee9194308/packets.go#L1119
b, err = appendDateTime(b, v.In(mc.cfg.Loc))
ではbunでTimeをINSERTするとどうなるか
type BTime struct {
bun.BaseModel `bun:"time2"`
ID string
Time time.Time
}
bundb.NewInsert().Model(&BTime{ID: "BUN Asia/Tokyo 2019-01-02T03:04:05", Time: t}).Exec(ctx)
このとき発行されるクエリでは、Timeに設定されているタイムゾーンを無視して 2019-01-02 03:04:05
とシリアライズしている。
2023-05-06T14:41:39.343913Z 10 Query INSERT INTO `time2` (`id`, `time`) VALUES ('BUN Asia/Tokyo 2019-01-02T03:04:05', '2019-01-02 03:04:05')
どうやらこれは意図的な変更の結果らしい。mysqlやgo-mysql-driverなどと二重変換してしまって正常に動作しないという問題があったようだ(←わかってない)。
https://github.com/uptrace/bun/issues/168
とにかく、じゃあどうやれば安心してDATETIMEを扱えるんだよという話になるんだけど、mysqlとまたがる範囲で暗黙的な変換が入ると状態の把握が難しくなるので、goのアプリケーション側で確実にUTCにしちゃってからORMなりクエリビルダーなりに放り込むことにした。そうすれば少なくともバグったときにfmt.Printfでなんとかなる。
goとmysqlの間で苦しんでる人はたくさんいた(類似記事が多い)がbunの話してる人は全然いなかったのでテキトーに書いてみた。
おまけ dockerのmysqlでgeneral_logを見る
適当なファイルに以下を書いておいて
[mysqld]
general_log=1
そのファイルが入ったディレクトリを、コンテナの/etc/mysql/conf.dにマウントする
-v /path/to/cnf-dir/:/etc/mysql/conf.d
コンテナ起動後にコンテナに入ってファイルの場所を探してtailする
nerdctl exec -it <container_id> /bin/sh
mysql -u root --host 127.0.0.1 -p
> SHOW VARIABLES LIKE '%general_log%';
> exit;
tail /path/to/general.log
なんで general_log_file 使わないの?
なんか general_log_file=/var/log/mysql/general.log
するとmysqlの設定値はそこになるんだけどファイルが作られないんだよね。パーミッションの問題とかあるのかな。
上記の方法でやるとログファイルは /var/lib/mysql/hoge.log
に生える。