Cloud Spanner: Gaming-Bestenliste mit Go erstellen

1. Übersicht

Google Cloud Spanner ist ein vollständig verwalteter, horizontal skalierbarer, global verteilter, relationaler Datenbankdienst, der ACID-Transaktionen und SQL-Semantik bietet, ohne Leistung und Hochverfügbarkeit zu beeinträchtigen.

In diesem Lab erfahren Sie, wie Sie eine Cloud Spanner-Instanz einrichten. Sie durchlaufen die Schritte zum Erstellen einer Datenbank und eines Schemas, die für eine Bestenliste für Spiele verwendet werden können. Zuerst erstellen Sie eine Tabelle „Players“ zum Speichern von Spielerinformationen und eine Tabelle „Scores“ zum Speichern von Spielergebnissen.

Als Nächstes füllen Sie die Tabellen mit Beispieldaten. Zum Schluss führen Sie einige Beispielabfragen für die zehn wichtigsten Elemente aus und löschen dann die Instanz, um Ressourcen freizugeben.

Lerninhalte

  • Cloud Spanner-Instanz einrichten
  • Datenbank und Tabellen erstellen
  • Commit-Zeitstempelspalte verwenden
  • Daten mit Zeitstempeln in die Cloud Spanner-Datenbanktabelle laden
  • Cloud Spanner-Datenbank abfragen
  • Cloud Spanner-Instanz löschen

W****as Sie benötigen

Wie werden Sie diese Anleitung verwenden?

Nur lesen Lesen und Übungen durchführen

Wie würden Sie Ihre Erfahrung mit der Google Cloud Platform bewerten?

Anfänger Mittelstufe Fortgeschritten

2. Einrichtung und Anforderungen

Umgebung zum selbstbestimmten Lernen einrichten

Wenn Sie noch kein Google-Konto (Gmail oder Google Apps) haben, müssen Sie eines erstellen. Melden Sie sich in der Google Cloud Console ( console.cloud.google.com) an und erstellen Sie ein neues Projekt.

Wenn Sie bereits ein Projekt haben, klicken Sie oben links in der Console auf das Drop-down-Menü zur Projektauswahl:

6c9406d9b014760.png

Klicken Sie im angezeigten Dialogfeld auf die Schaltfläche „NEUES PROJEKT“, um ein neues Projekt zu erstellen:

f708315ae07353d0.png

Wenn Sie noch kein Projekt haben, wird ein Dialogfeld wie das folgende angezeigt, in dem Sie Ihr erstes Projekt erstellen können:

870a3cbd6541ee86.png

Im nachfolgenden Dialogfeld zum Erstellen von Projekten können Sie die Details Ihres neuen Projekts eingeben:

6a92c57d3250a4b3.png

Merken Sie sich die Projekt-ID. Sie ist für alle Google Cloud-Projekte ein eindeutiger Name. Der Name oben ist bereits vergeben und kann nicht verwendet werden. Sie wird später in diesem Codelab als PROJECT_ID bezeichnet.

Als Nächstes müssen Sie, falls noch nicht geschehen, die Abrechnung in der Entwicklerkonsole aktivieren, um Google Cloud-Ressourcen verwenden zu können, und die Cloud Spanner API aktivieren.

15d0ef27a8fbab27.png

Dieses Codelab sollte Sie nicht mehr als ein paar Dollar kosten, aber es könnte mehr sein, wenn Sie sich für mehr Ressourcen entscheiden oder wenn Sie sie laufen lassen (siehe Abschnitt „Bereinigen“ am Ende dieses Dokuments). Die Preise für Google Cloud Spanner sind hier dokumentiert.

Neuen Nutzern der Google Cloud Platform steht eine kostenlose Testversion mit einem Guthaben von 300$ zur Verfügung. Dieses Codelab sollte damit vollständig kostenlos sein.

Google Cloud Shell einrichten

Während Sie Google Cloud und Spanner von Ihrem Laptop aus per Fernzugriff nutzen können, wird in diesem Codelab Google Cloud Shell verwendet, eine Befehlszeilenumgebung, die in der Cloud ausgeführt wird.

Diese Debian-basierte virtuelle Maschine verfügt über alle Entwicklungstools, die Sie benötigen. Sie bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und läuft in Google Cloud, was die Netzwerkleistung und Authentifizierung erheblich verbessert. Für dieses Codelab benötigen Sie also nur einen Browser (es funktioniert auch auf einem Chromebook).

  1. Klicken Sie zum Aktivieren von Cloud Shell in der Cloud Console einfach auf Cloud Shell aktivieren a8460e837e9f5fda.png. Die Bereitstellung und Verbindung mit der Umgebung sollte nur wenige Augenblicke dauern.

b532b2f19ab85dda.png

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

Sobald die Verbindung mit der Cloud Shell hergestellt ist, sehen Sie, dass Sie bereits authentifiziert sind und für das Projekt schon Ihre PROJECT_ID eingestellt ist.

gcloud auth list

Befehlsausgabe

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

Befehlsausgabe

[core]
project = <PROJECT_ID>

Wenn das Projekt aus irgendeinem Grund nicht festgelegt ist, führen Sie einfach den folgenden Befehl aus:

gcloud config set project <PROJECT_ID>

Suchst du nach deinem PROJECT_ID? Sehen Sie nach, welche ID Sie in den Einrichtungsschritten verwendet haben, oder suchen Sie sie im Cloud Console-Dashboard:

2485e00c1223af09.png

In Cloud Shell werden auch einige Umgebungsvariablen standardmäßig festgelegt, die für zukünftige Befehle nützlich sein können.

echo $GOOGLE_CLOUD_PROJECT

Befehlsausgabe

<PROJECT_ID>
  1. Legen Sie zum Schluss die Standardzone und die Projektkonfiguration fest.
gcloud config set compute/zone us-central1-f

Sie können verschiedene Zonen auswählen. Weitere Informationen finden Sie unter Regionen und Zonen.

Zusammenfassung

In diesem Schritt richten Sie Ihre Umgebung ein.

Als Nächstes

Als Nächstes richten Sie eine Cloud Spanner-Instanz ein.

3. Cloud Spanner-Instanz einrichten

In diesem Schritt richten wir unsere Cloud Spanner-Instanz für dieses Codelab ein. Suchen Sie nach dem Spanner-Eintrag 1a6580bd3d3e6783.pngim Dreistrich-Menü oben links 3129589f7bc9e5ce.png oder suchen Sie nach Spanner, indem Sie „/“ drücken und „Spanner“ eingeben.

36e52f8df8e13b99.png

Klicken Sie dann auf 19bb9864067757cb.png und füllen Sie das Formular aus, indem Sie den Instanznamen cloudspanner-leaderboard für Ihre Instanz eingeben, eine Konfiguration auswählen (regionale Instanz) und die Anzahl der Knoten festlegen. Für dieses Codelab benötigen wir nur einen Knoten. Für Produktionsinstanzen und um das Cloud Spanner-SLA zu erfüllen, müssen Sie mindestens drei Knoten in Ihrer Cloud Spanner-Instanz ausführen.

Klicken Sie abschließend auf „Erstellen“. Innerhalb von Sekunden steht Ihnen eine Cloud Spanner-Instanz zur Verfügung.

dceb68e9ed3801e8.png

Im nächsten Schritt verwenden wir die Go-Clientbibliothek, um eine Datenbank und ein Schema in unserer neuen Instanz zu erstellen.

4. Datenbank und Schema erstellen

In diesem Schritt erstellen wir unsere Beispieldatenbank und unser Schema.

Wir verwenden die Go-Clientbibliothek, um zwei Tabellen zu erstellen: eine „Players“-Tabelle für Spielerinformationen und eine „Scores“-Tabelle zum Speichern von Spielergebnissen. Dazu erstellen wir eine Go-Konsolenanwendung in Cloud Shell.

Klonen Sie zuerst den Beispielcode für dieses Codelab von GitHub, indem Sie den folgenden Befehl in Cloud Shell eingeben:

export GO111MODULE=auto
go get -u github.com/GoogleCloudPlatform/golang-samples/spanner/...

Wechseln Sie dann in das Verzeichnis „leaderboard“, in dem Sie Ihre Anwendung erstellen.

cd gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/spanner_leaderboard

Der gesamte für dieses Codelab erforderliche Code befindet sich im vorhandenen Verzeichnis golang-samples/spanner/spanner_leaderboard/ als ausführbare Go-Anwendung mit dem Namen leaderboard. Sie dient als Referenz, während Sie das Codelab durcharbeiten. Wir erstellen ein neues Verzeichnis und erstellen nach und nach eine Kopie der Bestenlistenanwendung.

Erstellen Sie mit dem folgenden Befehl ein neues Verzeichnis mit dem Namen „codelab“ für die Anwendung und wechseln Sie dorthin:

mkdir codelab && cd $_

Erstellen Sie nun eine einfache Go-Anwendung mit dem Namen „Leaderboard“, in der die Spanner-Clientbibliothek verwendet wird, um eine Bestenliste mit zwei Tabellen zu erstellen: „Players“ und „Scores“. Das können Sie direkt im Cloud Shell-Editor tun:

Öffnen Sie den Cloud Shell-Editor, indem Sie auf das unten hervorgehobene Symbol „Editor öffnen“ klicken:

7519d016b96ca51b.png

Erstellen Sie im Ordner „~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab“ eine Datei mit dem Namen „leaderboard.go“.

  • Achten Sie zuerst darauf, dass der Ordner „codelab“ in der Ordnerliste des Cloud Shell-Editors ausgewählt ist.
  • Wählen Sie dann im Menü „Datei“ des Cloud Shell-Editors „Neue Datei“ aus.
  • Geben Sie „leaderboard.go“ als Namen für die neue Datei ein.

Dies ist die Hauptdatei der Anwendung, die unseren Anwendungscode und Verweise zur Einbeziehung von Abhängigkeiten enthält.

Um die Datenbank leaderboard und die Tabellen Players und Scores zu erstellen, kopieren Sie den folgenden Go-Code (Strg + C) und fügen Sie ihn in die Datei leaderboard.go ein (Strg + V):

package main

import (
        "context"
        "flag"
        "fmt"
        "io"
        "log"
        "os"
        "regexp"
        "time"

        "cloud.google.com/go/spanner"
        database "cloud.google.com/go/spanner/admin/database/apiv1"

        adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)

type adminCommand func(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, database string) error


func createDatabase(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, db string) error {
        matches := regexp.MustCompile("^(.*)/databases/(.*)$").FindStringSubmatch(db)
        if matches == nil || len(matches) != 3 {
                return fmt.Errorf("Invalid database id %s", db)
        }
        op, err := adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
                Parent:          matches[1],
                CreateStatement: "CREATE DATABASE `" + matches[2] + "`",
                ExtraStatements: []string{
                        `CREATE TABLE Players(
                            PlayerId INT64 NOT NULL,
                            PlayerName STRING(2048) NOT NULL
                        ) PRIMARY KEY(PlayerId)`,
                        `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`,
                },
        })
        if err != nil {
                return err
        }
        if _, err := op.Wait(ctx); err != nil {
                return err
        }
        fmt.Fprintf(w, "Created database [%s]\n", db)
        return nil
}

func createClients(ctx context.Context, db string) (*database.DatabaseAdminClient, *spanner.Client) {
        adminClient, err := database.NewDatabaseAdminClient(ctx)
        if err != nil {
                log.Fatal(err)
        }

        dataClient, err := spanner.NewClient(ctx, db)
        if err != nil {
                log.Fatal(err)
        }

        return adminClient, dataClient
}

func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
        cmd string, db string, timespan int) error {
        // createdatabase command
        if cmd == "createdatabase" {
                err := createDatabase(ctx, w, adminClient, db)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }
        return nil
}

func main() {
        flag.Usage = func() {
                fmt.Fprintf(os.Stderr, `Usage: leaderboard <command> <database_name> [command_option]

        Command can be one of: createdatabase

Examples:
        leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
                - Create a sample Cloud Spanner database along with sample tables in your project.
`)
        }

        flag.Parse()
        flagCount := len(flag.Args())
        if flagCount != 2 {
                flag.Usage()
                os.Exit(2)
        }

        cmd, db := flag.Arg(0), flag.Arg(1)
        // Set timespan to zero, as it's not currently being used
        var timespan int = 0

        ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
        defer cancel()
        adminClient, dataClient := createClients(ctx, db)
        if err := run(ctx, adminClient, dataClient, os.Stdout, cmd, db, timespan); err != nil {
                os.Exit(1)
        }
}

Speichern Sie die Änderungen, die Sie an der Datei leaderboard.go vorgenommen haben, indem Sie im Cloud Shell-Editor im Menü „Datei“ die Option „Speichern“ auswählen.

In der Datei leaderboard.go im Verzeichnis golang-samples/spanner/spanner_leaderboard finden Sie ein Beispiel dafür, wie Ihre leaderboard.go-Datei aussehen sollte, nachdem Sie den Code zum Aktivieren des Befehls createdatabase hinzugefügt haben.

Führen Sie zum Erstellen Ihrer App in Cloud Shell den Befehl „go build“ im Verzeichnis codelab aus, in dem sich Ihre leaderboard.go-Datei befindet:

go build leaderboard.go

Nachdem Ihre Anwendung erfolgreich erstellt wurde, führen Sie sie in Cloud Shell aus, indem Sie den folgenden Befehl eingeben:

./leaderboard

Die Ausgabe sollte etwa so aussehen:

Usage: leaderboard <command> <database_name> [command_option]

        Command can be one of: createdatabase

Examples:
        leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
                - Create a sample Cloud Spanner database along with sample tables in your project.

Aus dieser Antwort geht hervor, dass es sich um die Anwendung Leaderboard handelt, die derzeit einen möglichen Befehl hat: createdatabase. Wir sehen, dass das erwartete Argument für den Befehl createdatabase ein String mit einer bestimmten Instanz-ID und Datenbank-ID ist.

Führen Sie nun den folgenden Befehl aus. Achten Sie darauf, my-project durch die Projekt-ID zu ersetzen, die Sie am Anfang dieses Codelabs erstellt haben.

./leaderboard createdatabase projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard

Nach einigen Sekunden sollte eine Antwort wie die folgende angezeigt werden:

Created database [projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard] 

Im Bereich Cloud Spanner-Datenbanken – Übersicht der Cloud Console sollten Ihre neue Datenbank und die neuen Tabellen im Menü auf der linken Seite angezeigt werden.

a12fa65e352836b1.png

Im nächsten Schritt aktualisieren wir unsere Anwendung, um einige Daten in Ihre neue Datenbank zu laden.

5. Daten laden

Wir haben jetzt eine Datenbank mit dem Namen leaderboard, die zwei Tabellen enthält: Players und Scores. Jetzt verwenden wir die Go-Clientbibliothek, um die Tabelle Players mit Spielern und die Tabelle Scores mit zufälligen Punktzahlen für jeden Spieler zu füllen.

Öffnen Sie den Cloud Shell-Editor, falls er noch nicht geöffnet ist. Klicken Sie dazu auf das unten hervorgehobene Symbol:

7519d016b96ca51b.png

Bearbeiten Sie als Nächstes die Datei leaderboard.go im Cloud Shell-Editor, um den Befehl insertplayers hinzuzufügen, mit dem 100 Spieler in die Tabelle Players eingefügt werden können. Außerdem fügen wir den Befehl insertscores hinzu, mit dem für jeden Spieler in der Tabelle Players vier zufällige Ergebnisse in die Tabelle Scores eingefügt werden können.

Aktualisieren Sie zuerst den imports-Abschnitt oben in der Datei leaderboard.go. Ersetzen Sie den aktuellen Inhalt durch Folgendes:

import (
        "context"
        "flag"
        "fmt"
        "io"
        "log"
        "math/rand"
        "os"
        "regexp"
        "time"

        "cloud.google.com/go/spanner"
        database "cloud.google.com/go/spanner/admin/database/apiv1"

        "google.golang.org/api/iterator"
        adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)

Fügen Sie als Nächstes oben in der Datei, direkt unter der Zeile, die mit „type adminCommand …“ beginnt, einen neuen Befehlstyp zusammen mit einer Liste von Befehlen hinzu. Wenn Sie fertig sind, sollte es so aussehen:

type adminCommand func(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, database string) error

type command func(ctx context.Context, w io.Writer, client *spanner.Client) error
var (
        commands = map[string]command{
                "insertplayers": insertPlayers,
                "insertscores":  insertScores,
        }
)

Fügen Sie als Nächstes die Funktionen „insertPlayers“ und „insertScores“ unter der vorhandenen Funktion createdatabase() hinzu:

func insertPlayers(ctx context.Context, w io.Writer, client *spanner.Client) error {
        // Get number of players to use as an incrementing value for each PlayerName to be inserted
        stmt := spanner.Statement{
                SQL: `SELECT Count(PlayerId) as PlayerCount FROM Players`,
        }
        iter := client.Single().Query(ctx, stmt)
        defer iter.Stop()
        row, err := iter.Next()
        if err != nil {
                return err
        }
        var numberOfPlayers int64 = 0
        if err := row.Columns(&numberOfPlayers); err != nil {
                return err
        }
        // Initialize values for random PlayerId
        rand.Seed(time.Now().UnixNano())
        min := 1000000000
        max := 9000000000
        // Insert 100 player records into the Players table
        _, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
                stmts := []spanner.Statement{}
                for i := 1; i <= 100; i++ {
                        numberOfPlayers++
                        playerID := rand.Intn(max-min) + min
                        playerName := fmt.Sprintf("Player %d", numberOfPlayers)
                        stmts = append(stmts, spanner.Statement{
                                SQL: `INSERT INTO Players
                                                (PlayerId, PlayerName)
                                                VALUES (@playerID, @playerName)`,
                                Params: map[string]interface{}{
                                        "playerID":   playerID,
                                        "playerName": playerName,
                                },
                        })
                }
                _, err := txn.BatchUpdate(ctx, stmts)
                if err != nil {
                        return err
                }
                return nil
        })
        fmt.Fprintf(w, "Inserted players \n")
        return nil
}

func insertScores(ctx context.Context, w io.Writer, client *spanner.Client) error {
        playerRecordsFound := false
        // Create slice for insert statements
        stmts := []spanner.Statement{}
        // Select all player records
        stmt := spanner.Statement{SQL: `SELECT PlayerId FROM Players`}
        iter := client.Single().Query(ctx, stmt)
        defer iter.Stop()
        // Insert 4 score records into the Scores table for each player in the Players table
        for {
                row, err := iter.Next()
                if err == iterator.Done {
                        break
                }
                if err != nil {
                        return err
                }
                playerRecordsFound = true
                var playerID int64
                if err := row.ColumnByName("PlayerId", &playerID); err != nil {
                        return err
                }
                // Initialize values for random score and date
                rand.Seed(time.Now().UnixNano())
                min := 1000
                max := 1000000
                for i := 0; i < 4; i++ {
                        // Generate random score between 1,000 and 1,000,000
                        score := rand.Intn(max-min) + min
                        // Generate random day within the past two years
                        now := time.Now()
                        endDate := now.Unix()
                        past := now.AddDate(0, -24, 0)
                        startDate := past.Unix()
                        randomDateInSeconds := rand.Int63n(endDate-startDate) + startDate
                        randomDate := time.Unix(randomDateInSeconds, 0)
                        // Add insert statement to stmts slice
                        stmts = append(stmts, spanner.Statement{
                                SQL: `INSERT INTO Scores
                                                (PlayerId, Score, Timestamp)
                                                VALUES (@playerID, @score, @timestamp)`,
                                Params: map[string]interface{}{
                                        "playerID":  playerID,
                                        "score":     score,
                                        "timestamp": randomDate,
                                },
                        })
                }

        }
        if !playerRecordsFound {
                fmt.Fprintln(w, "No player records currently exist. First insert players then insert scores.")
        } else {
                _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
                        // Commit insert statements for all scores to be inserted as a single transaction
                        _, err := txn.BatchUpdate(ctx, stmts)
                        return err
                })
                if err != nil {
                        return err
                }
                fmt.Fprintln(w, "Inserted scores")
        }
        return nil
}

Damit der Befehl insert funktioniert, fügen Sie den folgenden Code in die Funktion „run“ Ihrer Anwendung unter der Anweisung zur Verarbeitung von createdatabase ein und ersetzen Sie die Anweisung return nil :

        // insert and query commands
        cmdFn := commands[cmd]
        if cmdFn == nil {
                flag.Usage()
                os.Exit(2)
        }
        err := cmdFn(ctx, w, dataClient)
        if err != nil {
                fmt.Fprintf(w, "%s failed with %v", cmd, err)
        }
        return err

Wenn Sie fertig sind, sollte die Funktion run so aussehen:

func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
        cmd string, db string, timespan int) error {
        // createdatabase command
        if cmd == "createdatabase" {
                err := createDatabase(ctx, w, adminClient, db)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }

        // insert and query commands
        cmdFn := commands[cmd]
        if cmdFn == nil {
                flag.Usage()
                os.Exit(2)
        }
        err := cmdFn(ctx, w, dataClient)
        if err != nil {
                fmt.Fprintf(w, "%s failed with %v", cmd, err)
        }
        return err
}

Der letzte Schritt, um die „insert“-Funktion in Ihre Anwendung einzufügen, besteht darin, der flag.Usage()-Funktion Hilfetext für die Befehle „insertplayers“ und „insertscores“ hinzuzufügen. Fügen Sie der Funktion flag.Usage() den folgenden Hilfetext hinzu, um Hilfetext für die Einfügebefehle einzufügen:

Fügen Sie die beiden Befehle der Liste der möglichen Befehle hinzu:

Command can be one of: createdatabase, insertplayers, insertscores

Fügen Sie diesen zusätzlichen Hilfetext unter dem Hilfetext für den Befehl createdatabase ein.

        leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db
                - Insert 100 sample Player records into the database.
        leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db
                - Insert sample score data into Scores sample Cloud Spanner database table.

Speichern Sie die Änderungen, die Sie an der Datei leaderboard.go vorgenommen haben, indem Sie im Cloud Shell-Editor im Menü „Datei“ die Option „Speichern“ auswählen.

In der Datei leaderboard.go im Verzeichnis golang-samples/spanner/spanner_leaderboard finden Sie ein Beispiel dafür, wie Ihre leaderboard.go-Datei aussehen sollte, nachdem Sie den Code zum Aktivieren der Befehle insertplayers und insertscores hinzugefügt haben.

Erstellen und führen Sie die Anwendung nun aus, um zu bestätigen, dass die neuen Befehle insertplayers und insertscores in der Liste der möglichen Befehle der Anwendung enthalten sind. Führen Sie den folgenden Befehl aus, um die Anwendung zu erstellen:

go build leaderboard.go

Führen Sie die resultierende Anwendung in Cloud Shell aus, indem Sie den folgenden Befehl eingeben:

./leaderboard

Die Befehle insertplayers und insertscores sollten jetzt in der Standardausgabe der Anwendung enthalten sein:

Usage: leaderboard <command> <database_name> [command_option]

        Command can be one of: createdatabase, insertplayers, insertscores

Examples:
        leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
                - Create a sample Cloud Spanner database along with sample tables in your project.
        leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db
                - Insert 100 sample Player records into the database.
        leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db
                - Insert sample score data into Scores sample Cloud Spanner database table.

Führen wir nun den Befehl insertplayers mit denselben Argumentwerten aus, die wir beim Aufrufen des Befehls createdatabase verwendet haben. Achten Sie darauf, my-project durch die Projekt-ID zu ersetzen, die Sie am Anfang dieses Codelabs erstellt haben.

./leaderboard insertplayers projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard

Nach einigen Sekunden sollte eine Antwort wie die folgende angezeigt werden:

Inserted players

Nun verwenden wir die Go-Clientbibliothek, um die Tabelle Scores mit vier zufälligen Punktzahlen sowie Zeitstempeln für jeden Spieler in der Tabelle Players zu füllen.

Die Spalte Timestamp der Tabelle Scores wurde mit der folgenden SQL-Anweisung, die beim vorherigen Ausführen des Befehls create ausgeführt wurde, als „Commit-Zeitstempel“-Spalte definiert:

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

Beachten Sie das Attribut OPTIONS(allow_commit_timestamp=true). Dadurch wird Timestamp zu einer „Commit-Zeitstempel“-Spalte, die bei INSERT- und UPDATE-Vorgängen für eine bestimmte Tabellenzeile automatisch mit dem genauen Transaktionszeitstempel gefüllt werden kann.

Sie können auch eigene Zeitstempelwerte in eine Commit-Zeitstempelspalte einfügen, sofern Sie einen Zeitstempel mit einem Wert in der Vergangenheit einfügen. Das werden wir in diesem Codelab tun.

Führen wir nun den Befehl insertscores mit denselben Argumentwerten aus, die wir beim Aufrufen des Befehls insertplayers verwendet haben. Achten Sie darauf, my-project durch die Projekt-ID zu ersetzen, die Sie am Anfang dieses Codelabs erstellt haben.

./leaderboard insertscores projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard

Nach einigen Sekunden sollte eine Antwort wie die folgende angezeigt werden:

Inserted scores

Beim Ausführen der Funktion insertScores wird das folgende Code-Snippet verwendet, um einen zufällig generierten Zeitstempel mit einem Datum und einer Uhrzeit in der Vergangenheit einzufügen:

now := time.Now()
endDate := now.Unix()
past := now.AddDate(0, -24, 0)
startDate := past.Unix()
randomDateInSeconds := rand.Int63n(endDate-startDate) + startDate
randomDate := time.Unix(randomDateInSeconds, 0)
stmts = append(stmts, spanner.Statement{
        SQL: `INSERT INTO Scores
              (PlayerId, Score, Timestamp)
                 VALUES (@playerID, @score, @timestamp)`,
        Params: map[string]interface{}{
                "playerID":  playerID,
                "score":     score,
                "timestamp": randomDate,
        },
})

Wenn Sie die Spalte Timestamp automatisch mit dem Zeitstempel der „Insert“-Transaktion füllen möchten, können Sie stattdessen die Go-Konstante spanner.CommitTimestamp einfügen, wie im folgenden Code-Snippet:

...
stmts = append(stmts, spanner.Statement{
        SQL: `INSERT INTO Scores
              (PlayerId, Score, Timestamp)
                 VALUES (@playerID, @score, @timestamp)`,
        Params: map[string]interface{}{
                "playerID":  playerID,
                "score":     score,
                "timestamp": spanner.CommitTimestamp,
        },
})

Nachdem wir die Daten geladen haben, prüfen wir die Werte, die wir gerade in unsere neuen Tabellen geschrieben haben. Rufen Sie dazu in der Cloud Console den Cloud Spanner-Bereich auf. Wählen Sie zuerst die Datenbank leaderboard und dann die Tabelle Players aus. Klicken Sie auf den Tab Data. Sie sollten sehen, dass die Spalten PlayerId und PlayerName der Tabelle Daten enthalten.

86dc5b927809a4ec.png

Als Nächstes prüfen wir, ob die Tabelle „Scores“ auch Daten enthält. Klicken Sie dazu auf die Tabelle Scores und wählen Sie den Tab Data aus. Sie sollten sehen, dass die Spalten PlayerId, Timestamp und Score der Tabelle Daten enthalten.

87c8610c92d3c612.png

Gut gemacht! Wir aktualisieren unsere App, um einige Abfragen auszuführen, mit denen wir eine Bestenliste für Spiele erstellen können.

6. Bestenlistenabfragen ausführen

Nachdem wir unsere Datenbank eingerichtet und Informationen in unsere Tabellen geladen haben, erstellen wir nun eine Bestenliste mit diesen Daten. Dazu müssen wir die folgenden vier Fragen beantworten:

  1. Welche Spieler gehören zu den „Top 10“ aller Zeiten?
  2. Welche Spieler gehören zu den „Top Ten“ des Jahres?
  3. Welche Spieler gehören zu den Top 10 des Monats?
  4. Welche Spieler gehören zu den „Top Ten“ der Woche?

Wir aktualisieren unsere Anwendung, damit die SQL-Abfragen ausgeführt werden, mit denen diese Fragen beantwortet werden.

Wir fügen einen query-Befehl und einen queryWithTimespan-Befehl hinzu, mit denen die Abfragen ausgeführt werden können, um die Fragen zu beantworten, die die für unsere Bestenliste erforderlichen Informationen liefern.

Bearbeiten Sie die Datei leaderboard.go im Cloud Shell-Editor, um die Anwendung zu aktualisieren und einen query-Befehl und einen queryWithTimespan-Befehl hinzuzufügen. Außerdem fügen wir eine formatWithCommas-Hilfsfunktion hinzu, um unsere Werte mit Kommas zu formatieren.

Aktualisieren Sie zuerst den imports-Abschnitt oben in der Datei leaderboard.go. Ersetzen Sie den aktuellen Inhalt durch Folgendes:

import (
        "bytes"
        "context"
        "flag"
        "fmt"
        "io"
        "log"
        "math/rand"
        "os"
        "regexp"
        "strconv"
        "time"

        "cloud.google.com/go/spanner"
        database "cloud.google.com/go/spanner/admin/database/apiv1"

        "google.golang.org/api/iterator"
        adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)

Fügen Sie als Nächstes die folgenden beiden Funktionen und die Hilfsfunktion unter der vorhandenen insertScores-Methode hinzu:

func query(ctx context.Context, w io.Writer, client *spanner.Client) error {
        stmt := spanner.Statement{
                SQL: `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`}
        iter := client.Single().Query(ctx, stmt)
        defer iter.Stop()
        for {
                row, err := iter.Next()
                if err == iterator.Done {
                        return nil
                }
                if err != nil {
                        return err
                }
                var playerID, score int64
                var playerName string
                var timestamp time.Time
                if err := row.Columns(&playerID, &playerName, &score, &timestamp); err != nil {
                        return err
                }
                fmt.Fprintf(w, "PlayerId: %d  PlayerName: %s  Score: %s  Timestamp: %s\n",
                        playerID, playerName, formatWithCommas(score), timestamp.String()[0:10])
        }
}

func queryWithTimespan(ctx context.Context, w io.Writer, client *spanner.Client, timespan int) error {
        stmt := spanner.Statement{
                SQL: `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`,
                Params: map[string]interface{}{"Timespan": timespan},
        }
        iter := client.Single().Query(ctx, stmt)
        defer iter.Stop()
        for {
                row, err := iter.Next()
                if err == iterator.Done {
                        return nil
                }
                if err != nil {
                        return err
                }
                var playerID, score int64
                var playerName string
                var timestamp time.Time
                if err := row.Columns(&playerID, &playerName, &score, &timestamp); err != nil {
                        return err
                }
                fmt.Fprintf(w, "PlayerId: %d  PlayerName: %s  Score: %s  Timestamp: %s\n",
                        playerID, playerName, formatWithCommas(score), timestamp.String()[0:10])
        }
}

func formatWithCommas(n int64) string {
        numberAsString := strconv.FormatInt(n, 10)
        numberLength := len(numberAsString)
        if numberLength < 4 {
                return numberAsString
        }
        var buffer bytes.Buffer
        comma := []rune(",")
        bufferPosition := numberLength % 3
        if (bufferPosition) > 0 {
                bufferPosition = 3 - bufferPosition
        }
        for i := 0; i < numberLength; i++ {
                if bufferPosition == 3 {
                        buffer.WriteRune(comma[0])
                        bufferPosition = 0
                }
                bufferPosition++
                buffer.WriteByte(numberAsString[i])
        }
        return buffer.String()
}

Fügen Sie als Nächstes oben in der Datei leaderboard.go „query“ als eine der Befehlsoptionen in der Variablen commands direkt unter der Option insertscores": insertScores ein, sodass die Variable commands so aussieht:

var (
        commands = map[string]command{
                "insertplayers": insertPlayers,
                "insertscores":  insertScores,
                "query":         query,
        }
)

Fügen Sie als Nächstes „queryWithTimespan“ als Befehlsoption in die Funktion run ein, unter dem Befehlsabschnitt „createdatabase“ und über dem Abschnitt für die Verarbeitung der Befehle „insert and query“:

        // querywithtimespan command
        if cmd == "querywithtimespan" {
                err := queryWithTimespan(ctx, w, dataClient, timespan)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }

Wenn Sie fertig sind, sollte die Funktion run so aussehen:

func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
        cmd string, db string, timespan int) error {
        // createdatabase command
        if cmd == "createdatabase" {
                err := createDatabase(ctx, w, adminClient, db)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }

        // querywithtimespan command
        if cmd == "querywithtimespan" {
                if timespan == 0 {
                        flag.Usage()
                        os.Exit(2)
                }
                err := queryWithTimespan(ctx, w, dataClient, timespan)
                if err != nil {
                        fmt.Fprintf(w, "%s failed with %v", cmd, err)
                }
                return err
        }

        // insert and query commands
        cmdFn := commands[cmd]
        if cmdFn == nil {
                flag.Usage()
                os.Exit(2)
        }
        err := cmdFn(ctx, w, dataClient)
        if err != nil {
                fmt.Fprintf(w, "%s failed with %v", cmd, err)
        }
        return err
}

Damit der Befehl queryWithTimespan funktioniert, aktualisieren Sie den Codeblock „flag.Parse()“ in der „main“-Methode Ihrer Anwendung so, dass er wie folgt aussieht:

        flag.Parse()
        flagCount := len(flag.Args())
        if flagCount < 2 || flagCount > 3 {
                flag.Usage()
                os.Exit(2)
        }

        cmd, db := flag.Arg(0), flag.Arg(1)
        // If query timespan flag is specified, parse to int
        var timespan int = 0
        if flagCount == 3 {
                parsedTimespan, err := strconv.Atoi(flag.Arg(2))
                if err != nil {
                        fmt.Println(err)
                        os.Exit(2)
                }
                timespan = parsedTimespan
        }

        ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
        defer cancel()
        adminClient, dataClient := createClients(ctx, db)
        if err := run(ctx, adminClient, dataClient, os.Stdout, cmd, db, timespan); err != nil {
                os.Exit(1)
        }

Der letzte Schritt, um Ihrer Anwendung die Funktion „query“ hinzuzufügen, besteht darin, der flag.Usage()-Funktion Hilfetext für die Befehle „query“ und „querywithtimespan“ hinzuzufügen. Fügen Sie der Funktion flag.Usage() die folgenden Codezeilen hinzu, um Hilfetext für die Abfragebefehle einzufügen:

Fügen Sie der Liste der möglichen Befehle die beiden „query“-Befehle hinzu:

Command can be one of: createdatabase, insertplayers, insertscores, query, querywithtimespan

Fügen Sie diesen zusätzlichen Hilfetext unter dem Hilfetext für den Befehl insertscores ein.

        leaderboard query projects/my-project/instances/my-instance/databases/example-db
                - Query players with top ten scores of all time.
        leaderboard querywithtimespan projects/my-project/instances/my-instance/databases/example-db 168
                - Query players with top ten scores within a timespan specified in hours.

Speichern Sie die Änderungen, die Sie an der Datei leaderboard.go vorgenommen haben, indem Sie im Cloud Shell-Editor im Menü „Datei“ die Option „Speichern“ auswählen.

In der Datei leaderboard.go im Verzeichnis golang-samples/spanner/spanner_leaderboard finden Sie ein Beispiel dafür, wie Ihre leaderboard.go-Datei aussehen sollte, nachdem Sie den Code zum Aktivieren der Befehle query und querywithtimespan hinzugefügt haben.

Erstellen und führen Sie die Anwendung nun aus, um zu bestätigen, dass die neuen Befehle query und querywithtimespan in der Liste der möglichen Befehle der Anwendung enthalten sind.

Führen Sie den folgenden Befehl in Cloud Shell aus, um die Anwendung zu erstellen:

go build leaderboard.go

Führen Sie die resultierende Anwendung in Cloud Shell aus, indem Sie den folgenden Befehl eingeben:

./leaderboard

Die Befehle query und querywithtimespan sollten jetzt als neue Befehlsoption in der Standardausgabe der App enthalten sein:

Usage: leaderboard <command> <database_name> [command_option]

        Command can be one of: createdatabase, insertplayers, insertscores, query, querywithtimespan

Examples:
        leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
                - Create a sample Cloud Spanner database along with sample tables in your project.
        leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db
                - Insert 100 sample Player records into the database.
        leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db
                - Insert sample score data into Scores sample Cloud Spanner database table.
        leaderboard query projects/my-project/instances/my-instance/databases/example-db
                - Query players with top ten scores of all time.
        leaderboard querywithtimespan projects/my-project/instances/my-instance/databases/example-db 168
                - Query players with top ten scores within a timespan specified in hours.

Wie Sie in der Antwort sehen, können wir mit dem Befehl query eine Liste der zehn besten Spieler aller Zeiten abrufen. Wir sehen auch, dass wir mit dem Befehl querywithtimespan einen Zeitraum in Stunden angeben können, der zum Filtern von Datensätzen basierend auf ihrem Wert in der Spalte Timestamp der Tabelle Scores verwendet werden soll.

Führen wir den Befehl query mit denselben Argumentwerten aus, die wir beim Ausführen des Befehls create verwendet haben. Achten Sie darauf, my-project durch die Projekt-ID zu ersetzen, die Sie am Anfang dieses Codelabs erstellt haben.

./leaderboard query  projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard

Sie sollten eine Antwort mit den zehn besten Spielern aller Zeiten sehen, die in etwa so aussieht:

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

Führen Sie nun den Befehl querywithtimespan mit den erforderlichen Argumenten aus, um die zehn besten Spieler des Jahres abzufragen. Geben Sie dazu einen „timespan“ an, der der Anzahl der Stunden in einem Jahr entspricht, also 8.760. Achten Sie darauf, my-project durch die Projekt-ID zu ersetzen, die Sie am Anfang dieses Codelabs erstellt haben.

./leaderboard querywithtimespan projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 8760

Sie sollten eine Antwort mit den zehn besten Spielern des Jahres sehen, die so aussieht:

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

Führen Sie nun den Befehl querywithtimespan aus, um die zehn besten Spieler des Monats abzufragen. Geben Sie dazu einen „timespan“ an, der der Anzahl der Stunden in einem Monat entspricht, also 730. Achten Sie darauf, my-project durch die Projekt-ID zu ersetzen, die Sie am Anfang dieses Codelabs erstellt haben.

./leaderboard querywithtimespan projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 730

Sie sollten eine Antwort mit den zehn besten Spielern des Monats sehen, die in etwa so aussieht:

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

Führen Sie nun den Befehl querywithtimespan aus, um die zehn besten Spieler der Woche abzufragen. Geben Sie dazu einen „timespan“ (Zeitraum) an, der der Anzahl der Stunden in einer Woche entspricht, also 168. Achten Sie darauf, my-project durch die Projekt-ID zu ersetzen, die Sie am Anfang dieses Codelabs erstellt haben.

./leaderboard querywithtimespan  projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 168

Sie sollten eine Antwort mit den zehn besten Spielern der Woche sehen, die so aussieht:

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

Super!

Wenn Sie jetzt Datensätze hinzufügen, skaliert Cloud Spanner Ihre Datenbank auf die benötigte Größe. Ganz gleich, wie groß Ihre Datenbank wird, die Bestenliste Ihres Spiels kann mit Cloud Spanner und der Truetime-Technologie weiterhin genau skaliert werden.

7. Bereinigen

Nachdem wir uns mit Spanner ausgetobt haben, müssen wir unseren Spielplatz aufräumen, um wertvolle Ressourcen und Geld zu sparen. Glücklicherweise ist das ein einfacher Schritt. Rufen Sie einfach den Cloud Spanner-Bereich der Cloud Console auf und löschen Sie die Instanz, die wir im Codelab-Schritt „Cloud Spanner-Instanz einrichten“ erstellt haben.

8. Glückwunsch!

Behandelte Themen:

  • Google Cloud Spanner-Instanzen, -Datenbanken und Tabellenschema für eine Bestenliste
  • Go-Konsolenanwendung erstellen
  • Spanner-Datenbank und -Tabellen mit der Go-Clientbibliothek erstellen
  • Daten mit der Go-Clientbibliothek in eine Spanner-Datenbank laden
  • Die zehn besten Ergebnisse aus Ihren Daten mit Spanner-Commit-Zeitstempeln und der Go-Clientbibliothek abfragen

Vorgehensweise:

Feedback geben

  • Bitte nehmen Sie sich einen Moment Zeit, um an unserer kurzen Umfrage teilzunehmen.