🕐

Spanner × GORMでPENDING_COMMIT_TIMESTAMPを使用する

に公開

概要

SpannerにはcommitタイムスタンプをセットするためのPENDING_COMMIT_TIMESTAMP()という関数がありますが、これをGORMから利用するには少し工夫が必要です。
本記事ではgo-gorm-spannerに用意されているCommitTimestamp型の使い方と、利用時の注意点について紹介します。

PENDING_COMMIT_TIMESTAMPについて

https://docs.cloud.google.com/spanner/docs/commit-timestamp?hl=ja

CREATE TABLE Performances (
    ...
    LastUpdateTime  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
    ...
) PRIMARY KEY (...);

SpannerのTIMESTAMP型のカラムにallow_commit_timestamp=trueを設定することで、

UPDATE Performances SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP()
   WHERE SingerId=1 AND VenueId=2 AND EventDate="2015-10-21"

INSERTやUPDATEなどの際にPENDING_COMMIT_TIMESTAMP()を使用してcommitタイムスタンプをセットすることができるようになります。

allow_commit_timestampの制約

allow_commit_timestamp=trueを設定したカラムには未来の時刻を設定できなくなり、以下のようなエラーになります。

Cannot write timestamps in the future ...

例えば、

  • created_atというカラムにallow_commit_timestamp=trueを設定している。
  • GORMを使用している。
  • コード上で、CreatedAtはtime.Time型である。

という場合、基本的には自動で現在時刻がセットされますが、Spannerとアプリケーション側の時刻がわずかでもずれているとクエリが失敗する可能性があります。

GORMでのPENDING_COMMIT_TIMESTAMP使用

GORMでPENDING_COMMIT_TIMESTAMPを使用したい場合、go-gorm-spannerに用意されているCommitTimestamp型を用いることで実現できます。
https://github.com/googleapis/go-gorm-spanner/blob/main/commit_timestamp.go
実装は非常にシンプルで、以下のようになっています。

  • sql.NullTimeのラッパーである。
  • GormValue()で自身の値ではなく、PENDING_COMMIT_TIMESTAMP()を返すことでフィールドの値によらずcommitタイムスタンプが入るようになっている。

注意点としては以下が挙げられるかなと思います。

  • コード上で明示的に値をセットしてもPENDING_COMMIT_TIMESTAMP()が使用される。
  • Createなどの実行後も構造体のフィールドには値が入らない。

サンプルコード

基本的な使い方と注意点がわかるような簡易的なサンプルを以下に用意しました。

import (
	"database/sql"
	"fmt"
	"time"

	"github.com/googleapis/go-gorm-spanner"
)

type Performance struct {
	ID             string `gorm:"primaryKey"`
	LastUpdateTime gorm.CommitTimestamp
}

func main() {
	... // setup

	hoge := Performance{
		ID: "hoge",
	}
	_ = db.Create(&hoge)
	// => INSERT INTO `performances` (`id`,`last_update_time`) VALUES ('hoge',PENDING_COMMIT_TIMESTAMP())

	// Create後もゼロ値のまま
	fmt.Printf("LastUpdateTime: %#v\n", hoge.LastUpdateTime)
	// => LastUpdateTime: gorm.CommitTimestamp{Timestamp:sql.NullTime{Time:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), Valid:false}}

	fuga := Performance{
		ID: "fuga",
		// 明示的に値を入れてもPENDING_COMMIT_TIMESTAMP()が使用される。
		LastUpdateTime: gorm.CommitTimestamp{
			Timestamp: sql.NullTime{
				Time:  time.Now(),
				Valid: true,
			},
		},
	}
	_ = db.Create(&fuga)
	// => INSERT INTO `performances` (`id`,`last_update_time`) VALUES ('fuga',PENDING_COMMIT_TIMESTAMP())
}

まとめ

本記事では、go-gorm-spannerのCommitTimestamp型を使用して、SpannerのPENDING_COMMIT_TIMESTAMPをGORMから使用する方法を試しました。

  • コード上で明示的に値をセットしてもPENDING_COMMIT_TIMESTAMP()が使用される。
  • Createなどの実行後も構造体のフィールドには値が入らない。

といった注意点はありますが、これらを押さえれば問題なく使用できそうです。

REALITY Tech

Discussion