sqlxとpostgresでtimezoneを扱う
といってもほぼdatabase/sqlの話題になってしまっているかも。
この記事はQiita主催のGo Advent Calendar 2022の21日目の記事です。
書くぞとはいったもののネタが思いつかなかったので重箱の隅を突くような小ネタになります。
やりたいこと
UTCでデータを入れているpostgresに対して、Go側で任意のタイムゾーンでpostgresのtimestamp型を取り出したい
元データ
sample-db=# \d test_timezone
Table "public.test_timezone"
Column | Type | Collation | Nullable | Default
-----------+--------------------------+-----------+----------+---------
test_time | timestamp with time zone | | |
sample-db=# SELECT * from test_timezone;
test_time
------------------------
2022-12-21 15:00:00+00
(1 row)
得たい出力
2022-12-22 00:00:00 +0900 JST
そのまま取得
そのまま取得してみた結果がこちら
var examTime time.Time
db.Get(&examTime, "SELECT test_time from test_timezone")
fmt.Println(examTime)
// 出力
// 2022-12-21 15:00:00 +0000 UTC
time.Now()とかだとGoを実行するサーバにタイムゾーンが自動で寄せられていたりしますが、このケースの場合サーバ自体のtimezoneは影響しないようです。
timestamp型 OR timestamptz型
データがtimestamptz型でないとtimezone自体取得されません。
var examTime time.Time
db.Get(&examTime, "SELECT test_time::timestamp from test_timezone")
fmt.Println(examTime)
// 出力
// 2022-12-21 15:00:00 +0000 +0000
そのためpostgresのtimezone関数を使うとこう。時刻はいい感じですがtimezone関数と言いながらも返り値の型がtimestamp型のため、timezone情報が取得できません。ウーン。
PostgreSQL 8.4.4文書 9.9. 日付/時刻関数と演算子
var examTime time.Time
db.Get(&examTime, "SELECT timezone('JST', test_time::timestamptz) FROM test_timezone;")
fmt.Println(examTime)
// 出力
// 2022-12-22 00:00:00 +0000 +0000
答え
セッションのtimezoneをいい感じにしてあげると想定通りの値が取れます。
一回変えてしまうと明示的に戻すまでそのコネクションはずっとtimezoneが変わりっぱなしなのでそこだけ注意。
var examTime time.Time
db.Exec("SET SESSION timezone TO 'Asia/Tokyo';")
db.Get(&examTime, "SELECT test_time from test_timezone")
fmt.Println(examTime)
// 出力
// 2022-12-22 00:00:00 +0900 JST
別解
とはいえ、これだとただのpostgres小ネタになってしまう。。。ので、実用性はともかくGo上でtimezoneを制御する方法を。。。
このあたりを参考にScan関数を実装してあげると読み取り時に自前の処理を書くことができます。
type MyTime time.Time
const TZ = "Asia/Tokyo"
// println用
func (m MyTime) String() string {
return time.Time(m).String()
}
// 読み取り関数実装
func (m *MyTime) Scan(value any) error {
if value == nil {
m = &MyTime{}
return nil
}
t, ok := value.(time.Time)
if !ok {
return fmt.Errorf("型が違うよ")
}
tz, _ := time.LoadLocation(TZ)
mt := MyTime(t.In(tz))
*m = mt
return nil
}
func createMyType(db *sqlx.DB) {
var examTime MyTime
db.Get(&examTime, "SELECT test_time from test_timezone")
fmt.Println(examTime)
}
// 出力
// 2022-12-22 00:00:00 +0900 JST
自前の型だとそれはそれで面倒なこともあるので自分はtimestampで使うことはないと思いますが、json型や配列型をSELECTしてそのまま構造体に入れたいときにScanメソッド内でUnmarshalするなど結構便利な小技です。
おわりに
フロントエンドに日時の値を投げるという場面でこういうことを考えていたのですが、普通にUTCで投げてフロントエンド側で対応すればよいというアドバイスを頂き、とここに書いたものはボツとなりました。
個人メモの供養も兼ねて。
Discussion