🐙

GORM で操る Cloud Spanner with PostgreSQL Interface

2023/02/14に公開

はじめに

Go で人気の OR マッパーである GORM が Cloud Spanner で利用できるようになりました!

そこで GORM を使った API を作って、Cloud Run にデプロイしてみたいと思います。
GORM の利用には PostgreSQL Interface に対応した Cloud Spanner のデータベースが必要になります。

Google Cloud の人気のデータストアである Cloud Spanner ですが、 クエリーや操作には、Google Standard SQL と呼ばれる SQL を使うか、API(SDK)による操作が必要でした。
しかし昨年 GA した PostgreSQL Interface を使うと、PostgreSQL の SQL が利用できるようになります。
これにより、様々な PostgreSQL 対応のドライバや OR マッパーが利用できるようになってきていますが、今回は GitHub にサンプルが公開されている GORM を使ってみます。
なお、PostgreSQL Interface の Cloud Spanner のデータベース接続には PgAdapter と呼ばれる PostgreSQL Interface の Proxy を利用します。

PostgreSQL Interface のドキュメントはこちら
今回試すソースコードはこちら
サンプルのリポジトリ内にある create_data_model.sql 及び、sample.go(main 関数 を消去したもの)は、そのまま利用しています。

試す順番

  1. 準備
  2. ローカル環境から、psql を使ってスキーマを作成
  3. ローカル環境から、GORM を使って初期データを登録
  4. Cloud Run に GORM を使ったアプリケーションをデプロイ

0. 準備

  1. まず gcloud コマンドと Google Cloud のライブラリに権限を与えるため、ログインします
gcloud auth login
gcloud auth application-default login
  1. Cloud Spanner を構築して、PostgreSQL Interface 対応でデータベースを作成します
    今回は、3ヶ月無料で試せる無料トライアルインスタンスを使ってみます!
gcloud spanner instances create test-instance --config=regional-us-east5 \
    --instance-type=free-instance --description="Trial Instance"

無料トライアルインスタンスは利用できるリージョンが限られるのでご注意ください。
上記のコマンドではオハイオ州コロンバスを指定しています。
なお、無料トライアルインスタンスは性能が限定されているため、主に検証/テスト用途でご利用下さい。

  1. 手元にソースコードを clone しておきましょう
git clone https://github.com/shin5ok/simple-gorm-with-cloud-spanner.git working
cd working

今後は clone したディレクトリ内で作業します。

music データベースを、PostgreSQL Interface で作成します。

gcloud spanner databases create music --instance test-instance \
--database-dialect=POSTGRESQL

1. ローカル環境から、psql を使ってスキーマを作成

PostgreSQL の CLI ツール psql コマンドで Cloud Spanner に接続し、スキーマを作成します。
なお GORM には AutoMigrate() というモデルから自動的にスキーマを生成する機能がありますが、現在は動作しないようです。

以下はローカル環境に、Docker 実行環境と、PostgreSQL の psql コマンドが必要です。
事前に準備してください。

  1. PgAdapter をコンテナで実行します
    ドキュメントはこちら
docker run -d -p 5432:5432 \
    -v ~/.config:/root/.config \
    gcr.io/cloud-spanner-pg-adapter/pgadapter:latest \
    -p $GOOGLE_CLOUD_PROJECT -i test-instance -d music -x

※ Cloud Shell で実行する場合は、2行目を下記のように指定します)

    -v $CLOUDSDK_CONFIG:/.config/gcloud
  1. PostgreSQL の psql コマンドで接続します
psql -h localhost -d music

こちらで接続できたら、一度 exit します。

  1. psql コマンドでスキーマを作ります
    まず、作成するスキーマのファイルを確認します(schemas/create_data_model.sql)
create table if not exists singers (
    id         varchar not null primary key,
    first_name varchar,
    last_name  varchar not null,
    full_name  varchar generated always as (coalesce(concat(first_name, ' '::varchar, last_name), last_name)) stored,
    active     boolean,
    created_at timestamptz,
    updated_at timestamptz
);

create table if not exists albums (
    id               varchar not null primary key,
    title            varchar not null,
    marketing_budget numeric,
    release_date     date,
    cover_picture    bytea,
    singer_id        varchar not null,
    created_at       timestamptz,
    updated_at       timestamptz,
    constraint fk_albums_singers foreign key (singer_id) references singers (id)
);

create table if not exists tracks (
    id           varchar not null,
    track_number bigint not null,
    title        varchar not null,
    sample_rate  float8 not null,
    created_at   timestamptz,
    updated_at   timestamptz,
    primary key (id, track_number)
) interleave in parent albums on delete cascade;

singers テーブル、albums テーブル、tracks テーブルの定義を抜粋しています。

こちらは Cloud Spanner のベストプラクティスに沿った設計になっています。
それぞれ id フィールドがプライマリキーになっていますが、Auto Increment は使用せず、クライアントで生成した UUID などの値をセットする想定です。
tracks テーブルは、albums テーブルの Interleave になっていて、id と track_number の複合プライマリキーになっています。

では早速、スキーマを作成します。

psql -h localhost -d music < ./schemas/create_data_model.sql

実行後、再度 psql コマンドで接続して、スキーマを確認しましょう。

Google Cloud コンソールから確認すると、こんな感じです。

2. ローカル環境から、GORM を使って初期データを登録

現在、ローカルの環境にも、Google Cloud の操作権限がある状態です。
こちらを使って、musicデータベースに初期データを登録してみます。

GORM では、スキーマをモデルで表しますので、モデルを確認してみます。

  1. モデルを確認してみる
    sample.go で定義された GORM のモデルを抜粋して見てみます(コメントは消しました)
type BaseModel struct {
	ID string `gorm:"primaryKey;autoIncrement:false"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

type Singer struct {
	BaseModel
	FirstName sql.NullString
	LastName  string
	FullName string `gorm:"->;type:GENERATED ALWAYS AS (coalesce(concat(first_name,' '::varchar,last_name))) STORED;default:(-);"`
	Active   bool
	Albums   []Album
}

type Album struct {
	BaseModel
	Title           string
	MarketingBudget decimal.NullDecimal
	ReleaseDate     datatypes.Date
	CoverPicture    []byte
	SingerId        string
	Singer          Singer
	Tracks          []Track `gorm:"foreignKey:ID"`
}

type Track struct {
	BaseModel
	TrackNumber int64 `gorm:"primaryKey;autoIncrement:false"`
	Title       string
	SampleRate  float64
	Album       Album `gorm:"foreignKey:ID"`
}

スキーマと同様、プライマリキーの Auto Increment を false にしています。

  1. 初期データを登録する
    PgAdapter 経由で接続するよう、環境変数を設定します。
export CONNECTION_STRING='host=localhost port=5432'

データベース名を指定していませんが、PgAdapter 起動時に指定しているため、ここでは省略できます。

コマンドラインに -init 引数をつけて、実行します。

go run . -init

こちらの操作は sample.go の CreateRandomSingersAndAlbums 関数を実行して、終了します。
実行したら、psql コマンドで接続して確認してみましょう。

psql -h localhost -d music

いくつかの SQL を発行して、確認します。

select * from singers;
select * from albums;
select * from tracks;

このように適切な権限があれば、ローカル環境から Google Cloud 上の Cloud Spanner を問題なく操作できます。

3. Cloud Run に GORM を使ったアプリケーションをデプロイ

アプリケーションは、chi というシンプルなフレームワークで動作する Web API です。
詳しい説明は省略しますが、ご興味がある方は main.go をご確認下さい。

今回は、アプリケーション実行プラットフォームとして Cloud Run を使います。
Cloud Run はシンプルにコンテナアプリケーションを実行できる魅力的なサーバーレスプラットフォームですが、まだマルチコンテナに対応していません。
将来マルチコンテナに対応すれば、2 で利用した PgAdapter のコンテナをそのまま利用できるのではないかと期待できます。

PgAdapter を Java のスタンドアロンプロセスとして実行し、かつメインのアプリケーションも実行する必要があるため、Supervisor を使って1コンテナ内で複数プロセスを実行します。
コンテナ内で複数プロセスを実行するとシグナルのハンドリングなどに問題がでますが、Supervisor を使うのは定番の解決方法の一つです。

  1. Cloud Run で利用するサービスアカウントを準備します
    サービスアカウントを作成して、Cloud Spanner の データベースユーザーの権限をアタッチします。
gcloud iam service-accounts create music-api
export SA=music-api@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com

gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT --member=serviceAccount:$SA --role=roles/spanner.databaseUser
  1. アプリケーションのビルドのため、Dockerfile を用意します
    今回はこんな感じで用意しました。
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
RUN GGO_ENABLED=0 GOOS=linux go build -o main

FROM openjdk:slim-buster AS runner
RUN apt update && apt -y install curl supervisor
RUN curl -sL https://storage.googleapis.com/pgadapter-jar-releases/pgadapter.tar.gz \
  | tar xzf - -C /
COPY --from=builder /app/main /main
COPY supervisord.conf /etc/supervisor/supervisord.conf
CMD ["supervisord"]

supervisor の設定である supervisord.conf はこちら(抜粋)

[program:pgadapter]
command=java -jar pgadapter.jar -p %(ENV_PROJECT_ID)s -i %(ENV_INSTANCE_NAME)s -d %(ENV_DATABASE_NAME)s
priority=10
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0

[program:main]
command=/main
priority=20
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0

PgAdapter の起動箇所では、環境変数からパラメータを取得して、引数として設定しています。

PgAdapter の起動から接続可能になるまで少し時間がかかるため、アプリケーション側で一定回数接続をリトライするようにしています。

  1. Cloud Run をデプロイ
    事前に作成したサービスアカウントを指定しつつ、Cloud Build を利用してビルド、デプロイします。
CONNECTION_STRING="host=localhost port=5432"
gcloud run deploy --source=. \
--service-account=$SA gorm-test \
--set-env-vars=PROJECT_ID=$GOOGLE_CLOUD_PROJECT,INSTANCE_NAME=test-instance,DATABASE_NAME=music,CONNECTION_STRING="$CONNECTION_STRING" \
--region=us-east5 \
--cpu=1 --memory=2Gi

しばらく待つと、Cloud Run のデプロイが完了して、URL が発行されます。
URL 例

https://gorm-test-xxxxxx-uc.a.run.app
  1. テスト
    URL を変数に入れておきます。
    デバッグのため、GORM が生成する SQL については、ログに出すようにしているので必要に応じて確認して下さい。
URL=https://gorm-test-xxxxxx-uc.a.run.app
  • Singer と Album を追加
data='{"first_name":"Foo", "last_name": "Bar", "album_name":"Baz"}'
echo $data | curl -s -H "Content-Type: application/json" $URL/api/register-singer-with-album -X POST -d @-

レスポンスはこんな感じで、新しい Singer と Album の ID を返します。

{"album_id":"2a45186b-f9e7-489f-ba8e-f4d346a375f8","singer_id":"e72f26b7-76f5-49ee-a7fd-a29dc473683a"}

トランザクションを使って、Singer と Album、Track を一気に登録しています。
コード抜粋

	if err := m.db.Transaction(func(tx *gorm.DB) error {
		singerId, err := CreateSinger(tx, postData.FirstName, postData.LastName)
		if err != nil {
			return err
		}
		albumId, err := CreateAlbumWithRandomTracks(tx, singerId, postData.AlbumName, randInt(1, 22))
		if err != nil {
			return err
		}
		newSingerId = singerId
		newAlbumId = albumId
		return nil
	}); err != nil {
		errorRender(w, r, http.StatusInternalServerError, err)
		return
	}
  • Singer の ID を指定してマッチする情報を取得
    先ほど返ってきた Singer の ID を指定して、Album の情報を取得します。
curl -s $URL/api/get-albums-of-singer/e72f26b7-76f5-49ee-a7fd-a29dc473683a

Album のカバーなどの画像情報なども登録されているので、たくさんの情報が返ってきます。
ここでは、GORM の Preload を使って、明示的に JOIN を使わず関連データをクエリーしています。
コード抜粋

	var albums []*Album
	singerId := chi.URLParam(r, "singerId")
	if err := m.db.Model(&Album{}).Preload(clause.Associations).
		Where("singer_id = ?", singerId).Find(&albums).Error; err != nil {
		errorRender(w, r, http.StatusInternalServerError, err)
		return
	}

すべて成功すれば、テスト完了です。

おわりに

人気の ORマッパーである GORM に対応したことで、アプリケーション側の選択肢が増え、さらに Cloud Spanner の導入がしやすくなったのではないでしょうか。
PostgreSQL Interface は、現時点では一部機能※が標準 SQL の Cloud Spanner のデータベースに追いついていないところがありますが、すでに GA しているので要件に合う場合はご利用頂ければと思います。
(※ 2023年2月時点で、JSON型、CDC、エミュレータ等、未対応の機能があります)

ぜひお試しください!

Google Cloud Japan

Discussion