Google Cloud Spanner はフルマネージドで水平スケーリング可能なグローバルに分散されたリレーショナル データベース サービスであり、パフォーマンスと高可用性を損なうことなく ACID トランザクションと SQL セマンティクスを提供します。
このラボでは、Cloud Spanner インスタンスの設定方法について学びます。ゲーム リーダーボードに使用できるデータベースとスキーマを作成する手順について説明します。まず、プレーヤー情報を格納する Players テーブルと、プレーヤーのスコアを格納する Scores テーブルを作成します。
次に、テーブルにサンプルデータを入力します。続いて、トップ 10 のサンプルクエリをいくつか実行し、最後にインスタンスを削除してリソースを解放しラボを終了します。
ラボの内容
- Cloud Spanner インスタンスの設定方法。
- データベースとテーブルの作成方法。
- commit タイムスタンプ列の使用方法。
- タイムスタンプを含む Cloud Spanner データベース テーブルにデータを読み込む方法。
- Cloud Spanner データベースにクエリを実行する方法。
- Cloud Spanner インスタンスを削除する方法。
必要なもの
このチュートリアルをどのように使用されますか?
Google Cloud Platform のご利用経験について、いずれに該当されますか?
セルフペース型の環境設定
Google アカウント(Gmail または Google Apps)をお持ちでない場合は、1 つ作成する必要があります。Google Cloud Platform のコンソール(console.cloud.google.com)にログインし、新しいプロジェクトを作成します。
すでにプロジェクトが存在する場合は、コンソールの左上にあるプロジェクト選択プルダウン メニューをクリックします。
表示されるダイアログで [新しいプロジェクト] ボタンをクリックして新しいプロジェクトを作成します。
まだプロジェクトが存在しない場合は、次のような最初のプロジェクトを作成するためのダイアログが表示されます。
続いて表示されるプロジェクト作成ダイアログでは、新しいプロジェクトの詳細を入力できます。
プロジェクト ID を忘れないようにしてください。プロジェクト ID は、すべての Google Cloud プロジェクトを通じて一意の名前にする必要があります(上記の名前はすでに使用されているため使用できません)。以降、この Codelab では PROJECT_ID
と表します。
次に、Google Cloud リソースを使用し、Cloud Spanner API を有効にするために、Developers Console で課金を有効にする必要があります。
この Codelab の手順をすべて実行した際の費用は数百円程度ですが、さらにリソースを使用する場合、または実行状態で保持する場合は、追加の費用が発生する可能性があります(このドキュメントの最後にある「クリーンアップ」セクションをご覧ください)。Google Cloud Spanner の料金については、こちらをご覧ください。
Google Cloud Platform の新規ユーザーの皆さんには、$300 の無料トライアルをご利用いただけます。その場合は、この Codelab を完全に無料でご利用いただけます。
Google Cloud Shell の設定
Google Cloud と Spanner はノートパソコンからリモートで操作でき、この Codelab では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。
この Debian ベースの仮想マシンには、必要な開発ツールがすべて用意されています。5 GB の永続ホーム ディレクトリが用意されており、Google Cloud で稼働し、ネットワークのパフォーマンスと認証が大幅に強化されています。つまり、この Codelab で必要となるのはブラウザのみです(Chromebook でも動作します)。
- Cloud Console から Cloud Shell を有効にするには、[Cloud Shell をアクティブにする] をクリックします(環境のプロビジョニングと接続に若干時間を要します)。
Cloud Shell に接続すると、すでに認証は完了しており、プロジェクトに各自の PROJECT_ID
が設定されていることがわかります。
gcloud auth list
コマンド出力
Credentialed accounts: - <myaccount>@<mydomain>.com (active)
gcloud config list project
コマンド出力
[core] project = <PROJECT_ID>
なんらかの理由でプロジェクトが設定されていない場合は、次のコマンドを実行します。
gcloud config set project <PROJECT_ID>
PROJECT_ID
が見つからない場合は、セットアップ手順で使用した ID を確認するか、Cloud Console ダッシュボードで ID を調べます。
Cloud Shell では、デフォルトで環境変数もいくつか設定されます。これらの変数は、以降のコマンドを実行する際に有用なものです。
echo $GOOGLE_CLOUD_PROJECT
コマンド出力
<PROJECT_ID>
- 最後に、デフォルトのゾーンとプロジェクト構成を設定します。
gcloud config set compute/zone us-central1-f
さまざまなゾーンを選択できます。詳細については、リージョンとゾーンをご覧ください。
概要
このステップでは、環境を設定します。
次の設定
次に、Cloud Spanner インスタンスを設定します。
このステップでは、この Codelab 用に Cloud Spanner インスタンスを設定します。左上のハンバーガー メニュー で Spanner エントリ を検索するか、「/」を押して Spanner と入力し「Spanner」を検索します。
次に、 をクリックしてインスタンスのインスタンス名 cloudspanner-leaderboard を入力し、構成を選択(リージョン インスタンスを選択)してノード数を設定しフォームに入力します。この Codelab で必要となるノードは 1 つのみです。本番環境インスタンスの場合に Cloud Spanner SLA の対象となるには、Cloud Spanner インスタンスで 3 つ以上のノードを実行する必要があります。
最後に重要な点として [作成] をクリックしてください。数秒以内に必要な Cloud Spanner インスタンスが作成されます。
次のステップでは、Java クライアント ライブラリを使用して新しいインスタンスにデータベースとスキーマを作成します。
このステップでは、サンプルのデータベースとスキーマを作成します。
Java クライアント ライブラリを使用して、プレーヤー情報についての Players テーブルと、プレーヤー スコアを格納する Scores テーブルの 2 つのテーブルを作成してみましょう。これを行うために、Cloud Shell で Java コンソール アプリケーションを作成する手順について説明します。
まず、Cloud Shell で次のコマンドを入力して、GitHub から取得したこの Codelab 用のサンプルコードのクローンを作成します。
git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git
次に、アプリケーションを作成する「applications」ディレクトリに移動します。
cd java-docs-samples/spanner/leaderboard
この Codelab に必要なすべてのコードは、既存の java-docs-samples/spanner/leaderboard/complete
ディレクトリ(Leaderboard
という実行可能な C# アプリケーション)に格納されており、この Codelab を進める際の参考になります。新しいディレクトリを作成し、リーダーボード アプリケーションのコピーを段階的にビルドします。
アプリケーション用に「codelab」という新しいディレクトリを作成し、次のコマンドを使用して作成したディレクトリに移動します。
mkdir codelab && cd $_
次の Maven(mvn)コマンドを使用して、「Leaderboard」という名前の新しい基本的な Java アプリケーションを作成します。
mvn -B archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.google.codelabs -DartifactId=leaderboard -DarchetypeVersion=1.4
このコマンドは、Maven アプリ構成ファイル pom.xml
と Java アプリファイル App.java
の 2 つのプライマリ ファイルで構成されるシンプルなコンソール アプリケーションを作成します。
次に、先ほど作成したリーダーボードのディレクトリに移動し、コンテンツを一覧表示します。
cd leaderboard && ls
pom.xml
ファイルと src
ディレクトリが表示されます。
pom.xml src
ここで、App.java
を編集して、Java Spanner クライアント ライブラリを使用して Players と Scores の 2 つのテーブルで構成されるリーダーボードを作成するように、このコンソール アプリをアップデートしましょう。この操作は、Cloud Shell エディタで行うことができます。
以下のハイライト表示されたアイコンをクリックして、Cloud Shell エディタを開きます。
リーダーボード フォルダの下にある pom.xml を開きます。java-docs-samples\ spanner\leaderboard\codelab\leaderboard
フォルダにある pom.xml ファイルを開きます。このファイルでは、すべての依存関係を含む jar でアプリケーションがビルドされるように Maven ビルドシステムを構成します。
次の 1 つの新しい依存関係の管理セクションを既存の </properties> 要素の直下に追加します。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-bom</artifactId>
<version>0.83.0-alpha</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
さらに、既存の <dependencies> セクションに 1 つの新しい依存関係を追加し、Cloud Spanner Java クライアント ライブラリがアプリケーションに追加されるようにします。
<dependency>
<!-- Version auto-managed by BOM -->
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-spanner</artifactId>
</dependency>
次に、pom.xml
ファイルの既存の <build> セクションを、次の <build> セクションに置き換えます。
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<configuration>
<finalName>leaderboard</finalName>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.google.codelabs.App</mainClass>
</manifest>
</archive>
<appendAssemblyId>false</appendAssemblyId>
<attach>false</attach>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0-M3</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
</plugins>
</build>
pom.xml ファイルに加えた変更を保存するには、Cloud Shell エディタの [ファイル] メニューで [保存] を選択するか、キーボードの Ctrl キーと S キーを同時に押します。
次に、src/main/java/com/google/codelabs/
フォルダにある Cloud Shell エディタで App.java
ファイルを開きます。次の Java コードを App.java
ファイルに貼り付けて、ファイルの既存のコードを leaderboard
データベースと Players
テーブル、Scores
テーブルの作成に必要なコードに置き換えます。
package com.google.codelabs;
import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
/**
* Example code for using the Cloud Spanner API with the Google Cloud Java client library
* to create a simple leaderboard.
*
* This example demonstrates:
*
* <p>
*
* <ul>
* <li>Creating a Cloud Spanner database.
* </ul>
*/
public class App {
static void create(DatabaseAdminClient dbAdminClient, DatabaseId db) {
OperationFuture<Database, CreateDatabaseMetadata> op =
dbAdminClient.createDatabase(
db.getInstanceId().getInstance(),
db.getDatabase(),
Arrays.asList(
"CREATE TABLE Players(\n"
+ " PlayerId INT64 NOT NULL,\n"
+ " PlayerName STRING(2048) NOT NULL\n"
+ ") PRIMARY KEY(PlayerId)",
"CREATE TABLE Scores(\n"
+ " PlayerId INT64 NOT NULL,\n"
+ " Score INT64 NOT NULL,\n"
+ " Timestamp TIMESTAMP NOT NULL\n"
+ " OPTIONS(allow_commit_timestamp=true)\n"
+ ") PRIMARY KEY(PlayerId, Timestamp),\n"
+ "INTERLEAVE IN PARENT Players ON DELETE NO ACTION"));
try {
// Initiate the request which returns an OperationFuture.
Database dbOperation = op.get();
System.out.println("Created database [" + dbOperation.getId() + "]");
} catch (ExecutionException e) {
// If the operation failed during execution, expose the cause.
throw (SpannerException) e.getCause();
} catch (InterruptedException e) {
// Throw when a thread is waiting, sleeping, or otherwise occupied,
// and the thread is interrupted, either before or during the activity.
throw SpannerExceptionFactory.propagateInterrupt(e);
}
}
static void printUsageAndExit() {
System.out.println("Leaderboard 1.0.0");
System.out.println("Usage:");
System.out.println(" java -jar leaderboard.jar "
+ "<command> <instance_id> <database_id> [command_option]");
System.out.println("");
System.out.println("Examples:");
System.out.println(" java -jar leaderboard.jar create my-instance example-db");
System.out.println(" - Create a sample Cloud Spanner database along with "
+ "sample tables in your project.\n");
System.exit(1);
}
public static void main(String[] args) throws Exception {
if (!(args.length == 3 || args.length == 4)) {
printUsageAndExit();
}
SpannerOptions options = SpannerOptions.newBuilder().build();
Spanner spanner = options.getService();
try {
String command = args[0];
DatabaseId db = DatabaseId.of(options.getProjectId(), args[1], args[2]);
DatabaseClient dbClient = spanner.getDatabaseClient(db);
DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient();
switch (command) {
case "create":
create(dbAdminClient, db);
break;
default:
printUsageAndExit();
}
} finally {
spanner.close();
}
System.out.println("Closed client");
}
}
Cloud Shell エディタの [ファイル] メニューで [保存] を選択して、App.java
ファイルに加えた変更を保存します。
java-docs-samples/spanner/leaderboard/step4/src
ディレクトリの App.java
ファイルを使用して、create
コマンドを有効にするコードを追加した後の App.java
ファイルの表示例を確認できます。
アプリをビルドするには、pom.xml
があるディレクトリから mvn パッケージを実行します。
mvn package
Java jar ファイルが正常にビルドされたら、次のコマンドを入力して、Cloud Shell で生成されたアプリケーションを実行します。
java -jar target/leaderboard.jar
次のような出力が表示されます。
Leaderboard 1.0.0 Usage: java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option] Examples: java -jar leaderboard.jar create my-instance example-db - Create a sample Cloud Spanner database along with sample tables in your project.
このレスポンスから、これが Leaderboard
アプリケーションであることが確認できます。このアプリケーションには現在、コマンドが 1 つ(create
)設定されています。create
コマンドに想定される引数は、インスタンス ID とデータベース ID であることが確認できます。
そこで、次のコマンドを実行します。
java -jar target/leaderboard.jar create cloudspanner-leaderboard leaderboard
数秒後、次のようなレスポンスが表示されます。
Created database [projects/your-project/instances/cloudspanner-leaderboard/databases/leaderboard]
Cloud Console の [Cloud Spanner] セクションで、左側のメニューに新しいデータベースとテーブルが表示されます。
次のステップでは、新しいデータベースにデータを読み込むようにアプリケーションを更新します。
これで、Players
と Scores
の 2 つのテーブルを含む leaderboard
という名前のデータベースが作成されました。次に、Java クライアント ライブラリを使用して Players
テーブルにプレーヤーを、Scores
テーブルに各プレーヤーのランダムなスコアを入力してみましょう。
まだ開いていない場合は、以下でハイライト表示されているアイコンをクリックして Cloud Shell エディタを開きます。
次に、Cloud Shell エディタで App.java
ファイルを編集して、insert
コマンドを追加します。このコマンドを使用すると、100 人のプレーヤーを Players
テーブルに挿入する、または Players
テーブルの各プレーヤーの Scores
テーブルに 4 つのランダムなスコアを挿入することができます。
まず、アプリファイルの上部にある imports
セクションを更新して現在の内容を置き換えます。この操作を完了すると、次のようになります。
package com.google.codelabs;
import static com.google.cloud.spanner.TransactionRunner.TransactionCallable;
import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TransactionContext;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
次に、既存の create()
メソッドの下と、既存の printUsageAndExit()
メソッドの上に、以下の insert、insertPlayers、insertScores の各メソッドを追加します。
static void insert(DatabaseClient dbClient, String insertType) {
try {
insertType = insertType.toLowerCase();
} catch (Exception e) {
// Invalid input received, set insertType to empty string.
insertType = "";
}
if (insertType.equals("players")) {
// Insert players.
insertPlayers(dbClient);
} else if (insertType.equals("scores")) {
// Insert scores.
insertScores(dbClient);
} else {
// Invalid input.
System.out.println("Invalid value for 'type of insert'. "
+ "Specify a valid value: 'players' or 'scores'.");
System.exit(1);
}
}
static void insertPlayers(DatabaseClient dbClient) {
dbClient
.readWriteTransaction()
.run(
new TransactionCallable<Void>() {
@Override
public Void run(TransactionContext transaction) throws Exception {
// Get the number of players.
String sql = "SELECT Count(PlayerId) as PlayerCount FROM Players";
ResultSet resultSet = transaction.executeQuery(Statement.of(sql));
long numberOfPlayers = 0;
if (resultSet.next()) {
numberOfPlayers = resultSet.getLong("PlayerCount");
}
// Insert 100 player records into the Players table.
List<Statement> stmts = new ArrayList<Statement>();
long randomId;
for (int x = 1; x <= 100; x++) {
numberOfPlayers++;
randomId = (long) Math.floor(Math.random() * 9_000_000_000L) + 1_000_000_000L;
Statement statement =
Statement
.newBuilder(
"INSERT INTO Players (PlayerId, PlayerName) "
+ "VALUES (@PlayerId, @PlayerName) ")
.bind("PlayerId")
.to(randomId)
.bind("PlayerName")
.to("Player " + numberOfPlayers)
.build();
stmts.add(statement);
}
transaction.batchUpdate(stmts);
return null;
}
});
System.out.println("Done inserting player records...");
}
static void insertScores(DatabaseClient dbClient) {
boolean playerRecordsFound = false;
ResultSet resultSet =
dbClient
.singleUse()
.executeQuery(Statement.of("SELECT * FROM Players"));
while (resultSet.next()) {
playerRecordsFound = true;
final long playerId = resultSet.getLong("PlayerId");
dbClient
.readWriteTransaction()
.run(
new TransactionCallable<Void>() {
@Override
public Void run(TransactionContext transaction) throws Exception {
// Initialize objects for random Score and random Timestamp.
LocalDate endDate = LocalDate.now();
long end = endDate.toEpochDay();
int startYear = endDate.getYear() - 2;
int startMonth = endDate.getMonthValue();
int startDay = endDate.getDayOfMonth();
LocalDate startDate = LocalDate.of(startYear, startMonth, startDay);
long start = startDate.toEpochDay();
Random r = new Random();
List<Statement> stmts = new ArrayList<Statement>();
// Insert 4 score records into the Scores table
// for each player in the Players table.
for (int x = 1; x <= 4; x++) {
// Generate random score between 1,000,000 and 1,000
long randomScore = r.nextInt(1000000 - 1000) + 1000;
// Get random day within the past two years.
long randomDay = ThreadLocalRandom.current().nextLong(start, end);
LocalDate randomDayDate = LocalDate.ofEpochDay(randomDay);
LocalTime randomTime = LocalTime.of(
r.nextInt(23), r.nextInt(59), r.nextInt(59), r.nextInt(9999));
LocalDateTime randomDate = LocalDateTime.of(randomDayDate, randomTime);
Instant randomInstant = randomDate.toInstant(ZoneOffset.UTC);
Statement statement =
Statement
.newBuilder(
"INSERT INTO Scores (PlayerId, Score, Timestamp) "
+ "VALUES (@PlayerId, @Score, @Timestamp) ")
.bind("PlayerId")
.to(playerId)
.bind("Score")
.to(randomScore)
.bind("Timestamp")
.to(randomInstant.toString())
.build();
stmts.add(statement);
}
transaction.batchUpdate(stmts);
return null;
}
});
}
if (!playerRecordsFound) {
System.out.println("Parameter 'scores' is invalid since "
+ "no player records currently exist. First insert players "
+ "then insert scores.");
System.exit(1);
} else {
System.out.println("Done inserting score records...");
}
}
続いて、insert
コマンドが機能するようにするには、switch (command)
ステートメント内のアプリの「main」メソッドに次のコードを追加します。
case "insert":
String insertType;
try {
insertType = args[3];
} catch (ArrayIndexOutOfBoundsException exception) {
insertType = "";
}
insert(dbClient, insertType);
break;
完了すると、switch (command)
ステートメントは次のようになります。
switch (command) {
case "create":
create(dbAdminClient, db);
break;
case "insert":
String insertType;
try {
insertType = args[3];
} catch (ArrayIndexOutOfBoundsException exception) {
insertType = "";
}
insert(dbClient, insertType);
break;
default:
printUsageAndExit();
}
アプリに「挿入」機能を追加するための最後のステップでは、printUsageAndExit()
メソッドに「insert」コマンドのヘルプテキストを追加します。次のコード行を printUsageAndExit()
メソッドに追加して、insert コマンドのヘルプテキストを含めます。
System.out.println(" java -jar leaderboard.jar insert my-instance example-db players");
System.out.println(" - Insert 100 sample Player records into the database.\n");
System.out.println(" java -jar leaderboard.jar insert my-instance example-db scores");
System.out.println(" - Insert sample score data into Scores sample Cloud Spanner "
+ "database table.\n");
Cloud Shell エディタの [ファイル] メニューで [保存] を選択して、App.java
ファイルに加えた変更を保存します。
java-docs-samples/spanner/leaderboard/step5/src
ディレクトリの App.java
ファイルを使用して、insert
コマンドを有効にするコードを追加した後の App.java
ファイルの表示例を確認できます。
アプリを再ビルドして実行し、新しい insert
コマンドがアプリの使用可能なコマンドのリストに含まれていることを確認しましょう。
アプリをビルドするには、pom.xml があるディレクトリから mvn package
を実行します。
mvn package
Java jar ファイルが正常にビルドされたら、次のコマンドを実行します。
java -jar target/leaderboard.jar
アプリのデフォルトの出力に insert
コマンドが追加されたことを確認できます。
Leaderboard 1.0.0 Usage: java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option] Examples: java -jar leaderboard.jar create my-instance example-db - Create a sample Cloud Spanner database along with sample tables in your project. java -jar leaderboard.jar insert my-instance example-db players - Insert 100 sample Player records into the database. java -jar leaderboard.jar insert my-instance example-db scores - Insert sample score data into Scores sample Cloud Spanner database table.
レスポンスを見ると、インスタンス ID とデータベース ID に加えて、値が「プレーヤー」または「スコア」の値を持つ別の引数が存在することが確認できます。
次に、create
コマンドを呼び出した際に使用したのと同じ引数の値を使用して insert
コマンドを実行し、追加の「挿入の種類」として「players」を追加します。
java -jar target/leaderboard.jar insert cloudspanner-leaderboard leaderboard players
数秒後、次のようなレスポンスが表示されます。
Done inserting player records...
次に、Java クライアント ライブラリを使用して Scores
テーブルに、Players
テーブルの各プレーヤーのタイムスタンプとともに、ランダムなスコアを 4 つ入力します。
Scores
テーブルの Timestamp
列は、以前に create
コマンドを実行したときに実行された次の SQL ステートメントを使用して「commit タイムスタンプ」列として定義されました。
CREATE TABLE Scores(
PlayerId INT64 NOT NULL,
Score INT64 NOT NULL,
Timestamp TIMESTAMP NOT NULL OPTIONS(allow_commit_timestamp=true)
) PRIMARY KEY(PlayerId, Timestamp),
INTERLEAVE IN PARENT Players ON DELETE NO ACTION
OPTIONS(allow_commit_timestamp=true)
属性に注意してください。これにより Timestamp
が「commit タイムスタンプ」列になり、特定のテーブル行で INSERT オペレーションと UPDATE オペレーションの正確なトランザクション タイムスタンプが自動的に入力されます。
また、独自のタイムスタンプ値を「commit タイムスタンプ」列に挿入することもできます。この場合は、過去の値を持つタイムスタンプを挿入する必要があり、この Codelab でこの処理を行います。
次に、create
コマンドを呼び出した際に使用したのと同じ引数の値を使用して insert
コマンドを実行し、追加の「挿入の種類」として「scores」を追加します。
java -jar target/leaderboard.jar insert cloudspanner-leaderboard leaderboard scores
数秒後、次のようなレスポンスが表示されます。
Done inserting score records...
scores
として指定された「挿入の種類」を指定して insert
を実行すると、insertScores
メソッドが呼び出されます。このメソッドは、次のコード スニペットを使用してランダムに生成されたタイムスタンプを、過去の日時で挿入します。
LocalDate endDate = LocalDate.now();
long end = endDate.toEpochDay();
int startYear = endDate.getYear() - 2;
int startMonth = endDate.getMonthValue();
int startDay = endDate.getDayOfMonth();
LocalDate startDate = LocalDate.of(startYear, startMonth, startDay);
long start = startDate.toEpochDay();
...
long randomDay = ThreadLocalRandom.current().nextLong(start, end);
LocalDate randomDayDate = LocalDate.ofEpochDay(randomDay);
LocalTime randomTime = LocalTime.of(
r.nextInt(23), r.nextInt(59), r.nextInt(59), r.nextInt(9999));
LocalDateTime randomDate = LocalDateTime.of(randomDayDate, randomTime);
Instant randomInstant = randomDate.toInstant(ZoneOffset.UTC);
...
.bind("Timestamp")
.to(randomInstant.toString())
「Insert」トランザクションが行われたときの正確なタイムスタンプを Timestamp
列を自動入力するには、代わりに次のコード スニペットのように Java 定数 Value.COMMIT_TIMESTAMP
を挿入します。
.bind("Timestamp")
.to(Value.COMMIT_TIMESTAMP)
これでデータの読み込みが完了したため、先ほど新しいテーブルに書き込んだ値を確認しましょう。まず、leaderboard
データベースを選択し、次に Players
テーブルを選択します。[Data
] タブをクリックします。テーブルの [PlayerId
] 列と [PlayerName
] 列にデータが存在することを確認できます。
次に、[Scores
] テーブルをクリックして [Data
] タブを選択し、Scores テーブルにもデータが存在することを確認しましょう。テーブルの PlayerId
、Timestamp
、Score
の各列にデータが存在することを確認できます。
投稿するとゲーム リーダーボードの作成に使用できるクエリを実行するように、アプリを更新しましょう。
データベースを設定して情報をテーブルに読み込んだところで、このデータを使用してリーダーボードを作成してみましょう。そのためには、次の 4 つの質問に回答する必要があります。
- 常時「トップ 10」入りしているのは、どのプレーヤーですか?
- どのプレーヤーが今年の「トップ 10」のプレーヤーですか?
- どのプレーヤーが今月のトップ 10 のプレーヤーですか?
- どのプレーヤーが今週の「トップ 10」のプレーヤーですか?
これらの質問に回答できる SQL クエリを実行するようにアプリを更新しましょう。
次に、query
コマンドを追加します。このコマンドには、リーダーボードに必要な情報を生成するための質問に回答するクエリを実行する方法が用意されていますす。
Cloud Shell エディタで App.java
ファイルを編集し、アプリを更新して query
コマンドを追加します。query
コマンドは 2 つの query
メソッドで構成されます。1 つは DatabaseClient
引数のみを取り、もう 1 つは追加の timespan
引数を取り時間で指定された期間を基準に結果をフィルタリングできるようにします。
既存の insertScores()
メソッドの下、および既存の printUsageAndExit()
の上に、次の 2 つの query
メソッドを追加します。
static void query(DatabaseClient dbClient) {
String scoreDate;
String score;
ResultSet resultSet =
dbClient
.singleUse()
.executeQuery(
Statement.of(
"SELECT p.PlayerId, p.PlayerName, s.Score, s.Timestamp "
+ "FROM Players p "
+ "JOIN Scores s ON p.PlayerId = s.PlayerId "
+ "ORDER BY s.Score DESC LIMIT 10"));
while (resultSet.next()) {
scoreDate = String.valueOf(resultSet.getTimestamp("Timestamp"));
score = String.format("%,d", resultSet.getLong("Score"));
System.out.printf(
"PlayerId: %d PlayerName: %s Score: %s Timestamp: %s\n",
resultSet.getLong("PlayerId"), resultSet.getString("PlayerName"), score,
scoreDate.substring(0,10));
}
}
static void query(DatabaseClient dbClient, int timespan) {
String scoreDate;
String score;
Statement statement =
Statement
.newBuilder(
"SELECT p.PlayerId, p.PlayerName, s.Score, s.Timestamp "
+ "FROM Players p "
+ "JOIN Scores s ON p.PlayerId = s.PlayerId "
+ "WHERE s.Timestamp > "
+ "TIMESTAMP_SUB(CURRENT_TIMESTAMP(), "
+ " INTERVAL @Timespan HOUR) "
+ "ORDER BY s.Score DESC LIMIT 10")
.bind("Timespan")
.to(timespan)
.build();
ResultSet resultSet =
dbClient
.singleUse()
.executeQuery(statement);
while (resultSet.next()) {
scoreDate = String.valueOf(resultSet.getTimestamp("Timestamp"));
score = String.format("%,d", resultSet.getLong("Score"));
System.out.printf(
"PlayerId: %d PlayerName: %s Score: %s Timestamp: %s\n",
resultSet.getLong("PlayerId"), resultSet.getString("PlayerName"), score,
scoreDate.substring(0,10));
}
}
次に、query
コマンドが機能するようにするには、アプリの「main」メソッド内の switch(command)
ステートメントに次のコードを追加します。
case "query":
if (args.length == 4) {
int timespan = 0;
try {
timespan = Integer.parseInt(args[3]);
} catch (NumberFormatException e) {
System.err.println("query command's 'timespan' parameter must be a valid integer.");
System.exit(1);
}
query(dbClient, timespan);
} else {
query(dbClient);
}
break;
アプリへの「クエリ」機能の追加を完了するための最後のステップでは、「query」コマンドのヘルプテキストを printUsageAndExit()
メソッドに追加します。次のコード行を printUsageAndExit()
メソッドに追加して、「query」コマンドのヘルプテキストを含めます。
System.out.println(" java -jar leaderboard.jar query my-instance example-db");
System.out.println(" - Query players with top ten scores of all time.\n");
System.out.println(" java -jar leaderboard.jar query my-instance example-db 168");
System.out.println(" - Query players with top ten scores within a timespan "
+ "specified in hours.\n");
Cloud Shell エディタの [ファイル] メニューで [保存] を選択して、App.java
ファイルに加えた変更を保存します。
dotnet-docs-samples/applications/leaderboard/step6/src
ディレクトリの App.java
ファイルを使用して、query
コマンドを有効にするコードを追加した後の App.java
ファイルの表示例を確認できます。
アプリをビルドするには、pom.xml があるディレクトリから mvn package
を実行します。
mvn package
アプリを実行し、新しい query
コマンドがアプリの使用可能なコマンドのリストに含まれていることを確認しましょう。次のコマンドを実行します。
java -jar target/leaderboard.jar
アプリのデフォルトの出力に新しいコマンド オプションとして、query
コマンドが追加されたことを確認できます。
Leaderboard 1.0.0 Usage: java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option] Examples: java -jar leaderboard.jar create my-instance example-db - Create a sample Cloud Spanner database along with sample tables in your project. java -jar leaderboard.jar insert my-instance example-db players - Insert 100 sample Player records into the database. java -jar leaderboard.jar insert my-instance example-db scores - Insert sample score data into Scores sample Cloud Spanner database table. java -jar leaderboard.jar query my-instance example-db - Query players with top ten scores of all time. java -jar leaderboard.jar query my-instance example-db 168 - Query players with top ten scores within a timespan specified in hours.
レスポンスから、インスタンス ID とデータベース ID の引数に加えて、query
コマンドを使用すると、Scores
テーブルの Timestamp
列の値に基づいてレコードのフィルタリングに使用する期間(省略可)を指定可能であることが確認できます。オプションの timespan 引数は、timespan 引数が含まれていない場合、タイムスタンプによるフィルタリングは行われないことを示しています。したがって、「期間」値を指定せずに query
コマンドを使用して、常時「トップ 10」のプレーヤーの一覧を取得できます。
create
コマンドを実行する際に使用したのと同じ引数値を使用して、「期間」を指定せずに query
コマンドを実行してみましょう。
java -jar target/leaderboard.jar query cloudspanner-leaderboard leaderboard
次のような常時「トップ 10」のプレーヤーを含むレスポンスが表示されます。
PlayerId: 4018687297 PlayerName: Player 83 Score: 999,618 Timestamp: 2017-07-01
PlayerId: 4018687297 PlayerName: Player 83 Score: 998,956 Timestamp: 2017-09-02
PlayerId: 4285713246 PlayerName: Player 51 Score: 998,648 Timestamp: 2017-12-01
PlayerId: 5267931774 PlayerName: Player 49 Score: 997,733 Timestamp: 2017-11-09
PlayerId: 1981654448 PlayerName: Player 35 Score: 997,480 Timestamp: 2018-12-06
PlayerId: 4953940705 PlayerName: Player 87 Score: 995,184 Timestamp: 2018-09-14
PlayerId: 2456736905 PlayerName: Player 84 Score: 992,881 Timestamp: 2017-04-14
PlayerId: 8234617611 PlayerName: Player 19 Score: 992,399 Timestamp: 2017-12-27
PlayerId: 1788051688 PlayerName: Player 76 Score: 992,265 Timestamp: 2018-11-22
PlayerId: 7127686505 PlayerName: Player 97 Score: 992,038 Timestamp: 2017-12-02
次に必要な引数を指定して query
コマンドを実行し、1 年の時間数 8,760 に相当する「期間」を指定することによって、年間の「トップ 10」プレーヤーをクエリしてみましょう。
java -jar target/leaderboard.jar query cloudspanner-leaderboard leaderboard 8760
次のような年間の「トップ 10」プレーヤーを含むレスポンスが表示されます。
PlayerId: 1981654448 PlayerName: Player 35 Score: 997,480 Timestamp: 2018-12-06
PlayerId: 4953940705 PlayerName: Player 87 Score: 995,184 Timestamp: 2018-09-14
PlayerId: 1788051688 PlayerName: Player 76 Score: 992,265 Timestamp: 2018-11-22
PlayerId: 6862349579 PlayerName: Player 30 Score: 990,877 Timestamp: 2018-09-14
PlayerId: 5529627211 PlayerName: Player 16 Score: 989,142 Timestamp: 2018-03-30
PlayerId: 9743904155 PlayerName: Player 1 Score: 988,765 Timestamp: 2018-05-30
PlayerId: 6809119884 PlayerName: Player 7 Score: 986,673 Timestamp: 2018-05-16
PlayerId: 2132710638 PlayerName: Player 54 Score: 983,108 Timestamp: 2018-09-11
PlayerId: 2320093590 PlayerName: Player 79 Score: 981,373 Timestamp: 2018-05-07
PlayerId: 9554181430 PlayerName: Player 80 Score: 981,087 Timestamp: 2018-06-21
次に必要な引数を指定して query
コマンドを実行し、1 か月の時間数 730 に相当する「期間」を指定することによって、月間の「トップ 10」プレーヤーをクエリしてみましょう。
java -jar target/leaderboard.jar query cloudspanner-leaderboard leaderboard 730
次のような月間の「トップ 10」プレーヤーを含むレスポンスが表示されます。
PlayerId: 3869829195 PlayerName: Player 69 Score: 949,686 Timestamp: 2019-02-19
PlayerId: 7448359883 PlayerName: Player 20 Score: 938,998 Timestamp: 2019-02-07
PlayerId: 1981654448 PlayerName: Player 35 Score: 929,003 Timestamp: 2019-02-22
PlayerId: 9336678658 PlayerName: Player 44 Score: 914,106 Timestamp: 2019-01-27
PlayerId: 6968576389 PlayerName: Player 40 Score: 898,041 Timestamp: 2019-02-21
PlayerId: 5529627211 PlayerName: Player 16 Score: 896,433 Timestamp: 2019-01-29
PlayerId: 9395039625 PlayerName: Player 59 Score: 879,495 Timestamp: 2019-02-09
PlayerId: 2094604854 PlayerName: Player 39 Score: 860,434 Timestamp: 2019-02-01
PlayerId: 9395039625 PlayerName: Player 59 Score: 849,955 Timestamp: 2019-02-21
PlayerId: 4285713246 PlayerName: Player 51 Score: 805,654 Timestamp: 2019-02-02
次に 1 週間の時間数 168 に相当する「期間」 を指定して query
コマンドを実行し、週の「トップ 10」プレーヤーをクエリしてみましょう。
java -jar target/leaderboard.jar query cloudspanner-leaderboard leaderboard 168
次のような週の「トップ 10」プレーヤーを含むレスポンスが表示されます。
PlayerId: 3869829195 PlayerName: Player 69 Score: 949,686 Timestamp: 2019-02-19
PlayerId: 1981654448 PlayerName: Player 35 Score: 929,003 Timestamp: 2019-02-22
PlayerId: 6968576389 PlayerName: Player 40 Score: 898,041 Timestamp: 2019-02-21
PlayerId: 9395039625 PlayerName: Player 59 Score: 849,955 Timestamp: 2019-02-21
PlayerId: 5954045812 PlayerName: Player 8 Score: 795,639 Timestamp: 2019-02-22
PlayerId: 3889939638 PlayerName: Player 71 Score: 775,252 Timestamp: 2019-02-21
PlayerId: 5529627211 PlayerName: Player 16 Score: 604,695 Timestamp: 2019-02-19
PlayerId: 9006728426 PlayerName: Player 3 Score: 457,208 Timestamp: 2019-02-22
PlayerId: 8289497066 PlayerName: Player 58 Score: 227,697 Timestamp: 2019-02-20
PlayerId: 8065482904 PlayerName: Player 99 Score: 198,429 Timestamp: 2019-02-24
これで作業はすべて終了です。
レコードを追加すると、Cloud Spanner はデータベースを必要な大きさにスケーリングします。データベースがどれほど増加しても、ゲームのリーダーボードは Cloud Spanner とその Truetime テクノロジーにより正確性を維持したままスケーリングできます。
Spanner のプレイを楽しんだ後は、プレイグラウンドをクリーンアップして貴重なリソースと予算を節約する必要があります。この手順は簡単なため、Cloud Console の Cloud Spanner セクションに移動して、「Cloud Spanner インスタンスを設定する」という名前のコードラボで作成したインスタンスを削除します。
学習した内容:
- リーダーボードの Google Cloud Spanner インスタンス、データベース、テーブル スキーマ
- Java コンソール アプリケーションを作成する方法
- Java クライアント ライブラリを使用して Spanner のデータベースとテーブルを作成する方法
- Java クライアント ライブラリを使用して Spanner データベースにデータを読み込む方法
- Spanner の commit タイムスタンプと Java クライアント ライブラリを使用してデータから「上位 10 件」の結果をクエリする方法
次のステップ:
- Spanner CAP ホワイトペーパーを読む
- スキーマ設計とクエリのベスト プラクティスについて確認する
- Cloud Spanner commit タイムスタンプの詳細を確認する
フィードバックをお寄せください
- 簡単なアンケートにご協力ください