Spanner REST API で SQL を実行する方法: 基礎編
この記事は Spanner Advent Calendar 2日目の記事 & apstndb Advent Calendar 4日目の記事です。
この記事では Spanner REST API を使い SQL を実行する方法を説明します。
TL;DR
- Spanner は REST API を持つため curl のような HTTP クライアントで操作できる
- DDL, DML, クエリの基本的な実行例を紹介
- API レベルでの挙動が分かれば Spanner がもっと理解できるかも
導入
Spanner の使用方法はユーザにわかりやすいものでは
gcloud spanner
コマンドを使用する方法- 各言語の Spanner クライアントライブラリを使用する方法
- 公式の Web UI である Spanner Studio を使用する方法
がありますが、どれも REST API もしくは gRPC API を使って実装された抽象です。
これらを使っても API がどう動くのかは隠蔽されていて理解が進まない場合があります。分散トレースなどをする上でも API がどう働くかの理解は必須です。
公式ドキュメントには Getting started with Spanner using REST として REST API を API リファレンスの Try this method 機能を使って叩く例はあります。
しかし、コマンドラインでの curl 等での実行例がないとイメージが付かない人も居るのではないでしょうか。この記事では Spanner の API の基礎をコマンドラインを使って直接 Spanner REST API を叩いて説明します。
なおこの記事の例では 俺の考えた最強の curl ラッパーを考えてみる(jqurl) で解説した curl ラッパー jqurl を使用します。
jq(gojq) と curl の組み合わせのラッパーでしかないので適宜読み替えてください。
REST API を叩く
この記事では Spanner の SQL 実行に絞って説明していきます。
Spanner の SQL は主に下記の3つに分けて良いでしょう。
- スキーマ変更を行う DDL(Data Definition Language)
- データの更新を行う DML(Data Manipulation Language)
- データを取得するためのクエリ
なおこの記事では GCPUG Public Spanner 下のデータベースを使うため、特にプロジェクト名、インスタンス名、データベース名は隠していません。
DDL の実行
既に作成されたデータベースのスキーマ変更を行うために DDL を実行するには、projects.instances.databases.updateDdl
メソッドを使います。
この API メソッドは REST では次の HTTP メソッドと URL にマッピングされています。
PATCH https://spanner.googleapis.com/v1/{database=projects/*/instances/*/databases/*}/ddl
まだ、リクエストボディは下記のように説明されており、実行する DDL 文を入れる statements
のみが必須であることがわかります。
{
"statements": [
string
],
"operationId": string,
"protoDescriptors": string
}
よって、次のように実行できます。
$ jqurl -s -X PATCH --auth=google --yaml-output --data-jq '.statements = ["CREATE TABLE ExampleTable(PK INT64, Col INT64) PRIMARY KEY(PK)"]' \
"https://spanner.googleapis.com/v1/projects/${SPANNER_PROJECT}/instances/${SPANNER_INSTANCE}/databases/${SPANNER_DATABASE}/ddl"
metadata:
'@type': type.googleapis.com/google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata
actions:
- action: CREATE
entityNames:
- ExampleTable
entityType: TABLE
database: projects/gcpug-public-spanner/instances/merpay-sponsored-instance/databases/apstndb-sampledb3
progress:
- startTime: "2024-12-02T16:53:52.869590Z"
statements:
- |-
CREATE TABLE ExampleTable (
PK INT64,
Col INT64,
) PRIMARY KEY(PK)
name: projects/gcpug-public-spanner/instances/merpay-sponsored-instance/databases/apstndb-sampledb3/operations/_auto_op_7d0282940259b65c
これで DDL の実行がはじまります。拍子抜けするほど簡単ではないでしょうか。
実際には REST API レスポンスが返ってきても Long-Running Operation として DDL は実行途中であるため、実行結果は projects.instances.databases.operations.get
などで確認する必要があります。
GET https://spanner.googleapis.com/v1/{name=projects/*/instances/*/databases/*/operations/*}
先ほどの updateDdl
メソッドの実行結果に含まれる name
フィールドを見れば指定すべき name
がわかります。
$ jqurl -s --auth=google --yaml-output "https://spanner.googleapis.com/v1/projects/gcpug-public-spanner/instances/merpay-sponsored-instance/databases/apstndb-sampledb3/operations/_auto_op_7d0282940259b65c"
done: true
metadata:
'@type': type.googleapis.com/google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata
actions:
- action: CREATE
entityNames:
- ExampleTable
entityType: TABLE
commitTimestamps:
- "2024-12-02T16:53:57.578042Z"
database: projects/gcpug-public-spanner/instances/merpay-sponsored-instance/databases/apstndb-sampledb3
progress:
- endTime: "2024-12-02T16:53:57.578042Z"
progressPercent: 100
startTime: "2024-12-02T16:53:52.869590Z"
statements:
- |-
CREATE TABLE ExampleTable (
PK INT64,
Col INT64,
) PRIMARY KEY(PK)
name: projects/gcpug-public-spanner/instances/merpay-sponsored-instance/databases/apstndb-sampledb3/operations/_auto_op_7d0282940259b65c
response:
'@type': type.googleapis.com/google.protobuf.Empty
エラーなく成功していることが分かりますね。
gcloud spanner databases ddl update
コマンドなどの裏ではこの2つの API が実行されています。
DML の実行
DML や後述するクエリのように、データベース内のデータを扱う操作は同じコンテキストを持った一連の API 呼び出しが必要なため、それらを識別するためにセッションの概念が登場します。
この記事では最も基本的な流れを説明するためセッションの概念については詳しく触れません。詳しくは公式ドキュメントを参照することをおすすめします。
セッションの生成は projects.instances.databases.sessions.create
メソッドで行うことができます。
POST https://spanner.googleapis.com/v1/{database=projects/*/instances/*/databases/*}/sessions
この API のリクエストボディは次のように説明されています。
{
"session": {
object (Session) # Required. The session to create.
}
}
Session オブジェクトは次のような定義となっています
{
"name": string,
"labels": {
string: string,
...
},
"createTime": string,
"approximateLastUseTime": string,
"creatorRole": string,
"multiplexed": boolean
}
たくさんのフィールドがあるように見えますが、よく読むと全て出力のみか、必須ではないフィールドです。よって session
フィールドは必須ですが、その値は空オブジェクトで構いません。
なお、レスポンスも Session
オブジェクトとなります。 name
フィールドに入った値が今後使うセッションリソースを識別するものとなります。
jqurl は2番目の位置パラメータを jq フィルタとして扱うため、 --raw-output
と組み合わせることで目当ての名前をシェル変数に入れることができます。
$ session=$(jqurl -s --auth=google --data-jq '.session = {}' --raw-output \
"https://spanner.googleapis.com/v1/projects/${SPANNER_PROJECT}/instances/${SPANNER_INSTANCE}/databases/${SPANNER_DATABASE}/sessions" .name)
$ echo ${session}
projects/gcpug-public-spanner/instances/merpay-sponsored-instance/databases/apstndb-sampledb3/sessions/AL40lrGSvkoR2NLlp5__kZRx23C9XNkarGpgSwlL35USVZ3E4UT3bqoVzhLcJw
このセッションを使うことで DML の実行が可能です。
DML を実行する手段は次のようなものがあります。
- Method: projects.instances.databases.sessions.executeBatchDml
- Method: projects.instances.databases.sessions.executeSql
- Method: projects.instances.databases.sessions.executeStreamingSql
executeStreamingSql
サーバ側からストリームで分割された結果が返ってくるため curl では扱いづらいためこの記事では扱いません。
また、今回は THEN RETURN
も使って結果を取得してみたいため、結果を取得できない executeBatchDml
ではなく executeSql
を使います。
executeSql
のリクエストは複雑なので要点だけ説明します。
多くの場合、 DML の実行にはあらかじめ用意した Read/Write トランザクションを使用しますが、 Spanner では API 実行と同時に Read/Write トランザクションを開始することができます。 この機能を使うには transaction.begin.readWrite
に空オブジェクトを指定します。
また、 SQL 本文は sql
フィールドに指定します。
結果として、 DML の実行は先ほど作成したセッションを使用した下記のようなコマンドになります。
$ jqurl -s --auth=google --yaml-output \
--data-jq '.transaction.begin.readWrite = {} | .sql = "INSERT ExampleTable(PK, Col) VALUES(3, 42) THEN RETURN *"' \
"https://spanner.googleapis.com/v1/${session}:executeSql"
metadata:
rowType:
fields:
- name: PK
type:
code: INT64
- name: Col
type:
code: INT64
transaction:
id: QUw0MGxyR3VHeWtZcjgwMFVOQ3lYLWpSOXkweklYZ2F3THN0eFFqRncxcjhVZFFaVmc=
undeclaredParameters: {}
rows:
- - "3"
- "42"
stats:
rowCountExact: "1"
-
metadata.transaction.id
の Read/Write transaction の ID が入っている -
THEN RETURN
の結果返ってくる行の方がmetadata.rowType
に入っている -
rows
に今回 INSERT した行の値が入っている
などに注目してください。
なおこのままでは Read/Write トランザクションのコミットが行われないため、下記の通りトランザクションは10秒でアボートされロールバックされます。
A transaction is considered idle if it has no outstanding reads or SQL queries and has not started a read or SQL query within the last 10 seconds.
更新を確定するには10秒以内にコミットを行う必要があります。
コミットを行うには projects.instances.databases.sessions.commit
メソッドを呼びます。
DML に使った Read/Write トランザクションをコミットする場合 transactionId
が必須です。先ほどの executeSql
で返ってきた metadata.transaction.id
の値を使いましょう。(変数にとっていないのでコピペします。)
$ jqurl -s --auth=google --yaml-output --data-jq '.transactionId = "QUw0MGxyR3ZhM2FYdXBFUjNMaC13TDYyOWdjMzllNDNKbWRIMW1CTkdyTzBlUTZIYWc="' "https://spanner.googleapis.com/v1/${session}:commit"
commitTimestamp: "2024-12-02T17:55:20.756594Z"
コミットタイムスタンプが返ってきたので、このタイムスタンプで更新が確定したことになります。
クエリの実行
クエリの実行は先ほどの DML の実行とほぼ同じです。最大の違いは Read/Write トランザクションが不要で Read-Only トランザクションを使うことができることです。
Read-Only トランザクションは明示的に扱うこともできますが、 Single-Use トランザクションとして暗黙に扱うことができます。
$ jqurl -s --auth=google --yaml-output \
--data-jq '.transaction.singleUse.readOnly.strong = true | .sql = "SELECT * FROM ExampleTable"' \
"https://spanner.googleapis.com/v1/${session}:executeSql"
metadata:
rowType:
fields:
- name: PK
type:
code: INT64
- name: Col
type:
code: INT64
transaction: {}
undeclaredParameters: {}
rows:
- - "3"
- "42"
Single-Use トランザクションはその読み取りにのみ使い、他の操作には使わないためトランザクション ID は存在しません。これでクエリの実行は完了です。
まとめ
この記事では Spanner における SQL の実行方法の3種類について、もっとも基本的な形を説明しました。
- DDL
- DML
- クエリ
より複雑な形はこの基本的な形に対する差分として理解できます。
「まあ curl あれば Spanner の SQL は実行できるしな…」という気持ちを持てば Spanner を API レベルで理解するのは遠くはないでしょう。
Discussion