Cloud Spanner : créer un classement pour un jeu en Go

Google Cloud Spanner est un service de base de données relationnelle entièrement géré, distribué à l'échelle mondiale et évolutif horizontalement qui fournit des transactions ACID et une sémantique SQL sans compromis sur les performances et le niveau de disponibilité.

Dans cet atelier, vous allez apprendre à configurer une instance Cloud Spanner. Au fil des étapes, vous allez créer une base de données et un schéma qui peuvent être utilisés pour un classement de jeu. Vous allez commencer par créer une table "Players" (Joueurs) pour stocker les informations concernant les joueurs et un tableau "Scores" pour stocker les scores des joueurs.

Vous remplirez ensuite ces tables avec des exemples de données. Vous finirez l'atelier en exécutant quelques exemples de requêtes de "Top 10" avant de supprimer l'instance pour libérer les ressources.

Points abordés

  • Comment configurer une instance Cloud Spanner.
  • Comment créer une base de données et des tables.
  • Comment utiliser une colonne d'horodatage de commit.
  • Comment charger des données dans une table de base de données Cloud Spanner avec des horodatages.
  • Comment interroger votre base de données Cloud Spanner.
  • Comment supprimer une instance Cloud Spanner.

Prérequis

Comment allez-vous utiliser ce tutoriel ?

Je vais le lire uniquement Je vais le lire et effectuer les exercices

Quel est votre niveau d'expérience avec Google Cloud Platform ?

Débutant Intermédiaire Expert

Configuration de l'environnement au rythme de chacun

Si vous ne possédez pas encore de compte Google (Gmail ou Google Apps), vous devez en créer un. Connectez-vous à la console Google Cloud Platform (console.cloud.google.com) et créez un projet.

Si vous avez déjà un projet, cliquez sur le menu déroulant de sélection du projet dans l'angle supérieur gauche de la console :

6c9406d9b014760.png

Cliquez ensuite sur le bouton "NEW PROJECT" (NOUVEAU PROJET) dans la boîte de dialogue qui s'affiche pour créer un projet :

f708315ae07353d0.png

Si vous n'avez pas encore de projet, une boîte de dialogue semblable à celle-ci apparaîtra pour vous permettre d'en créer un :

870a3cbd6541ee86.png

La boîte de dialogue de création de projet suivante vous permet de saisir les détails de votre nouveau projet :

6a92c57d3250a4b3.png

Notez l'ID du projet. Il s'agit d'un nom unique pour tous les projets Google Cloud, ce qui implique que le nom ci-dessus n'est plus disponible pour vous… Désolé ! Tout au long de cet atelier de programmation, nous utiliserons PROJECT_ID pour faire référence à cet ID.

Ensuite, si ce n'est pas déjà fait, vous devez activer la facturation dans Developers Console afin de pouvoir utiliser les ressources Google Cloud puis activer l'API Cloud Spanner.

15d0ef27a8fbab27.png

Suivre cet atelier de programmation ne devrait pas vous coûter plus d'un euro. Cependant, cela peut s'avérer plus coûteux si vous décidez d'utiliser davantage de ressources ou si vous n'interrompez pas les ressources (voir la section "Effectuer un nettoyage" à la fin du présent document). Les tarifs de Google Cloud Spanner sont décrits sur cette page.

Les nouveaux utilisateurs de Google Cloud Platform peuvent bénéficier d'un Essai gratuit avec 300 $ de crédits afin de suivre gratuitement le présent atelier.

Configuration de Google Cloud Shell

Bien que Google Cloud et Spanner puissent être utilisés à distance depuis votre ordinateur portable, nous allons utiliser Google Cloud Shell pour cet atelier de programmation, un environnement de ligne de commande exécuté dans le cloud.

Cette machine virtuelle basée sur Debian contient tous les outils de développement dont vous aurez besoin. Elle intègre un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Cela signifie que tout ce dont vous avez besoin pour cet atelier de programmation est un navigateur (oui, tout fonctionne sur un Chromebook).

  1. Pour activer Cloud Shell à partir de Cloud Console, cliquez simplement sur Activer Cloud Shell gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A (l'opération de provisionnement et la connexion à l'environnement ne devraient prendre que quelques minutes).

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

Capture d'écran du 2017-06-14 à 10.13.43 PM.png

Une fois connecté à Cloud Shell, vous êtes normalement déjà authentifié et le projet PROJECT_ID est sélectionné :

gcloud auth list

Résultat de la commande

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

Résultat de la commande

[core]
project = <PROJECT_ID>

Si, pour une raison quelconque, le projet n'est pas défini, exécutez simplement la commande suivante :

gcloud config set project <PROJECT_ID>

Vous recherchez votre PROJECT_ID ? Vérifiez l'ID que vous avez utilisé pendant les étapes de configuration ou recherchez-le dans le tableau de bord Cloud Console :

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

Par défaut, Cloud Shell définit certaines variables d'environnement qui pourront s'avérer utiles pour exécuter certaines commandes dans le futur.

echo $GOOGLE_CLOUD_PROJECT

Résultat de la commande

<PROJECT_ID>
  1. Pour finir, définissez la configuration du projet et de la zone par défaut :
gcloud config set compute/zone us-central1-f

Vous pouvez choisir parmi différentes zones. Pour en savoir plus, consultez la page Régions et zones.

Résumé

Cette étape consiste à configurer votre environnement.

Étapes suivantes

Vous allez maintenant configurer une instance Cloud Spanner.

Dans cette étape, nous allons configurer notre instance Cloud Spanner pour cet atelier de programmation. Recherchez l'entrée Spanner 1a6580bd3d3e6783.png dans le menu principal en haut à gauche 3129589f7bc9e5ce.png ou recherchez Spanner en appuyant sur "/" avant de saisir "Spanner".

36e52f8df8e13b99.png

Cliquez ensuite sur 95269e75bc8c3e4d.png et remplissez le formulaire en renseignant le nom d'instance cloudspanner-leaderboard, en choisissant une configuration (sélectionnez une instance régionale), puis en définissant le nombre de nœuds (pour le présent atelier, nous n'aurons besoin que d'un seul nœud). Pour les instances en production et pour bénéficier du contrat de niveau de service de Cloud Spanner, vous devez exécuter au moins trois nœuds dans votre instance Cloud Spanner.

Dernière étape mais pas des moindres, cliquez sur "Créer". L'instance Cloud Spanner sera prête en quelques secondes.

dceb68e9ed3801e8.png

Pour l'étape suivante, nous allons utiliser la bibliothèque cliente Go afin de créer une base de données et un schéma dans notre nouvelle instance.

Au cours de cette étape, nous allons créer notre exemple de base de données et notre schéma.

Utilisons la bibliothèque cliente Go pour créer deux tables : une table "Players" (Joueurs) pour stocker les informations concernant les joueurs et une table "Scores" pour stocker les scores des joueurs. Pour ce faire, nous allons vous indiquer comment créer une application de console Go dans Cloud Shell.

Commencez par cloner l'exemple de code du présent atelier de programmation à partir de GitHub en saisissant la commande suivante dans Cloud Shell :

go get -u github.com/GoogleCloudPlatform/golang-samples/spanner/...

Accédez ensuite au répertoire "leaderboard" dans lequel vous allez créer votre application.

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

L'ensemble du code requis pour cet atelier de programmation se trouve dans le répertoire golang-samples/spanner/spanner_leaderboard/ existant en tant qu'application Go exécutable leaderboard, pour vous servir de référence tout au long de cet atelier. Nous allons créer un répertoire et créer une copie de l'application de classement étape par étape.

Créez un répertoire nommé "codelab" pour l'application et accédez-y avec la commande suivante :

mkdir codelab && cd $_

À présent, nous allons créer une application Go de base nommée "Leaderboard" qui utilise la bibliothèque cliente Spanner pour créer un classement composé des deux tables "Players" (Joueurs) et "Scores". Vous pouvez faire tout cela directement depuis l'éditeur Cloud Shell :

Ouvrez l'éditeur Cloud Shell en cliquant sur l'icône en surbrillance ci-dessous :

73cf70e05f653ca.png

Créez un fichier nommé "leaderboard.go" dans le dossier ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab.

  • Vérifiez d'abord que le dossier "codelab" est bien sélectionné dans la liste de dossiers de l'éditeur Cloud Shell.
  • Sélectionnez ensuite "New File" (Nouveau fichier) dans le menu "File " (Fichier) de l'éditeur Cloud Shell.
  • Nommez le nouveau fichier "leaderboard.go".

Il s'agit du fichier principal de l'application qui contiendra le code de l'application ainsi que les références permettant d'inclure des dépendances.

Pour créer la base de données leaderboard et les tables Players et Scores, copiez (Ctrl+P) puis collez (Ctrl+V) le code Go suivant dans le fichier leaderboard.go :

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

Enregistrez les modifications apportées au fichier leaderboard.go en sélectionnant "Enregistrer" dans le menu "Fichier" de l'éditeur Cloud Shell.

Vous pouvez utiliser le fichier leaderboard.go du répertoire golang-samples/spanner/spanner_leaderboard comme référence de ce à quoi doit ressembler votre fichier leaderboard.go après l'ajout du code pour activer la commande createdatabase.

Pour créer votre application dans Cloud Shell, exécutez la commande "go build" à partir du répertoire codelab où se trouve votre fichier leaderboard.go :

go build leaderboard.go

Une fois votre application créée, exécutez-la dans Cloud Shell à l'aide de la commande suivante :

./leaderboard

Vous devriez voir une sortie semblable à ce qui suit.

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.

À partir de cette réponse, nous pouvons voir qu'il s'agit bien de l'application Leaderboard qui possède actuellement une seule commande possible : createdatabase. Nous constatons que l'argument attendu de la commande createdatabase est une chaîne contenant un ID d'instance et un ID de base de données spécifiques.

Exécutez maintenant la commande suivante : Assurez-vous de bien remplacer my-project par l'ID de projet que vous avez créé au début du présent atelier de programmation.

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

Après quelques secondes, une réponse de ce type doit s'afficher :

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

Dans la section Cloud Spanner de Cloud Console, vous devriez voir votre nouvelle base de données et vos tables dans le menu de gauche.

ba9008bb84cb90b0.png

À l'étape suivante, nous mettrons à jour notre application afin de charger des données dans votre nouvelle base de données.

Nous disposons à présent d'une base de données appelée leaderboard et contenant deux tables, Players et Scores. Utilisons maintenant la bibliothèque cliente Go pour remplir la table Players avec des joueurs et la table Scores avec des scores aléatoires pour chaque joueur.

S'il n'est pas déjà ouvert, ouvrez l'éditeur Cloud Shell en cliquant sur l'icône en surbrillance ci-dessous :

ef49fcbaaed19024.png

Ensuite, modifiez le fichier leaderboard.go dans l'éditeur Cloud Shell afin d'ajouter une commande insertplayers permettant d'insérer 100 joueurs dans le tableau Players. Nous allons également ajouter une commande insertscores permettant d'insérer quatre résultats aléatoires dans la table Scores pour chaque joueur de la table Players.

Commencez par mettre à jour la section imports en haut du fichier leaderboard.go en la remplaçant de sorte à obtenir le résultat ci-dessous :

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

Ajoutez ensuite un nouveau type de commande ainsi qu'une liste de commandes en haut du fichier, juste en dessous de la ligne qui commence par "type adminCommand ...". Une fois cette opération terminée, vous devriez obtenir un résultat semblable à ceci :

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

Ajoutez ensuite les fonctions insertPlayers et insertScores suivantes sous la fonction createdatabase() existante :

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
        }
        // Intialize 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
                }
                // Intialize 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
}

Pour que la commande insert fonctionne, ajoutez le code suivant à la fonction "run" de votre application sous l'instruction de traitement createdatabase, en remplaçant l'instruction 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

Une fois l'opération terminée, la fonction run doit ressembler à ceci :

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
}

Pour finaliser l'ajout de la fonctionnalité "insert" à votre application, la dernière étape consiste à ajouter un texte d'aide pour les commandes "insertplayers" et "insertscores" dans la fonction flag.Usage(). Ajoutez le texte d'aide suivant à la fonction flag.Usage() afin d'inclure le texte d'aide pour les commandes d'insertion :

Ajoutez les deux commandes à la liste des commandes possibles :

Command can be one of: createdatabase, insertplayers, insertscores

Ajoutez ce texte d'aide supplémentaire sous le texte d'aide pour la commande createdatabase.

        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.

Enregistrez les modifications apportées au fichier leaderboard.go en sélectionnant "Enregistrer" dans le menu "Fichier" de l'éditeur Cloud Shell.

Vous pouvez utiliser le fichier leaderboard.go dans le répertoire golang-samples/spanner/spanner_leaderboard comme référence de ce à quoi doit ressembler votre fichier leaderboard.go après avoir ajouté le code pour activer les commandes insertplayers et insertscores.

Maintenant, nous allons créer et exécuter l'application pour vérifier que les nouvelles commandes insertplayers et insertscores sont bien incluses dans la liste des commandes possibles de l'application. Exécutez la commande suivante pour créer l'application :

go build leaderboard.go

Exécutez l'application créée dans Cloud Shell en saisissant la commande suivante :

./leaderboard

Les commandes insertplayers et insertscores devraient maintenant être incluses dans la sortie par défaut de l'application :

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.

Nous allons maintenant exécuter la commande insertplayers avec les mêmes valeurs d'argument que celles utilisées lors de l'appel de la commande createdatabase. Assurez-vous de bien remplacer my-project par l'ID de projet que vous avez créé au début du présent atelier de programmation.

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

Après quelques secondes, une réponse de ce type doit s'afficher :

Inserted players

Utilisons maintenant la bibliothèque cliente Go pour remplir notre table Scores avec quatre scores et horodatages aléatoires pour chaque joueur dans la table Players.

La colonne Timestamp de la table Scores a été définie comme colonne d'horodatage de commit ("commit timestamp") via l'instruction SQL suivante, exécutée lors de l'exécution de la commande create :

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

Notez l'attribut OPTIONS(allow_commit_timestamp=true). Cela fait de Timestamp une colonne d'horodatage de commit qu'il est possible de renseigner avec l'horodatage de transaction exact des opérations INSERT et UPDATE sur une ligne de table donnée.

Vous pouvez également insérer vos propres valeurs d'horodatage dans une colonne "commit timestamp" à condition d'utiliser un horodatage situé dans le passé, ce qui est exactement ce que nous avons fait dans le présent atelier de programmation.

Nous allons maintenant exécuter la commande insertscores avec les mêmes valeurs d'argument que celles utilisées lors de l'appel de la commande insertplayers. Assurez-vous de bien remplacer my-project par l'ID de projet que vous avez créé au début du présent atelier de programmation.

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

Après quelques secondes, une réponse de ce type doit s'afficher :

Inserted scores

L'exécution de la fonction insertScores utilise l'extrait de code suivant pour insérer un horodatage généré de manière aléatoire avec une date et une heure dans le passé :

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

Pour automatiquement remplir la colonne Timestamp avec l'horodatage correspondant à l'heure exacte de la transaction "Insert", vous pouvez insérer la constante Go spanner.CommitTimestamp comme dans l'extrait de code suivant :

...
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,
        },
})

Maintenant que le chargement des données est terminé, vérifions les valeurs que nous venons d'écrire dans les nouvelles tables en consultant la section Cloud Spanner de Cloud Console. Sélectionnez d'abord la base de données leaderboard, puis la table Players. Cliquez sur l'onglet Data. Vous devriez voir des données dans les colonnes PlayerId et PlayerName de la table.

7bc2c96293c31c49.png

Vérifions maintenant le tableau des scores contient bien des données en cliquant sur le tableau Scores et en sélectionnant l'onglet Data. Vous devriez voir des données dans les colonnes PlayerId, Timestamp et Score de la table.

d8a4ee4f13244c19.png

Bravo ! Mettons à jour notre application pour exécuter des requêtes qui nous permettront de créer un classement de jeu.

Maintenant que nous avons configuré notre base de données et chargé des informations dans nos tables, nous allons créer un classement à l'aide de ces données. Pour ce faire, nous devons répondre aux quatre questions suivantes :

  1. Quels joueurs sont les "Top 10" joueurs de tous les temps ?
  2. Quels joueurs sont les "Top 10" joueurs de l'année ?
  3. Quels joueurs sont les "Top 10" joueurs du mois ?
  4. Quels joueurs sont les "Top 10" joueurs de la semaine ?

Mettons à jour notre application pour exécuter les requêtes SQL qui répondent à ces questions.

Nous allons ajouter une commande query et une commande queryWithTimespan afin d'exécuter les requêtes permettant de répondre à ces questions en générant les informations requises pour notre classement.

Modifiez le fichier leaderboard.go dans l'éditeur Cloud Shell afin de mettre à jour l'application et d'ajouter des commandes query et queryWithTimespan. Nous allons également ajouter une fonction d'assistance formatWithCommas pour mettre en forme nos scores avec des virgules.

Commencez par mettre à jour la section imports en haut du fichier leaderboard.go en la remplaçant de sorte à obtenir le résultat ci-dessous :

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

Ajoutez ensuite les deux fonctions et la fonction d'assistance suivantes, sous la méthode insertScores existante :

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

Dans la partie supérieure du fichier leaderboard.go, ajoutez "query" comme option de commande dans la variable commands, juste en dessous de l'option "insertscores": insertScores", afin que la variable commands ressemble à ceci :

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

Ajoutez ensuite "queryWithTimespan" comme option de commande dans la fonction run, sous la section de la commande "createdatabase" et au-dessus de la section de gestion des commandes "insert" et "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
        }

Une fois l'opération terminée, la fonction run doit ressembler à ceci :

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
}

Pour que la commande queryWithTimespan fonctionne, mettez à jour le bloc de code flag.Parse() dans la méthode "main" de votre application de sorte qu'elle se présente comme suit :

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

La dernière étape pour finaliser l'ajout de la fonctionnalité "query" à votre application consiste à ajouter du texte d'aide pour les commandes "query" et "querywithtimespan" dans la fonction flag.Usage(). Ajoutez les lignes de code suivantes à la fonction flag.Usage() afin d'inclure du texte d'aide pour les commandes "query" :

Ajoutez les deux commandes "query" à la liste des commandes possibles :

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

Ajoutez ce texte d'aide supplémentaire sous le texte d'aide pour la commande insertscores.

        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.

Enregistrez les modifications apportées au fichier leaderboard.go en sélectionnant "Enregistrer" dans le menu "Fichier" de l'éditeur Cloud Shell.

Vous pouvez utiliser le fichier leaderboard.go dans le répertoire golang-samples/spanner/spanner_leaderboard comme référence de ce à quoi doit ressembler votre fichier leaderboard.go après avoir ajouté le code pour activer les commandes query et querywithtimespan.

Vous allez maintenant créer et exécuter l'application pour confirmer que les nouvelles commandes query et querywithtimespan sont bien incluses dans la liste des commandes possibles.

Exécutez la commande suivante dans Cloud Shell pour créer l'application :

go build leaderboard.go

Exécutez l'application créée dans Cloud Shell en saisissant la commande suivante :

./leaderboard

Vous devriez à présent voir s'afficher les commandes query et querywithtimespan en tant que nouvelles options de commande dans la sortie par défaut de l'application :

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.

Vous pouvez voir dans la réponse que nous pouvons utiliser la commande query pour obtenir la liste des joueurs du "Top 10" sur l'intégralité des données disponibles. Nous pouvons également voir que la commande querywithtimespan nous permet de spécifier une période en nombre d'heures à utiliser pour filtrer les enregistrements en fonction de leur valeur dans la colonne Timestamp de la table Scores.

Nous allons exécuter la commande query en utilisant les mêmes valeurs d'argument que celles utilisées lors de l'exécution de la commande create. Assurez-vous de bien remplacer my-project par l'ID de projet que vous avez créé au début du présent atelier de programmation.

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

Vous devriez voir une réponse similaire à celle ci-dessous et incluant les joueurs du "Top 10" :

PlayerId: 4018687297  PlayerName: Player 83  Score: 999,618  Timestamp: 2017-07-01
PlayerId: 4018687297  PlayerName: Player 83  Score: 998,956  Timestamp: 2017-09-02
PlayerId: 4285713246  PlayerName: Player 51  Score: 998,648  Timestamp: 2017-12-01
PlayerId: 5267931774  PlayerName: Player 49  Score: 997,733  Timestamp: 2017-11-09
PlayerId: 1981654448  PlayerName: Player 35  Score: 997,480  Timestamp: 2018-12-06
PlayerId: 4953940705  PlayerName: Player 87  Score: 995,184  Timestamp: 2018-09-14
PlayerId: 2456736905  PlayerName: Player 84  Score: 992,881  Timestamp: 2017-04-14
PlayerId: 8234617611  PlayerName: Player 19  Score: 992,399  Timestamp: 2017-12-27
PlayerId: 1788051688  PlayerName: Player 76  Score: 992,265  Timestamp: 2018-11-22
PlayerId: 7127686505  PlayerName: Player 97  Score: 992,038  Timestamp: 2017-12-02

Exécutez maintenant la commande querywithtimespan avec les arguments nécessaires pour obtenir les joueurs du "Top 10" de l'année en spécifiant une valeur "timespan" égale au nombre d'heures dans une année (8760). Assurez-vous de bien remplacer my-project par l'ID de projet que vous avez créé au début du présent atelier de programmation.

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

Vous devriez voir une réponse similaire à celle ci-dessous et incluant les joueurs du "Top 10" de l'année :

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

À présent, vous allez exécuter la commande querywithtimespan pour obtenir les joueurs du "Top 10 " du mois en spécifiant une valeur "timespan" égale au nombre d'heures dans un mois (730). Assurez-vous de bien remplacer my-project par l'ID de projet que vous avez créé au début du présent atelier de programmation.

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

Vous devriez voir une réponse similaire à celle ci-dessous et incluant les joueurs du "Top 10" du mois :

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

À présent, exécutez la commande querywithtimespan pour obtenir les joueurs du "Top 10 " de la semaine en spécifiant une valeur "timespan" égale au nombre d'heures dans une semaine (168). Assurez-vous de bien remplacer my-project par l'ID de projet que vous avez créé au début du présent atelier de programmation.

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

Vous devriez voir une réponse similaire à celle ci-dessous et incluant les joueurs du "Top 10" de la semaine :

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

Beau travail !

À mesure que vous ajoutez des enregistrements, Cloud Spanner fait évoluer votre base de données en fonction de vos besoins. Quel que soit le volume de votre base de données, le classement de votre jeu peut continuer d'évoluer avec précision grâce à Cloud Spanner et sa technologie Truetime.

Après s'être amusé avec Spanner, il faut maintenant nettoyer notre aire de jeux pour ne gaspiller de préciseuses ressources et donc d'argent. Heureusement, il s'agit d'une étape très facile puisqu'il vous suffit d'accéder à la section Cloud Spanner de Cloud Console et de supprimer l'instance que nous avons créée à l'étape "Configurer une instance Cloud Spanner".

Points abordés :

  • Instances, bases de données et schéma de table Google Cloud Spanner pour un classement.
  • Comment créer une application de console Go.
  • Comment créer une base de données et des tables Spanner à l'aide de la bibliothèque cliente Go.
  • Comment charger des données dans une base de données Spanner à l'aide de la bibliothèque cliente Go.
  • Comment interroger les résultats du "Top 10" à partir de vos données à l'aide des horodatages de commit Spanner et de la bibliothèque cliente Go.

Prochaines étapes :

Votre avis nous intéresse !

  • Veuillez prendre quelques minutes pour répondre à notre courte enquête.