Java 版 Spanner 使用入门


目标

本教程将介绍如何使用 Spanner 完成以下步骤: 适用于 Java 的客户端库:

  • 创建 Spanner 实例和数据库。
  • 写入、读取数据库中的数据和对数据执行 SQL 查询。
  • 更新数据库架构。
  • 使用读写事务更新数据。
  • 向数据库添加二级索引。
  • 使用索引来读取数据和对数据执行 SQL 查询。
  • 使用只读事务检索数据。

费用

本教程使用 Spanner,它是 Google Cloud 的收费组件。如需了解 Spanner 的使用费用,请参阅价格

准备工作

完成设置中介绍的步骤,包括创建和设置默认 Google Cloud 项目、启用结算功能、启用 Cloud Spanner API 以及设置 OAuth 2.0 来获取身份验证凭据以使用 Cloud Spanner API。

尤其要确保运行 gcloud auth application-default login,以便使用身份验证凭据设置本地开发环境。

准备本地 Java 环境

  1. 在开发机器上安装以下内容(如果尚未安装):

  2. 将示例应用代码库克隆到本地机器:

    git clone https://github.com/googleapis/java-spanner.git
    
  3. 切换到包含 Spanner 示例代码的目录:

    cd java-spanner/samples/snippets
    

创建实例

首次使用 Spanner 时,您必须创建一个实例,实例是 Spanner 数据库使用的资源分配单位。创建实例时,请选择一个实例配置(决定数据的存储位置),同时选择要使用的节点数(决定实例中服务资源和存储资源的数量)。

执行以下命令,在区域 us-central1 中创建具有 1 个节点的 Spanner 实例:

gcloud spanner instances create test-instance --config=regional-us-central1 \
    --description="Test Instance" --nodes=1

请注意,此命令将创建一个具有以下特征的实例:

  • 实例 ID 为 test-instance
  • 显示名为 Test Instance
  • 实例配置为 regional-us-central1(单区域配置将数据存储在单个区域中,而多区域配置则将数据分布在多个区域中。如需了解详情,请参阅实例简介。)
  • 节点数为 1(node_count 对应于实例中数据库可用的服务资源和存储资源的数量。如需了解详情,请参阅节点和处理单元。)

您应该会看到:

Creating instance...done.

浏览示例文件

示例代码库包含一个示例,展示了如何在 Java 中使用 Spanner。

创建数据库

通过在命令行运行以下命令,在名为 test-instance 的实例中创建名为 example-db 的数据库。

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]
以下代码会在该数据库中创建一个数据库和两个表。

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 来读取、写入和执行事务。

GoogleSQL

SpannerOptions options = SpannerOptions.newBuilder().build();
Spanner spanner = options.getService();
DatabaseAdminClient dbAdminClient = null;
try {
  final String command = args[0];
  DatabaseId db = DatabaseId.of(options.getProjectId(), args[1], args[2]);
  DatabaseClient dbClient = spanner.getDatabaseClient(db);
  dbAdminClient = spanner.createDatabaseAdminClient();

  // Use client here...
} finally {
  if (dbAdminClient != null) {
    if (!dbAdminClient.isShutdown() || !dbAdminClient.isTerminated()) {
      dbAdminClient.close();
    }
  }
  spanner.close();
}

PostgreSQL

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.jar \
    writeusingdml test-instance example-db

您应该会看到:

4 records inserted.

使用变更写入数据

您还可以使用变更插入数据。

您可以使用 Mutation 对象写入数据。Mutation 对象是用于变更操作的容器。Mutation 表示 Spanner 插入、更新和删除的序列, 以原子方式应用于 Spanner 数据库中的不同行和表。

newInsertBuilder() Mutation 类中的方法构造了一个 INSERT 变更,该变更会在 在表中添加新行。如果该行已经存在,则写入失败。或者,您可以使用 newInsertOrUpdateBuilder 方法构建 INSERT_OR_UPDATE 变更,该变更会在该行已经存在时更新列值。

DatabaseClient 类中的 write() 方法可写入变更。单个批处理中的所有变更均以原子方式应用。

此代码演示了如何使用变更写入数据:

static final List<Singer> SINGERS =
    Arrays.asList(
        new Singer(1, "Marc", "Richards"),
        new Singer(2, "Catalina", "Smith"),
        new Singer(3, "Alice", "Trentor"),
        new Singer(4, "Lea", "Martin"),
        new Singer(5, "David", "Lomond"));

static final List<Album> ALBUMS =
    Arrays.asList(
        new Album(1, 1, "Total Junk"),
        new Album(1, 2, "Go, Go, Go"),
        new Album(2, 1, "Green"),
        new Album(2, 2, "Forever Hold Your Peace"),
        new Album(2, 3, "Terrified"));
static void writeExampleData(DatabaseClient dbClient) {
  List<Mutation> mutations = new ArrayList<>();
  for (Singer singer : SINGERS) {
    mutations.add(
        Mutation.newInsertBuilder("Singers")
            .set("SingerId")
            .to(singer.singerId)
            .set("FirstName")
            .to(singer.firstName)
            .set("LastName")
            .to(singer.lastName)
            .build());
  }
  for (Album album : ALBUMS) {
    mutations.add(
        Mutation.newInsertBuilder("Albums")
            .set("SingerId")
            .to(album.singerId)
            .set("AlbumId")
            .to(album.albumId)
            .set("AlbumTitle")
            .to(album.albumTitle)
            .build());
  }
  dbClient.write(mutations);
}

使用 write 参数运行示例。

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

您应该会看到命令成功运行。

使用 SQL 查询数据

Spanner 支持使用 SQL 接口读取数据,您可以使用 Google Cloud CLI 在命令行中使用该接口,也可以通过 Java 版 Spanner 客户端库以编程方式使用该接口。

在命令行中

执行以下 SQL 语句,读取 Albums 表中所有列的值:

gcloud spanner databases execute-sql example-db --instance=test-instance \
    --sql='SELECT SingerId, AlbumId, AlbumTitle FROM Albums'

结果应为:

SingerId AlbumId AlbumTitle
1        1       Total Junk
1        2       Go, Go, Go
2        1       Green
2        2       Forever Hold Your Peace
2        3       Terrified

使用 Java 版 Spanner 客户端库

除了在命令行中执行 SQL 语句外,还可以使用 Java 版 Spanner 客户端库以编程方式发出相同的 SQL 语句。

使用以下方法和类运行 SQL 查询:

  • singleUse() DatabaseClient 类中的方法:使用此方法读取一个或多个 来自 Spanner 表中的一行或多行的列。退货费用 singleUse() 一个 ReadContext 对象,用于运行读取或 SQL 语句。
  • ReadContext 类的 executeQuery() 方法:使用此方法对数据库执行查询。
  • Statement 类:使用此类构造 SQL 字符串。
  • ResultSet 类:使用此类访问由 SQL 语句或读取调用返回的数据。

下面演示了如何发出查询并访问数据:

static void query(DatabaseClient dbClient) {
  try (ResultSet resultSet =
      dbClient
          .singleUse() // Execute a single read or query against Cloud Spanner.
          .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
    }
  }
}

使用 query 参数运行示例。

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

您应该会看到以下结果:

1 1 Total Junk
1 2 Go, Go, Go
2 1 Green
2 2 Forever Hold Your Peace
2 3 Terrified

使用 SQL 参数进行查询

如果您的应用具有频繁执行的查询,您可以提高其性能 对其进行参数化处理。生成的参数查询可以缓存下来并重复使用,这样做可以降低编译开销。如需了解详情,请参阅 使用查询参数加快频繁执行的查询的速度

以下示例展示了如何在 WHERE 子句中使用形参 包含特定 LastName 值的查询记录。

GoogleSQL

static void queryWithParameter(DatabaseClient dbClient) {
  Statement statement =
      Statement.newBuilder(
              "SELECT SingerId, FirstName, LastName "
                  + "FROM Singers "
                  + "WHERE LastName = @lastName")
          .bind("lastName")
          .to("Garcia")
          .build();
  try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong("SingerId"),
          resultSet.getString("FirstName"),
          resultSet.getString("LastName"));
    }
  }
}

PostgreSQL

static void queryWithParameter(DatabaseClient dbClient) {
  Statement statement =
      Statement.newBuilder(
              "SELECT singerid AS \"SingerId\", "
                  + "firstname as \"FirstName\", lastname as \"LastName\" "
                  + "FROM Singers "
                  + "WHERE LastName = $1")
          .bind("p1")
          .to("Garcia")
          .build();
  try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong("SingerId"),
          resultSet.getString("FirstName"),
          resultSet.getString("LastName"));
    }
  }
}

使用 queryWithParameter 参数运行示例。

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

您应该会看到以下结果:

12 Melissa Garcia

使用读取 API 读取数据

除 Spanner 的 SQL 接口外,Spanner 还支持读取接口。

使用 ReadContext 类的 read() 方法可从数据库中读取行。使用 KeySet 对象定义要读取的键和键范围的集合。

下面演示了如何读取数据:

static void read(DatabaseClient dbClient) {
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .read(
              "Albums",
              KeySet.all(), // Read all rows in a table.
              Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
    }
  }
}

使用 read 参数运行示例。

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

您看到的输出结果应该类似于以下内容:

1 1 Total Junk
1 2 Go, Go, Go
2 1 Green
2 2 Forever Hold Your Peace
2 3 Terrified

更新数据库架构

假设您需要将名为 MarketingBudget 的新列添加到 Albums 表。向现有表添加新列需要更新数据库架构。Spanner 支持对数据库进行架构更新,同时 以便继续处理流量架构更新不需要 数据库离线,且不会锁定整个表或列;您可以继续 在架构更新期间将数据写入数据库。如需详细了解支持的架构更新和架构更改性能,请参阅进行架构更新

添加列

您可以使用 Google Cloud CLI 或 以编程方式使用 适用于 Java 的 Spanner 客户端库。

在命令行中

使用以下 ALTER TABLE 命令向表添加新列:

GoogleSQL

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='ALTER TABLE Albums ADD COLUMN MarketingBudget INT64'

PostgreSQL

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT'

您应该会看到:

Schema updating...done.

使用 Java 版 Spanner 客户端库

使用 DatabaseAdminClient 类的 updateDatabaseDdl() 方法来修改架构:

GoogleSQL

static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64")).get();
    System.out.println("Added MarketingBudget column");
  } 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 addMarketingBudget(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget bigint")).get();
    System.out.println("Added MarketingBudget column");
  } 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);
  }
}

使用 addmarketingbudget 参数运行示例。

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

您应该会看到:

Added MarketingBudget column.

将数据写入新列

以下代码可将数据写入新列。对于 Albums(1, 1) 键控的行,该代码会将 MarketingBudget 设置为 100000;而对于 Albums(2, 2) 键控的行,该代码会将其设置为 500000

static void update(DatabaseClient dbClient) {
  // Mutation can be used to update/insert/delete a single row in a table. Here we use
  // newUpdateBuilder to create update mutations.
  List<Mutation> mutations =
      Arrays.asList(
          Mutation.newUpdateBuilder("Albums")
              .set("SingerId")
              .to(1)
              .set("AlbumId")
              .to(1)
              .set("MarketingBudget")
              .to(100000)
              .build(),
          Mutation.newUpdateBuilder("Albums")
              .set("SingerId")
              .to(2)
              .set("AlbumId")
              .to(2)
              .set("MarketingBudget")
              .to(500000)
              .build());
  // This writes all the mutations to Cloud Spanner atomically.
  dbClient.write(mutations);
}

使用 update 参数运行示例。

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

您还可以执行 SQL 查询或读取调用来获取刚才写入的值。

以下是执行查询的代码:

GoogleSQL

static void queryMarketingBudget(DatabaseClient dbClient) {
  // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to
  // null. A try-with-resource block is used to automatically release resources held by
  // ResultSet.
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .executeQuery(Statement.of("SELECT SingerId, AlbumId, MarketingBudget FROM Albums"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s\n",
          resultSet.getLong("SingerId"),
          resultSet.getLong("AlbumId"),
          // We check that the value is non null. ResultSet getters can only be used to retrieve
          // non null values.
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
    }
  }
}

PostgreSQL

static void queryMarketingBudget(DatabaseClient dbClient) {
  // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to
  // null. A try-with-resource block is used to automatically release resources held by
  // ResultSet.
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .executeQuery(Statement.of("SELECT singerid as \"SingerId\", "
              + "albumid as \"AlbumId\", marketingbudget as \"MarketingBudget\" "
              + "FROM Albums"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s\n",
          resultSet.getLong("SingerId"),
          resultSet.getLong("AlbumId"),
          // We check that the value is non null. ResultSet getters can only be used to retrieve
          // non null values.
          resultSet.isNull("MarketingBudget") ? "NULL" :
              resultSet.getLong("MarketingBudget"));
    }
  }
}

如需执行此查询,请使用 querymarketingbudget 参数运行示例。

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

您应该会看到:

1 1 100000
1 2 NULL
2 1 NULL
2 2 500000
2 3 NULL

更新数据

您可以在读写事务中使用 DML 来更新数据。

您可以使用 executeUpdate() 方法来执行 DML 语句。

GoogleSQL

static void writeWithTransactionUsingDml(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        // Transfer marketing budget from one album to another. We do it in a transaction to
        // ensure that the transfer is atomic.
        String sql1 =
            "SELECT MarketingBudget from Albums WHERE SingerId = 2 and AlbumId = 2";
        ResultSet resultSet = transaction.executeQuery(Statement.of(sql1));
        long album2Budget = 0;
        while (resultSet.next()) {
          album2Budget = resultSet.getLong("MarketingBudget");
        }
        // Transaction will only be committed if this condition still holds at the time of
        // commit. Otherwise it will be aborted and the callable will be rerun by the
        // client library.
        long transfer = 200000;
        if (album2Budget >= transfer) {
          String sql2 =
              "SELECT MarketingBudget from Albums WHERE SingerId = 1 and AlbumId = 1";
          ResultSet resultSet2 = transaction.executeQuery(Statement.of(sql2));
          long album1Budget = 0;
          while (resultSet2.next()) {
            album1Budget = resultSet2.getLong("MarketingBudget");
          }
          album1Budget += transfer;
          album2Budget -= transfer;
          Statement updateStatement =
              Statement.newBuilder(
                      "UPDATE Albums "
                          + "SET MarketingBudget = @AlbumBudget "
                          + "WHERE SingerId = 1 and AlbumId = 1")
                  .bind("AlbumBudget")
                  .to(album1Budget)
                  .build();
          transaction.executeUpdate(updateStatement);
          Statement updateStatement2 =
              Statement.newBuilder(
                      "UPDATE Albums "
                          + "SET MarketingBudget = @AlbumBudget "
                          + "WHERE SingerId = 2 and AlbumId = 2")
                  .bind("AlbumBudget")
                  .to(album2Budget)
                  .build();
          transaction.executeUpdate(updateStatement2);
        }
        return null;
      });
}

PostgreSQL

static void writeWithTransactionUsingDml(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        // Transfer marketing budget from one album to another. We do it in a transaction to
        // ensure that the transfer is atomic.
        String sql1 =
            "SELECT marketingbudget as \"MarketingBudget\" from Albums WHERE "
                + "SingerId = 2 and AlbumId = 2";
        ResultSet resultSet = transaction.executeQuery(Statement.of(sql1));
        long album2Budget = 0;
        while (resultSet.next()) {
          album2Budget = resultSet.getLong("MarketingBudget");
        }
        // Transaction will only be committed if this condition still holds at the time of
        // commit. Otherwise it will be aborted and the callable will be rerun by the
        // client library.
        long transfer = 200000;
        if (album2Budget >= transfer) {
          String sql2 =
              "SELECT marketingbudget as \"MarketingBudget\" from Albums WHERE "
                  + "SingerId = 1 and AlbumId = 1";
          ResultSet resultSet2 = transaction.executeQuery(Statement.of(sql2));
          long album1Budget = 0;
          while (resultSet2.next()) {
            album1Budget = resultSet2.getLong("MarketingBudget");
          }
          album1Budget += transfer;
          album2Budget -= transfer;
          Statement updateStatement =
              Statement.newBuilder(
                      "UPDATE Albums "
                          + "SET MarketingBudget = $1"
                          + "WHERE SingerId = 1 and AlbumId = 1")
                  .bind("p1")
                  .to(album1Budget)
                  .build();
          transaction.executeUpdate(updateStatement);
          Statement updateStatement2 =
              Statement.newBuilder(
                      "UPDATE Albums "
                          + "SET MarketingBudget = $1"
                          + "WHERE SingerId = 2 and AlbumId = 2")
                  .bind("p1")
                  .to(album2Budget)
                  .build();
          transaction.executeUpdate(updateStatement2);
        }
        return null;
      });
}

使用 writewithtransactionusingdml 参数运行示例。

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

使用二级索引

假设您想要提取 Albums 表中 AlbumTitle 值在特定范围内的所有行。您可以使用 SQL 语句或读取调用读取 AlbumTitle 列中的所有值,然后舍弃不符合条件的行。不过,执行全表扫描费用高昂,特别是对内含大量行的表来说更是如此。相反,如果对表创建二级索引,按非主键列进行搜索,则可以提高行检索速度。

向现有表添加二级索引需要更新架构。点赞 Spanner 支持添加索引,同时 以便继续处理流量Spanner 会自动回填 使用现有数据创建索引。回填可能需要几分钟时间才能完成,但在此过程中,您无需使数据库离线,也无需避免写入已编入索引的表。有关详情,请参阅 添加二级索引

添加二级索引后,Spanner 会自动将其用于 使用索引可能会提高运行速度的 SQL 查询。如果使用读取接口,则必须指定要使用的索引。

添加二级索引

您可以使用 gcloud CLI 或 以编程方式使用 Java 版 Spanner 客户端库。

在命令行中

使用以下 CREATE INDEX 命令向数据库添加索引:

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)'

您应该会看到:

Schema updating...done.

使用 Java 版 Spanner 客户端库

使用 updateDatabaseDdl() 方法以添加索引:DatabaseAdminClient

static void addIndex(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)")).get();
    System.out.println("Added AlbumsByAlbumTitle index");
  } 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);
  }
}

使用 addindex 参数运行示例。

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

添加索引可能需要几分钟时间。添加索引后,您应该会看到:

Added the AlbumsByAlbumTitle index.

使用索引进行读取

对于 SQL 查询,Spanner 会自动使用适当的索引。在读取接口中,您必须在请求中指定索引。

如需在读取接口中使用索引,请使用 ReadContext 类的 readUsingIndex() 方法。

以下代码会从 AlbumsByAlbumTitle 索引中提取所有 AlbumIdAlbumTitle 列。

static void readUsingIndex(DatabaseClient dbClient) {
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .readUsingIndex(
              "Albums",
              "AlbumsByAlbumTitle",
              KeySet.all(),
              Arrays.asList("AlbumId", "AlbumTitle"))) {
    while (resultSet.next()) {
      System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1));
    }
  }
}

使用 readindex 参数运行示例。

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

您应该会看到:

2 Forever Hold Your Peace
2 Go, Go, Go
1 Green
3 Terrified
1 Total Junk

添加了仅索引读取的索引

您可能已经注意到,之前的读取示例不包括 MarketingBudget 列。这是因为,Spanner 的读取接口不支持将索引与数据表联接起来查找未存储在索引中的值。

请创建 AlbumsByAlbumTitle 的备用定义,用于将 MarketingBudget 的副本存储到索引中。

在命令行中

GoogleSQL

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)

PostgreSQL

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) INCLUDE (MarketingBudget)

添加索引可能需要几分钟时间。添加索引后,您应该会看到:

Schema updating...done.

使用 Java 版 Spanner 客户端库

使用 DatabaseAdminClient 类的 updateDatabaseDdl() 方法来添加带 STORING 子句的索引(适用于 GoogleSQL)和带 INCLUDE 子句的索引(适用于 PostgreSQL):

GoogleSQL

static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList(
            "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) "
                + "STORING (MarketingBudget)")).get();
    System.out.println("Added AlbumsByAlbumTitle2 index");
  } 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 addStoringIndex(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList(
            "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) "
                + "INCLUDE (MarketingBudget)")).get();
    System.out.println("Added AlbumsByAlbumTitle2 index");
  } 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);
  }
}

使用 addstoringindex 参数运行示例。

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

添加索引可能需要几分钟时间。添加索引后,应显示如下信息:

Added AlbumsByAlbumTitle2 index

现在,当您执行读取操作时便可从 AlbumsByAlbumTitle2 索引中提取所有 AlbumIdAlbumTitleMarketingBudget 列:

static void readStoringIndex(DatabaseClient dbClient) {
  // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget.
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .readUsingIndex(
              "Albums",
              "AlbumsByAlbumTitle2",
              KeySet.all(),
              Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong(0),
          resultSet.getString(1),
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
    }
  }
}

使用 readstoringindex 参数运行示例。

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

您看到的输出结果应该类似于以下内容:

2 Forever Hold Your Peace 300000
2 Go, Go, Go NULL
1 Green NULL
3 Terrified NULL
1 Total Junk 300000

使用只读事务检索数据

假设您要在同一时间戳执行多个读取操作。只读事务会观察事务提交记录的一致前缀,以便应用始终获得一致的数据。 使用 ReadOnlyTransaction 对象执行只读事务。使用 DatabaseClient 类的 readOnlyTransaction() 方法来获取 ReadOnlyTransaction 对象。

下面演示了如何运行查询并在同一只读事务中执行读取操作:

static void readOnlyTransaction(DatabaseClient dbClient) {
  // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it.
  // We use a try-with-resource block to automatically do so.
  try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) {
    ResultSet queryResultSet =
        transaction.executeQuery(
            Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"));
    while (queryResultSet.next()) {
      System.out.printf(
          "%d %d %s\n",
          queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2));
    }
    try (ResultSet readResultSet =
        transaction.read(
            "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) {
      while (readResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2));
      }
    }
  }
}

使用 readonlytransaction 参数运行示例。

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

您看到的输出结果应该类似于以下内容:

2 2 Forever Hold Your Peace
1 2 Go, Go, Go
2 1 Green
2 3 Terrified
1 1 Total Junk
1 1 Total Junk
1 2 Go, Go, Go
2 1 Green
2 2 Forever Hold Your Peace
2 3 Terrified

清理

为避免因本教程中使用的资源导致您的 Google Cloud 账号产生额外费用,请删除数据库和您创建的实例。

删除数据库

如果您删除一个实例,则该实例中的所有数据库都会自动删除。 本步骤演示了如何在不删除实例的情况下删除数据库(您仍需为该实例付费)。

在命令行中

gcloud spanner databases delete example-db --instance=test-instance

使用 Google Cloud 控制台

  1. 前往 Google Cloud 控制台中的 Spanner 实例页面。

    转到“实例”页面

  2. 点击实例。

  3. 点击您想删除的数据库。

  4. 数据库详细信息页面中,点击删除

  5. 确认您要删除数据库并点击删除

删除实例

删除实例会自动删除在该实例中创建的所有数据库。

在命令行中

gcloud spanner instances delete test-instance

使用 Google Cloud 控制台

  1. 前往 Google Cloud 控制台中的 Spanner 实例页面。

    转到“实例”页面

  2. 点击您的实例。

  3. 点击删除

  4. 确认您要删除实例并点击删除

后续步骤