Java で Spanner を使ってみる

目標

このチュートリアルでは、Java 用の Spanner クライアント ライブラリを使用する以下の手順について説明します。

  • Spanner のインスタンスとデータベースを作成します。
  • データベースのデータに対し、書き込み、読み取り、SQL クエリの実行を行います。
  • データベース スキーマを更新します。
  • 読み取り / 書き込みトランザクションを使用してデータを更新します。
  • セカンダリ インデックスをデータベースに追加します。
  • インデックスを使用して、データの読み込みと SQL クエリの実行を行います。
  • 読み取り専用トランザクションを使用してデータを取得します。

費用

このチュートリアルでは、Google Cloudの課金対象コンポーネントである Spanner を使用します。Spanner の使用料金については、料金についてのページをご覧ください。

始める前に

設定で説明されている手順を終わらせておきます。この手順では、デフォルトの Google Cloud プロジェクトの作成と設定、課金の有効化、Cloud Spanner API の有効化、Cloud Spanner API の使用に必要な認証情報を取得するための OAuth 2.0 の設定を行います。

特に、gcloud auth application-default login を使用したローカル開発環境の認証情報の設定は必ず行ってください。

ローカルの Java 環境を準備する

  1. 開発マシンに次のものがまだインストールされていない場合はインストールします。

  2. ローカルマシンにサンプルアプリのリポジトリのクローンを作成します。

    git clone https://github.com/googleapis/java-spanner.git
    
  3. Spanner のサンプルコードが含まれるディレクトリに移動します。

    cd java-spanner/samples/snippets
    
  4. サンプル JAR ファイルを生成します。

    mvn clean package
    

インスタンスを作成する

Spanner を最初に使用する際は、インスタンスを作成する必要があります。インスタンスとは、Spanner データベースによって使用されるリソースの割り当てのことです。インスタンスの作成時には、インスタンス構成を選択してデータの格納場所を指定し、さらに使用するノード数も選択してインスタンスの配信リソースおよびストレージ リソースの量を決定します。

次のいずれかの方法で Spanner インスタンスを作成する方法については、インスタンスを作成するをご覧ください。インスタンスに test-instance という名前を付けることで、このドキュメント内の他のトピックで test-instance という名前のインスタンスを参照している箇所と関連付けて使用できます。

  • Google Cloud CLI
  • Google Cloud コンソール
  • クライアント ライブラリ(C++、C#、Go、Java、Node.js、PHP、Python、Ruby)

サンプル ファイルを確認する

サンプル リポジトリには、C# で Java を使用する方法を示すサンプルが含まれています。

データベースを作成する

GoogleSQL

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
createdatabase test-instance example-db

PostgreSQL

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
createpgdatabase test-instance example-db

以下のように表示されます。

Created database [example-db]
次のコードにより、データベースが 1 つと、このデータベース内に 2 つのテーブルが作成されます。

GoogleSQL

static void createDatabase(DatabaseAdminClient dbAdminClient,
    InstanceName instanceName, String databaseId) {
  CreateDatabaseRequest createDatabaseRequest =
      CreateDatabaseRequest.newBuilder()
          .setCreateStatement("CREATE DATABASE `" + databaseId + "`")
          .setParent(instanceName.toString())
          .addAllExtraStatements(Arrays.asList(
              "CREATE TABLE Singers ("
                  + "  SingerId   INT64 NOT NULL,"
                  + "  FirstName  STRING(1024),"
                  + "  LastName   STRING(1024),"
                  + "  SingerInfo BYTES(MAX),"
                  + "  FullName STRING(2048) AS "
                  + "  (ARRAY_TO_STRING([FirstName, LastName], \" \")) STORED"
                  + ") PRIMARY KEY (SingerId)",
              "CREATE TABLE Albums ("
                  + "  SingerId     INT64 NOT NULL,"
                  + "  AlbumId      INT64 NOT NULL,"
                  + "  AlbumTitle   STRING(MAX)"
                  + ") PRIMARY KEY (SingerId, AlbumId),"
                  + "  INTERLEAVE IN PARENT Singers ON DELETE CASCADE")).build();
  try {
    // Initiate the request which returns an OperationFuture.
    com.google.spanner.admin.database.v1.Database db =
        dbAdminClient.createDatabaseAsync(createDatabaseRequest).get();
    System.out.println("Created database [" + db.getName() + "]");
  } 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);
  }
}

PostgreSQL

static void createPostgreSqlDatabase(
    DatabaseAdminClient dbAdminClient, String projectId, String instanceId, String databaseId) {
  final CreateDatabaseRequest request =
      CreateDatabaseRequest.newBuilder()
          .setCreateStatement("CREATE DATABASE \"" + databaseId + "\"")
          .setParent(InstanceName.of(projectId, instanceId).toString())
          .setDatabaseDialect(DatabaseDialect.POSTGRESQL).build();

  try {
    // Initiate the request which returns an OperationFuture.
    Database db = dbAdminClient.createDatabaseAsync(request).get();
    System.out.println("Created database [" + db.getName() + "]");
  } 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 createTableUsingDdl(DatabaseAdminClient dbAdminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    dbAdminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList(
            "CREATE TABLE Singers ("
                + "  SingerId   bigint NOT NULL,"
                + "  FirstName  character varying(1024),"
                + "  LastName   character varying(1024),"
                + "  SingerInfo bytea,"
                + "  FullName character varying(2048) GENERATED "
                + "  ALWAYS AS (FirstName || ' ' || LastName) STORED,"
                + "  PRIMARY KEY (SingerId)"
                + ")",
            "CREATE TABLE Albums ("
                + "  SingerId     bigint NOT NULL,"
                + "  AlbumId      bigint NOT NULL,"
                + "  AlbumTitle   character varying(1024),"
                + "  PRIMARY KEY (SingerId, AlbumId)"
                + ") INTERLEAVE IN PARENT Singers ON DELETE CASCADE")).get();
    System.out.println("Created Singers & Albums tables in database: [" + databaseName + "]");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw SpannerExceptionFactory.asSpannerException(e);
  } 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);
  }
}

次のステップでは、データベースにデータを書き込みます。

データベース クライアントを作成する

読み取りまたは書き込みを行うには、まず DatabaseClient を作成する必要があります。DatabaseClient はデータベース接続と考えることができます。Spanner とのすべてのやり取りは DatabaseClient を経由する必要があります。通常はアプリケーション開始時に DatabaseClient を作成し、読み取り、書き込み、トランザクションの実行に DatabaseClient を再利用します。

SpannerOptions options = SpannerOptions.newBuilder().build();
Spanner spanner = options.getService();
DatabaseAdminClient dbAdminClient = null;
try {
  DatabaseClient dbClient = spanner.getDatabaseClient(db);
  dbAdminClient = spanner.createDatabaseAdminClient();
} finally {
  if (dbAdminClient != null) {
    if (!dbAdminClient.isShutdown() || !dbAdminClient.isTerminated()) {
      dbAdminClient.close();
    }
  }
  spanner.close();
}

クライアントごとに Spanner のリソースを使用するため、close() を呼び出して不要なクライアントを閉じることをおすすめします。

詳細については、DatabaseClient Javadoc リファレンスをご覧ください。

DML を使用してデータを書き込む

読み取り / 書き込みトランザクションでデータ操作言語(DML)を使用してデータを挿入できます。

executeUpdate() メソッドを使用して DML ステートメントを実行します。

static void writeUsingDml(DatabaseClient dbClient) {
  // Insert 4 singer records
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        String sql =
            "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES "
                + "(12, 'Melissa', 'Garcia'), "
                + "(13, 'Russell', 'Morales'), "
                + "(14, 'Jacqueline', 'Long'), "
                + "(15, 'Dylan', 'Shaw')";
        long rowCount = transaction.executeUpdate(Statement.of(sql));
        System.out.printf("%d records inserted.\n", rowCount);
        return null;
      });
}

writeusingdml 引数を使用してサンプルを実行します。

java -jar target/spanner-snippets/spanner-google-cloud-samples.