Cloud Spanner: Java でゲーム リーダーボードを作成する

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)にログインし、新しいプロジェクトを作成します。

すでにプロジェクトが存在する場合は、コンソールの左上にあるプロジェクト選択プルダウン メニューをクリックします。

6c9406d9b014760.png

表示されるダイアログで [新しいプロジェクト] ボタンをクリックして新しいプロジェクトを作成します。

f708315ae07353d0.png

まだプロジェクトが存在しない場合は、次のような最初のプロジェクトを作成するためのダイアログが表示されます。

870a3cbd6541ee86.png

続いて表示されるプロジェクト作成ダイアログでは、新しいプロジェクトの詳細を入力できます。

6a92c57d3250a4b3.png

プロジェクト ID を忘れないようにしてください。プロジェクト ID は、すべての Google Cloud プロジェクトを通じて一意の名前にする必要があります(上記の名前はすでに使用されているため使用できません)。以降、この Codelab では PROJECT_ID と表します。

次に、Google Cloud リソースを使用し、Cloud Spanner API を有効にするために、Developers Console で課金を有効にする必要があります。

15d0ef27a8fbab27.png

この 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 でも動作します)。

  1. Cloud Console から Cloud Shell を有効にするには、[Cloud Shell をアクティブにする] gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A をクリックします(環境のプロビジョニングと接続に若干時間を要します)。

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjvIEx9pIkE-246DomWuCfiGHK78DgoeWkHRw

Screen Shot 2017-06-14 at 10.13.43 PM.png

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 を調べます。

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

Cloud Shell では、デフォルトで環境変数もいくつか設定されます。これらの変数は、以降のコマンドを実行する際に有用なものです。

echo $GOOGLE_CLOUD_PROJECT

コマンド出力

<PROJECT_ID>
  1. 最後に、デフォルトのゾーンとプロジェクト構成を設定します。
gcloud config set compute/zone us-central1-f

さまざまなゾーンを選択できます。詳細については、リージョンとゾーンをご覧ください。

概要

このステップでは、環境を設定します。

次の設定

次に、Cloud Spanner インスタンスを設定します。

このステップでは、この Codelab 用に Cloud Spanner インスタンスを設定します。左上のハンバーガー メニュー 3129589f7bc9e5ce.png で Spanner エントリ 1a6580bd3d3e6783.png を検索するか、「/」を押して Spanner と入力し「Spanner」を検索します。

36e52f8df8e13b99.png

次に、95269e75bc8c3e4d.png をクリックしてインスタンスのインスタンス名 cloudspanner-leaderboard を入力し、構成を選択(リージョン インスタンスを選択)してノード数を設定しフォームに入力します。この Codelab で必要となるノードは 1 つのみです。本番環境インスタンスの場合に Cloud Spanner SLA の対象となるには、Cloud Spanner インスタンスで 3 つ以上のノードを実行する必要があります。

最後に重要な点として [作成] をクリックしてください。数秒以内に必要な Cloud Spanner インスタンスが作成されます。

dceb68e9ed3801e8.png

次のステップでは、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 エディタを開きます。

73cf70e05f653ca.png

リーダーボード フォルダの下にある 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] セクションで、左側のメニューに新しいデータベースとテーブルが表示されます。

ba9008bb84cb90b0.png

次のステップでは、新しいデータベースにデータを読み込むようにアプリケーションを更新します。

これで、PlayersScores の 2 つのテーブルを含む leaderboard という名前のデータベースが作成されました。次に、Java クライアント ライブラリを使用して Players テーブルにプレーヤーを、Scores テーブルに各プレーヤーのランダムなスコアを入力してみましょう。

まだ開いていない場合は、以下でハイライト表示されているアイコンをクリックして Cloud Shell エディタを開きます。

ef49fcbaaed19024.png

次に、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] 列にデータが存在することを確認できます。

7bc2c96293c31c49.png

次に、[Scores] テーブルをクリックして [Data] タブを選択し、Scores テーブルにもデータが存在することを確認しましょう。テーブルの PlayerIdTimestampScore の各列にデータが存在することを確認できます。

d8a4ee4f13244c19.png

投稿するとゲーム リーダーボードの作成に使用できるクエリを実行するように、アプリを更新しましょう。

データベースを設定して情報をテーブルに読み込んだところで、このデータを使用してリーダーボードを作成してみましょう。そのためには、次の 4 つの質問に回答する必要があります。

  1. 常時「トップ 10」入りしているのは、どのプレーヤーですか?
  2. どのプレーヤーが今年の「トップ 10」のプレーヤーですか?
  3. どのプレーヤーが今月のトップ 10 のプレーヤーですか?
  4. どのプレーヤーが今週の「トップ 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 件」の結果をクエリする方法

次のステップ:

フィードバックをお寄せください