こんにちは。宿泊事業本部の宇都宮です。
一休では、基幹データベースにSQL Serverを使用しています。また、Goアプリケーションでは、go-mssqldbというライブラリを使用して、データベースとのやりとりを行っています。
このgo-mssqldbには、タイムゾーンに関して厄介な挙動があります。タイトルにもあるように、タイムゾーンが常にUTCになってしまうのです。本記事では、go-mssqldbのタイムゾーン関係の振る舞いと、go-mssqldbを使いつつ正しくタイムゾーンを扱うための対処法を紹介します。
go-mssqldbのタイムゾーン問題
go-mssqldbのタイムゾーン問題は、以下のコードで再現できます。
package main import ( "database/sql" "fmt" "log" "os" "time" // blank import必須 _ "github.com/denisenkom/go-mssqldb" ) func main() { conn, err := sql.Open("sqlserver", "sqlserver://user:password@dbhost:1433?ApplicationIntent=ReadWrite&MultiSubnetFailover=Yes&database=DB") if err != nil { fmt.Fprint(os.Stderr, err) os.Exit(1) } row := conn.QueryRow(` select GETDATE() `) var dbnow time.Time if err := row.Scan(&dbnow); err != nil { fmt.Fprint(os.Stderr, err) os.Exit(1) } now := time.Now() after1Hour := now.Add(1 * time.Hour) fmt.Println(after1Hour.After(now)) fmt.Println(after1Hour.After(dbnow)) fmt.Println(now) fmt.Println(after1Hour) fmt.Println(dbnow) }
dbnowにはDBの現在時刻から取得した時刻が、nowにはサーバの現在時刻が入ります。after1Hourはサーバの現在時刻から1時間後です。したがって、 after1Hour.After(now)
と after1Hour.After(dbnow)
は、DBとサーバの時計が1時間以上ずれていない限り、trueになるはずです。
しかし、DBとサーバの両方のタイムゾーンがJST(+09:00)の状態でこのコードを実行すると、 after1Hour.After(now)
はtrue、after1Hour.After(dbnow)
はfalseになります。
原因は、このコードの出力を見ると分かります。
dbnow
は日時は同じ(2019-08-23 15:54)ですが、タイムゾーンがUTCになっています。UTCでJSTと表面上の日時が同じと言うことは、実質9時間後の日時ということです。そのため、after1Hour.After(dbnow)
はfalseになってしまうのです。
タイムゾーン問題の対処方法
go用の mysqlドライバ のように、Data Source NameにDBサーバのロケーションを指定できる機能があればよいのですが、go-mssqldbにはそのような機能はありません。
この問題を解決しようとしているissueやプルリクエストはいくつか見つかりますが、近いうちに取り込まれそう、というステータスではありません。
そこで、ライブラリ側の解決を待つのではなく、利用者の側で対処する必要があります。
最も根本的な対応は、datetimeoffset型の使用です。datetimeoffset型はタイムゾーンも保持しているため、日時の保存にこの型を使っておけば、go-mssqldbを使っていても問題なくタイムゾーンを扱えます。
すでにdatetime型で日時を保存していて、datetimeoffset型での保存が難しい場合は、TODATETIMEOFFSET関数を使ってデータを取得する際に型変換しましょう。
row := conn.QueryRow(` select GETDATE() , TODATETIMEOFFSET(GETDATE(), '+09:00') `) var dbnow time.Time var dbnowoffset time.Time if err := row.Scan(&dbnow, &dbnowoffset); err != nil { fmt.Fprint(os.Stderr, err) os.Exit(1) } now := time.Now() after1Hour := now.Add(1 * time.Hour) fmt.Println(after1Hour.After(now)) fmt.Println(after1Hour.After(dbnow)) fmt.Println(after1Hour.After(dbnowoffset)) fmt.Println(now) fmt.Println(after1Hour) fmt.Println(dbnow) fmt.Println(dbnowoffset)
dbnowoffset は+0900(JST)になっているため、after1Hour.After(dbnowoffset)
はtrueになります。意図したとおりの動作といえます。