Erste Schritte mit Spanner in Java


Lernziele

In dieser Anleitung werden Sie durch die folgenden Schritte mit der Spanner-Clientbibliothek für Java geführt:

  • Spanner-Instanz und -Datenbank erstellen
  • SQL-Abfragen für Daten in der Datenbank schreiben, lesen und ausführen
  • Datenbankschema aktualisieren
  • Daten mit einer Lese-Schreib-Transaktion aktualisieren
  • Sekundären Index für die Datenbank hinzufügen
  • Mit dem Index Daten lesen und SQL-Abfragen ausführen
  • Daten über eine schreibgeschützte Transaktion abrufen

Kosten

In dieser Anleitung wird Spanner verwendet, eine kostenpflichtige Komponente von Google Cloud. Informationen zu den Kosten für die Verwendung von Spanner finden Sie unter Preise.

Hinweise

Führen Sie die unter Einrichten beschriebenen Schritte aus, die das Erstellen und Festlegen eines standardmäßigen Google Cloud-Projekts, das Aktivieren der Rechnungsstellung, das Aktivieren der Cloud Spanner API und das Einrichten von OAuth 2.0 umfassen, um Anmeldedaten für die Authentifizierung für die Verwendung der Cloud Spanner API zu erhalten.

Sie müssen insbesondere gcloud auth application-default login ausführen, um die lokale Entwicklungsumgebung mit Anmeldedaten für die Authentifizierung einzurichten.

Lokale Java-Umgebung vorbereiten

  1. Installieren Sie Folgendes auf Ihrem Entwicklungscomputer, falls nicht bereits vorhanden:

  2. Klonen Sie das Repository der Beispiel-App auf Ihren lokalen Computer:

    git clone https://github.com/googleapis/java-spanner.git
    
  3. Wechseln Sie in das Verzeichnis, das den Spanner-Beispielcode enthält:

    cd java-spanner/samples/snippets
    

Instanz erstellen

Wenn Sie Spanner zum ersten Mal verwenden, müssen Sie eine Instanz erstellen. Dabei handelt es sich um eine Zuweisung von Ressourcen, die von Spanner-Datenbanken verwendet werden. Wenn Sie eine Instanz erstellen, müssen Sie eine Instanzkonfiguration auswählen. Abhängig davon werden der Speicherort Ihrer Daten sowie die Anzahl der zu verwendenden Knoten festgelegt. Anhand der Knotenanzahl wird dann die Menge der Bereitstellungs- und Speicherressourcen in Ihrer Instanz festgelegt.

Führen Sie den folgenden Befehl aus, um eine Spanner-Instanz in der Region us-central1 mit 1 Knoten zu erstellen:

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

Dadurch wird eine Instanz mit diesen Properties erstellt:

  • Instanz-ID test-instance
  • Anzeigename Test Instance
  • Instanzkonfiguration regional-us-central1 – Bei regionalen Konfigurationen werden Daten in nur einer Region gespeichert, während sie bei multiregionalen Konfigurationen auf mehrere Regionen verteilt werden. Weitere Informationen finden Sie unter Informationen zu Instanzen.
  • Knotenanzahl 1 – node_count entspricht der Anzahl der Bereitstellungs- und Speicherressourcen in der Instanz, die für Datenbanken zur Verfügung stehen. Weitere Informationen finden Sie unter Knoten und Verarbeitungseinheiten.)

Hier sollten Sie das sehen:

Creating instance...done.

Beispieldateien ansehen

Das Beispiel-Repository enthält ein Beispiel, das zeigt, wie Spanner mit Java verwendet wird.

Der pom.xml fügt den Abhängigkeiten des Projekts die Spanner-Clientbibliothek für Java hinzu und konfiguriert das Assembly-Plug-in so, dass eine ausführbare JAR-Datei mit der in dieser Anleitung definierten Java-Klasse erstellt wird.

Erstellen Sie das Beispiel aus dem Verzeichnis samples/snippets:

mvn package

Datenbank erstellen

Erstellen Sie eine Datenbank mit dem Namen example-db in der Instanz test-instance, indem Sie Folgendes in der Befehlszeile ausführen:

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

Hier sollten Sie das sehen:

Created database [example-db]

Mit dem folgenden Code werden eine Datenbank und zwei Tabellen in der Datenbank erstellt.

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);
  }
}

Im nächsten Schritt werden Daten in die Datenbank geschrieben.

Datenbankclient erstellen

Zum Ausführen von Lese- oder Schreibvorgängen müssen Sie einen DatabaseClient erstellen. Sie können sich eine DatabaseClient wie eine Datenbankverbindung vorstellen: Alle Interaktionen mit Spanner müssen einen DatabaseClient durchlaufen. In der Regel erstellen Sie einen DatabaseClient beim Starten Ihrer Anwendung. Anschließend verwenden Sie DatabaseClient zum Lesen, Schreiben und Ausführen von Transaktionen.

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();
}

Da jeder Client Ressourcen in Spanner verwendet, empfiehlt es sich, nicht benötigte Clients durch Aufrufen von close() zu schließen.

Weitere Informationen finden Sie in der Javadoc-Referenz DatabaseClient.

Daten mit DML schreiben

Sie können Daten mit der Datenbearbeitungssprache (Data Manipulation Language, DML) in eine Lese-Schreib-Transaktion einfügen.

Für das Ausführen einer DML-Anweisung verwenden Sie die Methode executeUpdate().

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;
      });
}

Führen Sie das Beispiel mit dem Argument writeusingdml aus.

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

Hier sollten Sie das sehen:

4 records inserted.

Daten mit Mutationen schreiben

Sie können Daten auch mithilfe von Mutationen einfügen.

Daten werden mit einem Mutation-Objekt geschrieben. Ein Mutation-Objekt ist ein Container für Mutationsvorgänge. Ein Mutation stellt eine Folge von Einfügungs-, Aktualisierungs- und Löschvorgängen dar, die Spanner in kleinstmöglichen Schritten auf verschiedene Zeilen und Tabellen in einer Spanner-Datenbank anwendet.

Die Methode newInsertBuilder() in der Klasse Mutation konstruiert eine INSERT-Mutation, die eine neue Zeile in eine Tabelle einfügt. Wenn die Zeile bereits vorhanden ist, kann der Schreibvorgang nicht durchgeführt werden. Alternativ können Sie die Methode newInsertOrUpdateBuilder verwenden, um eine INSERT_OR_UPDATE-Mutation zu erstellen, die Spaltenwerte aktualisiert, wenn die Zeile bereits vorhanden ist.

Die Methode write() in der Klasse DatabaseClient schreibt die Mutationen. Alle Mutationen in einem einzelnen Batch werden in kleinstmöglichen Schritten angewendet.

Dieser Code zeigt, wie die Daten mithilfe von Mutationen geschrieben werden:

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);
}

Führen Sie das Beispiel mit dem Argument write aus.

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

Der Befehl sollte erfolgreich ausgeführt werden.

Daten mit SQL abfragen

Spanner unterstützt eine SQL-Schnittstelle zum Lesen von Daten, auf die Sie mit der Google Cloud CLI in der Befehlszeile oder programmgesteuert mit der Spanner-Clientbibliothek für Java zugreifen können.

Über die Befehlszeile

Führen Sie die folgende SQL-Anweisung aus, damit Sie die Werte aller Spalten aus der Tabelle Albums lesen können:

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

Das Ergebnis sollte in etwa so aussehen:

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

Spanner-Clientbibliothek für Java verwenden

Als Alternative zum Ausführen einer SQL-Anweisung in der Befehlszeile können Sie die gleiche SQL-Anweisung programmatisch mithilfe der Spanner-Clientbibliothek für Java ausführen.

Zum Ausführen der SQL-Abfrage werden die folgenden Methoden und Klassen verwendet:

  • Die Methode singleUse() in der Klasse DatabaseClient: Mit dieser Methode können Sie den Wert einer oder mehrerer Spalten aus einer oder mehreren Zeilen in einer Spanner-Tabelle lesen. singleUse() gibt ein ReadContext-Objekt zurück, das zum Ausführen einer Lese- oder SQL-Anweisung verwendet wird.
  • Die Methode executeQuery() der Klasse ReadContext: Verwenden Sie diese Methode, um eine Abfrage für eine Datenbank auszuführen.
  • Die Klasse Statement: Mit dieser Klasse können Sie einen SQL-String erstellen.
  • Die Klasse ResultSet: Mit dieser Klasse können Sie auf die von SQL-Anweisungen oder Leseaufrufen zurückgegebenen Daten zugreifen.

So geben Sie die Abfrage aus und greifen auf die Daten zu:

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));
    }
  }
}

Führen Sie das Beispiel mit dem Argument query aus.

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

Sie sollten folgendes Ergebnis sehen:

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

Abfrage mit einem SQL-Parameter

Wenn es für Ihre Anwendung eine häufig ausgeführte Abfrage gibt, können Sie die Leistung durch Parametrieren verbessern. Die resultierende parametrische Abfrage kann im Cache gespeichert und wiederverwendet werden, wodurch die Kompilierungskosten reduziert werden. Weitere Informationen finden Sie unter Mit Abfrageparametern häufig ausgeführte Abfragen beschleunigen.

Im folgenden Beispiel wird ein Parameter in der WHERE-Klausel verwendet, um Datensätze abzufragen, die einen bestimmten Wert für LastName enthalten.

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"));
    }
  }
}

Führen Sie das Beispiel mit dem Argument queryWithParameter aus.

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

Sie sollten dieses Ergebnis sehen:

12 Melissa Garcia

Daten mit der Lese-API auslesen

Zusätzlich zur SQL-Schnittstelle unterstützt Spanner auch eine Leseschnittstelle.

Verwenden Sie die Methode read() der Klasse ReadContext, um Zeilen aus der Datenbank zu lesen. Mit dem Objekt KeySet können Sie eine Sammlung der zu lesenden Schlüssel und Schlüsselbereiche definieren.

So lesen Sie die Daten aus:

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));
    }
  }
}

Führen Sie das Beispiel mit dem Argument read aus.

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

Die Ausgabe sollte etwa so aussehen:

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

Datenbankschema aktualisieren

Beispiel: Sie müssen eine neue Spalte namens MarketingBudget zur Tabelle Albums hinzufügen. Damit einer vorhandenen Tabelle eine neue Spalte hinzugefügt werden kann, muss das Datenbankschema aktualisiert werden. Spanner unterstützt Schemaaktualisierungen für eine Datenbank, während die Datenbank weiterhin Traffic bereitstellt. Bei Schemaaktualisierungen muss die Datenbank nicht offline geschaltet werden und es müssen keine ganzen Tabellen oder Spalten gesperrt werden. Sie können während der Schemaaktualisierung weiter Daten in die Datenbank schreiben. Weitere Informationen zu unterstützten Schemaaktualisierungen und zur Leistung während der Schemaänderung finden Sie unter Schemaaktualisierungen vornehmen.

Spalte hinzufügen

Sie können eine Spalte mit der Google Cloud CLI in der Befehlszeile oder programmatisch mithilfe der Spanner-Clientbibliothek für Java hinzufügen.

Über die Befehlszeile

Verwenden Sie den folgenden Befehl ALTER TABLE, um die neue Spalte zur Tabelle hinzuzufügen:

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'

Hier sollten Sie das sehen:

Schema updating...done.

Spanner-Clientbibliothek für Java verwenden

Verwenden Sie die Methode updateDatabaseDdl() der Klasse DatabaseAdminClient, um das Schema zu ändern:

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);
  }
}

Führen Sie das Beispiel mit dem Argument addmarketingbudget aus.

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

Hier sollten Sie das sehen:

Added MarketingBudget column.

Daten in die neue Spalte schreiben

Mit dem folgenden Code werden Daten in die neue Spalte geschrieben. Er legt für MarketingBudget den Wert 100000 für den Zeilenschlüssel fest, der durch Albums(1, 1) angegeben wird, und 500000 für den Zeilenschlüssel, der durch Albums(2, 2) angegeben wird.

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);
}

Führen Sie das Beispiel mit dem Argument update aus.

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

Sie können auch eine SQL-Abfrage oder einen Leseaufruf ausführen, um die Werte abzurufen, die Sie gerade geschrieben haben.

Mit diesem Code können Sie die Abfrage ausführen:

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"));
    }
  }
}

Für diese Abfrage führen Sie das Beispiel mit dem Argument querymarketingbudget aus.

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

Hier sollten Sie das sehen:

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

Daten aktualisieren

Sie können Daten mit DML in einer Lese-Schreib-Transaktion aktualisieren.

Zum Ausführen einer DML-Anweisung verwenden Sie die Methode executeUpdate().

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;
      });
}

Führen Sie das Beispiel mit dem Argument writewithtransactionusingdml aus.

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

Sekundären Index verwenden

Beispiel: Sie möchten alle Zeilen aus Albums abrufen, deren Wert für AlbumTitle in einem bestimmten Bereich liegen. Sie könnten dazu alle Werte aus der Spalte AlbumTitle mit einer SQL-Anweisung oder einem Leseaufruf lesen und dann die Zeilen verwerfen, die die Kriterien nicht erfüllen. Dieser vollständige Tabellenscan wäre jedoch sehr kostspielig, insbesondere bei Tabellen mit vielen Zeilen. Stattdessen können Sie einen sekundären Index für die Tabelle erstellen und damit das Abrufen von Zeilen beim Suchen über Spalten mit nicht primärem Schlüssel beschleunigen.

Damit ein sekundärer Index einer vorhandenen Tabelle hinzugefügt werden kann, muss das Schema aktualisiert werden. Wie bei anderen Schemaaktualisierungen unterstützt Spanner das Hinzufügen eines Index, während die Datenbank weiterhin Traffic verarbeitet. Spanner füllt den Index automatisch mit Ihren vorhandenen Daten auf. Backfills können einige Minuten dauern. Sie müssen aber die Datenbank nicht offline schalten und können während des Vorgangs weiter in die indexierten Tabellen schreiben. Weitere Informationen finden Sie unter Sekundären Index hinzufügen.

Nachdem Sie einen sekundären Index hinzugefügt haben, verwendet Spanner ihn automatisch für SQL-Abfragen, die mit dem Index wahrscheinlich schneller ausgeführt werden. Wenn Sie die Leseschnittstelle verwenden, müssen Sie den Index angeben, den Sie nutzen möchten.

Sekundären Index hinzufügen

Sie können einen Index mit der gcloud CLI oder programmgesteuert über die Spanner-Clientbibliothek für Java in der Befehlszeile hinzufügen.

Über die Befehlszeile

Verwenden Sie den folgenden Befehl CREATE INDEX, um der Datenbank einen Index hinzuzufügen:

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

Hier sollten Sie das sehen:

Schema updating...done.

Spanner-Clientbibliothek für Java verwenden

Verwenden Sie die Methode updateDatabaseDdl() der Klasse DatabaseAdminClient, um einen Index hinzuzufügen:

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);
  }
}

Führen Sie das Beispiel mit dem Argument addindex aus.

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

Das Hinzufügen eines Index kann einige Minuten dauern. Nachdem der Index hinzugefügt wurde, sollten Sie das sehen:

Added the AlbumsByAlbumTitle index.

Mit dem Index auslesen

Für SQL-Abfragen verwendet Spanner automatisch einen geeigneten Index. In der Leseschnittstelle müssen Sie den Index in Ihrer Anfrage angeben.

Für die Verwendung des Index in der Leseschnittstelle nutzen Sie die Methode readUsingIndex() der Klasse ReadContext.

Mit dem folgenden Code werden alle Spalten AlbumId und AlbumTitle aus dem Index AlbumsByAlbumTitle abgerufen.

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));
    }
  }
}

Führen Sie das Beispiel mit dem Argument readindex aus.

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

Hier sollten Sie das sehen:

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

Index für reine Indexlesevorgänge hinzufügen

Vielleicht haben Sie bemerkt, dass im vorherigen Lesebeispiel das Lesen der Spalte MarketingBudget nicht enthalten ist. Das liegt daran, dass die Leseschnittstelle von Spanner die Möglichkeit, einen Index mit einer Datentabelle zu verknüpfen, nicht unterstützt, um nach Werten zu suchen, die nicht im Index gespeichert sind.

Erstellen Sie eine alternative Definition von AlbumsByAlbumTitle, die eine Kopie von MarketingBudget im Index speichert.

Über die Befehlszeile

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)

Das Hinzufügen eines Index kann einige Minuten dauern. Nachdem der Index hinzugefügt wurde, sollte Folgendes angezeigt werden:

Schema updating...done.

Spanner-Clientbibliothek für Java verwenden

Verwenden Sie die Methode updateDatabaseDdl() der Klasse DatabaseAdminClient, um einen Index mit einer STORING-Klausel für GoogleSQL und der INCLUDE-Klausel für PostgreSQL hinzuzufügen:

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);
  }
}

Führen Sie das Beispiel mit dem Argument addstoringindex aus.

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

Das Hinzufügen eines Index kann einige Minuten dauern. Nachdem der Index hinzugefügt wurde, sollten Sie das sehen:

Added AlbumsByAlbumTitle2 index

Sie können jetzt einen Lesevorgang ausführen, der die Spalten AlbumId, AlbumTitle und MarketingBudget aus dem Index AlbumsByAlbumTitle2 abruft:

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"));
    }
  }
}

Führen Sie das Beispiel mit dem Argument readstoringindex aus.

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

Die Ausgabe sollte etwa so aussehen:

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

Daten mit schreibgeschützten Transaktionen abrufen

Angenommen, Sie möchten mehr als einen Lesevorgang mit demselben Zeitstempel ausführen. Bei schreibgeschützten Transaktionen wird ein gleichbleibendes Präfix des Commit-Verlaufs der Transaktionen beibehalten, damit die Anwendung immer konsistente Daten erhält. Zum Ausführen schreibgeschützter Transaktionen verwenden Sie ein ReadOnlyTransaction-Objekt . Verwenden Sie die Methode readOnlyTransaction() der Klasse DatabaseClient, um ein ReadOnlyTransaction-Objekt zu erhalten.

So werden eine Abfrage und ein Lesevorgang in derselben schreibgeschützten Transaktion ausgeführt:

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));
      }
    }
  }
}

Führen Sie das Beispiel mit dem Argument readonlytransaction aus.

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

Die Ausgabe sollte etwa so aussehen:

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

Bereinigen

Löschen Sie die Datenbank und die erstellte Instanz, um zu vermeiden, dass Ihrem Cloud-Rechnungskonto die in dieser Anleitung verwendeten Ressourcen in Rechnung gestellt werden.

Datenbank löschen

Wenn Sie eine Instanz löschen, werden alle darin enthaltenen Datenbanken automatisch gelöscht. In diesem Schritt wird gezeigt, wie eine Datenbank gelöscht wird, ohne eine Instanz zu löschen (dabei fallen weiterhin Gebühren für die Instanz an).

Über die Befehlszeile

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

Google Cloud Console verwenden

  1. Rufen Sie in der Google Cloud Console die Seite Spanner-Instanzen auf.

    Zur Seite "VM-Instanzen"

  2. Klicken Sie auf die Instanz.

  3. Klicken Sie auf die Datenbank, die Sie löschen möchten.

  4. Klicken Sie auf der Seite Datenbankdetails auf Löschen.

  5. Bestätigen Sie, dass die Datenbank gelöscht werden soll, und klicken Sie auf Löschen.

Instanz löschen

Beim Löschen einer Instanz werden alle Datenbanken, die in der Instanz erstellt wurden, automatisch gelöscht.

Über die Befehlszeile

gcloud spanner instances delete test-instance

Google Cloud Console verwenden

  1. Rufen Sie in der Google Cloud Console die Seite Spanner-Instanzen auf.

    Zur Seite "VM-Instanzen"

  2. Klicken Sie auf die Instanz.

  3. Klicken Sie auf Löschen.

  4. Bestätigen Sie, dass die Instanz gelöscht werden soll, und klicken Sie auf Löschen.

Nächste Schritte