Kotlin + Spring Boot + Cloud Spanner 入門
Ubie では Kotlin + Spring Boot で作っているサービスが複数あり、そのうちいくつかは DB に Cloud Spanner を使用しています。
このあたりの実装を進めていると全然まとまった情報に出会えなかったので、今回は「これから Spring で Cloud Spanner を使おう」と思っている方向けに、ドキュメントへのリンク多めでまとめていこうと思います。
そもそも Cloud Spanner とは
Cloud Spanner とは Google Cloud が提供するサービスの一つで、端的に言うと 分散型リレーショナルデータベース [1]といった感じでしょうか。
リレーション構造を持ちながらも read, write ともに処理が分散されるので水平スケーラビリティを担保したデータベースです。
なお、最近 PostgreSQL Interface が提供され初めましたが、今回はこれは使わずに実装していきます。
この記事ではこれ以降 Cloud Spanner について詳しくは述べないので、詳しくは公式ドキュメントを参照ください。
Spring + Cloud Spanner
Cloud Spanner 向けのライブラリは様々な言語、フレームワーク向けに提供されていますが、 Java 向けの選択肢は特に多いです。
まず1つ目の選択肢としては、 Spring 傘下で Spring Data プロジェクトの一員である Spring Data Cloud Spanner を使う手が有力です。
GCP 公式ガイドにも比較的詳しく書かれており、 Spring Data JPA と Hibernate ORM のような機能が Spanner 向けに実装されています。
機能的には Batch DML 以外は全てサポートされており、更新も活発です。
これを使った、Spring Boot application with Cloud Spanner という Java で書かれたチュートリアル(Codelab)があり、ページ数も少ないのでこちらをサクッと見てみるのもイメージが湧いて良いと思います。
2つ目の選択肢として Hibernate ORM を Cloud Spanner 向けに使うための Google Cloud Spanner Dialect for Hibernate ORM を使う手があります。
一応 GCP 公式ガイド もありますが、「Hibernate ORM を知ってるなら使い方はわかるよね?」と言わんばかりで、ほぼほぼリンク集としての役割しかありません。
使用には一部の制限があるのと、Mutationが使えませんが、基本的な機能は概ねサポートされています。
こちらも Cloud Spanner with Hibernate ORM という Codelab があるので興味があれば見てみると良さそうです。
これら以外の選択肢も一応ありますが、現実的には上記のどちらかを選ぶことになりそうです。
簡単に紹介だけしておきます。
-
Google Cloud Spanner Client for Java
- Google 公式に提供されている Client Library で、依存も無い
- Spring Data Cloud Spanner はこのライブラリに依存している
- かなり重厚なチュートリアルも公式で提供されている
-
Google Google Cloud Spanner JDBC Client for Java
- Google 公式に提供されている Spanner 向けの JDBC
- Google Cloud Spanner Dialect for Hibernate ORM はこのライブラリに依存している
-
Cloud Spanner R2DBC Driver
- R2DBC(The Reactive Relational Database Connectivity) プロジェクト向けに Spanner を使えるようにしたもの
- Spring Data ユーザー向けに Cloud Spanner Spring Data R2DBC というものもある
今回は Spring Data Cloud Spanner を使った実装 をしていきます。
実際に使ってみる
0. Set Up
Cloud Spanner インスタンスと DB を作っておきましょう[2]。
今回は実際の Spanner ではなく、公式から提供されている Emulator を使用します。
$ gcloud emulators spanner start
$ gcloud config configurations create emulator
$ gcloud config set auth/disable_credentials true
$ gcloud config set project sample-prj
$ gcloud config set api_endpoint_overrides/spanner http://localhost:9020/
$ gcloud spanner instances create test-instance \
--config=emulator-config --description="Test Instance" --nodes=1
$ gcloud spanner databases create test-db --instance test-instance
準備はできたので、DB とテーブルを作っておきます。
$ gcloud spanner databases ddl update test-db --instance test-instance --ddl='
CREATE TABLE orders (
order_id STRING(36) NOT NULL,
description STRING(255),
creation_timestamp TIMESTAMP,
) PRIMARY KEY (order_id)'
$ gcloud spanner databases ddl update test-db --instance test-instance --ddl='
CREATE TABLE order_items (
order_id STRING(36) NOT NULL,
order_item_id STRING(36) NOT NULL,
description STRING(255),
quantity INT64,
) PRIMARY KEY (order_id, order_item_id),
INTERLEAVE IN PARENT orders ON DELETE CASCADE'
軽くデータを入れてテストしておきましょう
$ gcloud spanner rows insert --instance=test-instance --database test-db --table=orders --data=order_id=1c181c2e-b37c-47a2-8cd6-9b418032a391,description="First Test"
$ gcloud spanner databases execute-sql --instance test-instance test-db --sql='SELECT * FROM orders'
# order_id description creation_timestamp
# 1c181c2e-b37c-47a2-8cd6-9b418032a391 First Test
この辺の手順は spanner-cli
という CLI ツールを使うとインタラクティブにテーブルをいじれたりするのでこちらもおすすめです。
$ $(gcloud emulators spanner env-init)
$ spanner-cli -p your-project-id -i test-instance -d test-db
# Connected.
# spanner> show databases;
# +----------+
# | Database |
# +----------+
# | test-db |
# +----------+
# 1 rows in set (0.01 sec)
1. Install
Spring Data Cloud Spanner (spring-cloud-gcp-starter-data-spanner
) を install していきます[3]。
Maven:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gcp-starter-data-spanner</artifactId>
</dependency>
Gradle:
dependencies {
implementation("org.springframework.cloud:spring-cloud-gcp-starter-data-spanner")
}
Cloud Spanner への接続情報を application.properties
や application.yml
に書いておきます。
spring:
cloud:
gcp:
spanner:
instance-id: test-instance
database: test-db
project-id: sample-prj
これで準備は整いましたが、もし Intellij IDEA を利用する場合は実行時に環境変数を読み込まれるように Run Configurations で Environment Variable に追加しておきましょう。
2. Object Mapping
Spanner のデータ構造をマッピングする Object を定義していきましょう。
@Table(name="orders")
class Order (
@PrimaryKey
@Column(name="order_id")
val id String,
val description String,
@Column(name="creation_timestamp")
val timestamp LocalDateTime,
@Interleaved
val items List<OrderItem>,
) { }
@Table(name="order_items")
class OrderItem (
@PrimaryKey(keyOrder = 1)
@Column(name="order_id")
val orderId String,
@PrimaryKey(keyOrder = 2)
@Column(name="order_item_id")
val orderItemId String,
val description String,
val quantity Long,
) { }
ポイントはこの辺りです。
- インターリーブを表現するために
@Interleaved
アノテーションをつけておく - OrderItem は
PRIMARY KEY (order_id, order_item_id)
に合わせてkeyOrder
を書いておく - Spanner の
Int64
型は Kotlin のLong
型に対応させる - Spanner の
TIMESTAMP
型はcom.google.cloud.Timestamp
も使えるが LocalDateTime でも OK - 今回はシンプルに
class
で書いているが、data class
にも対応しているので用途に合わせて変更する
3. 読み書きの実装
3パターンの実装方法があり、それぞれ特徴があります。
-
SpannerRepository
を使う -
SpannerTemplate
を使う -
DatabaseClient
を使う
今回は簡単のために SpannerRepository
を使うパターンで実装してみます。
Spring Data Cloud Spanner では SpannerRepository
interface が提供されています。
SpannerRepository
は Spring Data の PagingAndSortingRepository
, CrudRepository
を継承しているため、使い方としてはこれらと同様です。
SpannerRepository
を継承したinterface を定義して使います。
ポイントとしては
- CRUD 周りの関数を自動生成をしてくれるので便利
- カスタムクエリもアノテーションで対応可能
- 一方、命名規則などを覚える必要がある
といった感じでしょうか。
実際に実装してみましょう。
interface を定義しておき、
import org.springframework.cloud.gcp.data.spanner.repository.SpannerRepository
interface OrderRepository : SpannerRepository<Order, String>
あとは Spring Data と同様に、こんな感じで利用できます。
class OrderController(
private val orderRepository: OrderRepository,
private val orderItemRepository: OrderItemRepository,
) {
@GetMapping("/order")
fun index(): Order? {
val order = orderRepository.save(
Order(
id = "11111111-2222-3333-4444-555555555555",
description = "Test Order",
timestamp = LocalDateTime.now(),
items = emptyList(),
)
)
orderItemRepository.saveAll(
arrayListOf(
OrderItem(
orderId = order.id,
orderItemId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
description = "Test Order Item",
quantity = 3
),
OrderItem(
orderId = order.id,
orderItemId = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
description = "Test Order Item",
quantity = 1
)
)
)
val ret = orderRepository.findById(order.id)
return ret.orElseGet(null)
}
spanner-cli で確認してみるとこんな感じ[4]。
spanner> SELECT * FROM orders INNER JOIN order_items USING (order_id);
+--------------------------------------+-------------+-----------------------------+--------------------------------------+-----------------+----------+
| order_id | description | creation_timestamp | order_item_id | description | quantity |
+--------------------------------------+-------------+-----------------------------+--------------------------------------+-----------------+----------+
| 11111111-2222-3333-4444-555555555555 | Test Order | 2022-12-03T07:24:50.103974Z | AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE | Test Order Item | 1 |
| 11111111-2222-3333-4444-555555555555 | Test Order | 2022-12-03T07:24:50.103974Z | aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee | Test Order Item | 3 |
+--------------------------------------+-------------+-----------------------------+--------------------------------------+-----------------+----------+
2 rows in set (832.667us)
おわりに
以上、簡単に Spanner を Spring Boot から実際に使うまでを紹介しました。
なかなか日本語ドキュメントが無い & 公式ドキュメントが 🤪😇🤬🥱 って感じなので調べ甲斐(?)がありました。
参考になれば幸いです。
ref
- Cloud Spanner 公式
- Spring Data Cloud Spanner (docs.spring.io 版)
- Spring Data Cloud Spanner (cloud.spring.io 版)
- Google Cloud Advocate の入門記事
- @saturnism 氏による Community Documentation
-
公式には A no-compromise relational database service と強気に書かれています ↩︎
-
この DB 管理自体も Kotlin のツールを使っても良かったんですが、一旦 Web App としての振る舞いとして必要な記述に集中することにしました ↩︎
-
ちなみに
spring-cloud-gcp-data-spanner
という artifact もありますが、starter の方だと GCP 周りで必要な依存をすべて入れてくれるので特別な理由がなければ starter で良いです ↩︎ -
パフォーマンスを考える必要があるケースでは、 JOIN を使わずに SELECT AS STRUCT を使ったサブクエリを利用するほうが良いです ↩︎
Discussion