🗓️

sqlxとpostgresでtimezoneを扱う

2022/12/22に公開

といってもほぼ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