Cloud Spanner: Crea una tabla de clasificación de videojuegos con Go

Google Cloud Spanner es un servicio de base de datos relacional, distribuido de forma global y escalable de forma vertical completamente administrado que proporciona transacciones ACID y semántica de SQL sin renunciar al rendimiento y a la alta disponibilidad.

En este lab, aprenderás a configurar una instancia de Cloud Spanner. Completarás los pasos para crear una base de datos y un esquema que se puedan usar en una tabla de clasificación de videojuegos. Comenzarás por crear una tabla de jugadores a fin de almacenar su información y una tabla de puntuaciones para almacenar sus puntuaciones.

A continuación, propagarás las tablas con datos de muestra. Por último, finalizarás el lab mediante la ejecución de las diez consultas de muestra principales y borrarás la instancia para liberar recursos.

Qué aprenderá

  • Cómo configurar una instancia de Cloud Spanner.
  • Cómo crear una base de datos y tablas.
  • Cómo usar una columna de marca de tiempo de confirmación
  • Cómo cargar datos en tu tabla de base de datos de Cloud Spanner con marcas de tiempo
  • Cómo consultar tu base de datos de Cloud Spanner.
  • Cómo borrar tu instancia de Cloud Spanner.

Qué necesitarás

¿Cómo usarás este instructivo?

Leer Leer y completar los ejercicios

¿Cómo calificarías tu experiencia con Google Cloud Platform?

Principiante Intermedio Avanzado

Configuración del entorno a su propio ritmo

Si aún no tienes una Cuenta de Google (Gmail o Google Apps), debes crear una. Accede a Google Cloud Platform Console (console.cloud.google.com) y crea un proyecto nuevo.

Si ya tienes un proyecto, haz clic en el menú desplegable de selección de proyectos en la parte superior izquierda de la Console:

6c9406d9b014760.png

y haz clic en el botón “PROYECTO NUEVO” en el diálogo resultante para crear un proyecto nuevo:

f708315ae07353d0.png

Si aún no tienes un proyecto, deberías ver un cuadro de diálogo como este para crear el primero:

870a3cbd6541ee86.png

El cuadro de diálogo de creación posterior del proyecto te permite ingresar los detalles de tu proyecto nuevo:

6a92c57d3250a4b3.png

Recuerda el ID del proyecto, que es un nombre único en todos los proyectos de Google Cloud (el nombre anterior ya se encuentra en uso y no lo podrá usar). Se mencionará más adelante en este codelab como PROJECT_ID.

A continuación, si aún no lo has hecho, deberás habilitar la facturación en Developers Console para usar los recursos de Google Cloud y habilitar la API de Cloud Spanner.

15d0ef27a8fbab27.png

Ejecutar este codelab debería costar solo unos pocos dólares, pero su costo podría aumentar si decides usar más recursos o si los dejas en ejecución (consulta la sección “Limpiar” al final de este documento). Los precios de Google Cloud Spanner se documentan aquí.

Los usuarios nuevos de Google Cloud Platform están aptas para obtener una prueba gratuita de $300, por lo que este codelab es completamente gratuito.

Configuración de Google Cloud Shell

Si bien Google Cloud y Spanner se pueden operar de manera remota desde tu laptop, en este codelab usaremos Google Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.

Esta máquina virtual basada en Debian está cargada con todas las herramientas de desarrollo que necesitarás. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Esto significa que todo lo que necesitarás para este Codelab es un navegador (sí, funciona en una Chromebook).

  1. Para activar Cloud Shell desde Cloud Console, solo haz clic en Activar Cloud ShellgcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A (el aprovisionamiento y la conexión al entorno debería llevar solo unos minutos).

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

Captura de pantalla del 14 de junio de 2017 a las 10.13.43 p.m. .png

Una vez conectado a Cloud Shell, debería ver que ya se autenticó y que el proyecto ya se configuró con tu PROJECT_ID:

gcloud auth list

Resultado del comando

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

Resultado del comando

[core]
project = <PROJECT_ID>

Si, por algún motivo, el proyecto no está configurado, solo emite el siguiente comando:

gcloud config set project <PROJECT_ID>

Si no conoce su PROJECT_ID, Observa el ID que usaste en los pasos de configuración o búscalo en el panel de Cloud Console:

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

Cloud Shell también configura algunas variables de entorno de forma predeterminada, lo que puede resultar útil cuando ejecutas comandos futuros.

echo $GOOGLE_CLOUD_PROJECT

Resultado del comando

<PROJECT_ID>
  1. Establece la zona predeterminada y la configuración del proyecto.
gcloud config set compute/zone us-central1-f

Puedes elegir una variedad de zonas diferentes. Para obtener más información, consulta Regiones y zonas.

Resumen

En este paso, configurarás tu entorno.

A continuación

A continuación, configurarás una instancia de Cloud Spanner.

En este paso, configuraremos nuestra instancia de Cloud Spanner para este codelab. Busca la entrada de Spanner 1a6580bd3d3e6783.png en el menú de opciones superior izquierdo 3129589f7bc9e5ce.png o ingresa “/” y escribe “Spanner”

36e52f8df8e13b99.png

A continuación, haz clic en 95269e75bc8c3e4d.png y completa el formulario. Para ello, ingresa el nombre de la instancia cloudspanner-leaderboard de la instancia, elige una configuración (selecciona una instancia regional) y establece la cantidad de nodos. Para este codelab solo necesitaremos 1 nodo. Para las instancias de producción y a fin de cumplir con los requisitos del ANS de Cloud Spanner, debes ejecutar 3 o más nodos en tu instancia de Cloud Spanner.

Por último, pero no menos importante, haz clic en “Crear” y, en segundos, selecciona una instancia de Cloud Spanner a su disposición.

dceb68e9ed3801e8.png

En el siguiente paso, usaremos la biblioteca cliente de Go para crear una base de datos y un esquema en la instancia nueva.

En este paso, crearemos nuestra base de datos de muestra y un esquema.

Usemos la biblioteca cliente de Go para crear dos tablas, una tabla de jugadores con información sobre ellos y una tabla de puntuaciones a fin de almacenar las puntuaciones Para ello, analizaremos los pasos a fin de crear una aplicación de consola de Go en Cloud Shell.

Primero, clona el código de muestra para este codelab desde GitHub. Para ello, escribe el siguiente comando en Cloud Shell:

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

Luego, cambia el directorio al directorio “tabla de clasificación” en el que crearás tu aplicación.

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

Todo el código necesario para este codelab se encuentra en el directorio golang-samples/spanner/spanner_leaderboard/ existente como una aplicación ejecutable Go llamada leaderboard que sirve como referencia a medida que avanzas a través del codelab. Crearemos un directorio nuevo y compilaremos una copia de la aplicación Tablas de clasificación en etapas.

Crea un nuevo directorio llamado “codelab” para la aplicación y usa el comando de cambio de directorio a este con el siguiente comando:

mkdir codelab && cd $_

Ahora, actualicemos una aplicación básica de Go llamada “Leaderboard” que usa la biblioteca cliente de Spanner para crear una tabla de clasificación que incluya dos tablas, jugadores y puntuación. Puedes hacerlo directamente en el editor de Cloud Shell:

Haz clic en el siguiente ícono destacado para abrir el editor de Cloud Shell:

73cf70e05f653ca.png

Crea un archivo llamado "leaderboard.go" en la carpeta ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab.

  • Primero, asegúrate de tener la carpeta “codelab” seleccionada en la lista de carpetas del editor de Cloud Shell.
  • Luego, seleccione “Archivo nuevo” en el menú “Archivo” del editor de Cloud Shell.
  • Ingresa "leaderboard.go" como el nombre del archivo nuevo.

Este es el archivo principal de la aplicación que contendrá el código de la aplicación y las referencias para incluir las dependencias.

Para crear la base de datos leaderboard y las tablas Players y Scores, copia (Ctrl + P) y pega (Ctrl + V) el siguiente código de Go en el archivo 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)
        }
}

Para guardar los cambios realizados en el archivo leaderboard.go, selecciona “Guardar” en el menú “Archivo” de Cloud Shell.

Puedes usar el archivo leaderboard.go en el directorio golang-samples/spanner/spanner_leaderboard si deseas ver un ejemplo de cómo debe verse tu archivo leaderboard.go después de agregar el código para habilitar el comando createdatabase.

Para compilar tu aplicación en Cloud Shell, ejecuta “go build” desde el directorio codelab en el que se encuentra tu archivo leaderboard.go:

go build leaderboard.go

Una vez que su aplicación se haya compilado correctamente, ejecuta el siguiente comando para ejecutar la aplicación resultante en Cloud Shell:

./leaderboard

Deberías ver un resultado como el siguiente:

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.

En esta respuesta, podemos ver que esta es la aplicación Leaderboard, que en la actualidad tiene un comando posible: createdatabase. Podemos ver que el argumento esperado del comando createdatabase es una string que contiene un ID de instancia y un ID de base de datos específicos.

Ahora, ejecuta el siguiente comando: Asegúrate de reemplazar el my-project por el ID del proyecto que creaste al comienzo de este codelab.

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

Después de unos segundos, deberías ver una respuesta como la siguiente:

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

En la sección de Cloud Spanner de Cloud Console, deberías ver su nueva base de datos y tablas en el menú del lado izquierdo.

ba9008bb84cb90b0.png

En el próximo paso, actualizaremos nuestra aplicación para cargar algunos datos en tu base de datos nueva.

Ahora tenemos una base de datos llamada leaderboard que contiene las dos tablas, Players y Scores. Ahora usemos la biblioteca cliente de Go a fin de propagar la tabla Players con los jugadores y nuestra tabla Scores con puntuaciones aleatorias para cada jugador.

Si aún no se abrió, abre el editor de Cloud Shell y haz clic en el ícono que se destaca a continuación:

ef49fcbaaed19024.png

A continuación, edita el archivo leaderboard.go en el editor de Cloud Shell para agregar un comando insertplayers que se puede usar a fin de insertar 100 jugadores en la tabla Players. También agregaremos un comando insertscores que se puede usar a fin de insertar 4 puntuaciones aleatorias en la tabla Scores para cada jugador de la tabla Players.

Primero, actualiza la sección imports en la parte superior del archivo leaderboard.go. Para ello, reemplaza el contenido actual de modo que, una vez que termines, se vea de la siguiente manera:

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

A continuación, agrega un nuevo tipo de comando junto con una lista de comandos en la parte superior del archivo, justo debajo de la línea que comienza con “tipo de adminCommand…”, así que, una vez que termines, debería verse de la siguiente manera:

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

A continuación, agrega las siguientes funciones insertPlayers e insertScores debajo de la función createdatabase() existente:

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
}

Luego, para que el comando insert funcione, agrega el siguiente código a la función “run” de tu aplicación debajo de la declaración de administración createdatabase, y reemplaza la declaración 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

Cuando termines, la función run debería verse de la siguiente manera:

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
}

El último paso a fin de que termines de agregar la funcionalidad “insert” a tu aplicación es agregar texto de ayuda para los comandos “insertplayers” y “insertscores” a la función flag.Usage(). Agrega el siguiente texto de ayuda a la función flag.Usage() a fin de incluir texto de ayuda para los comandos de inserción:

Agrega los dos comandos a la lista de comandos posibles:

Command can be one of: createdatabase, insertplayers, insertscores

Agrega este texto de ayuda adicional debajo del texto de ayuda para el comando 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.

Para guardar los cambios realizados en el archivo leaderboard.go, selecciona “Guardar” en el menú “Archivo” de Cloud Shell.

Puedes usar el archivo leaderboard.go en el directorio golang-samples/spanner/spanner_leaderboard para ver un ejemplo de cómo debe verse tu archivo leaderboard.go después de agregar el código para habilitar los comandos insertplayers y insertscores.

Ahora, compila y ejecuta la aplicación para confirmar que los nuevos comandos insertplayers y insertscores estén incluidos en la lista de comandos posibles de la aplicación. Ejecute el siguiente comando para compilar la aplicación:

go build leaderboard.go

Ingresa el siguiente comando para ejecutar la aplicación resultante en Cloud Shell:

./leaderboard

Ahora deberías ver los comandos insertplayers y insertscores en el resultado predeterminado de la aplicación:

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.

Ahora, ejecutemos el comando insertplayers con los mismos valores de argumento que usamos cuando llamamos al comando createdatabase. Asegúrate de reemplazar el my-project por el ID del proyecto que creaste al comienzo de este codelab.

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

Después de unos segundos, deberías ver una respuesta como la siguiente:

Inserted players

Ahora usemos la biblioteca cliente de Go a fin de propagar nuestra tabla Scores con cuatro puntuaciones aleatorias junto con marcas de tiempo para cada jugador en la tabla Players.

La columna Timestamp de la tabla Scores se definió como una columna de “marca de tiempo de confirmación” a través de la siguiente instrucción de SQL que se ejecutaba cuando ejecutamos el comando 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

Observa el atributo OPTIONS(allow_commit_timestamp=true). Esto hace que Timestamp sea una columna de “marca de tiempo de confirmación” y permite que se propague automáticamente con la marca de tiempo exacta de la transacción para operaciones INSERTAR y ACTUALIZAR en una fila de tabla determinada.

También puedes insertar tus propios valores de marca de tiempo en una columna “marca de tiempo de confirmación”, siempre y cuando insertes una marca de tiempo con un valor anterior, lo que haremos para este codelab.

Ahora, ejecutemos el comando insertscores con los mismos valores de argumento que usamos cuando llamamos al comando insertplayers. Asegúrate de reemplazar el my-project por el ID del proyecto que creaste al comienzo de este codelab.

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

Después de unos segundos, deberías ver una respuesta como la siguiente:

Inserted scores

La ejecución de la función insertScores usa el siguiente fragmento de código para insertar una marca de tiempo generada de forma aleatoria con una fecha y hora en el pasado:

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

Para propagar automáticamente la columna Timestamp con la marca de tiempo exacta cuando la transacción “Insertar” se instale, puedes insertar la constante spanner.CommitTimestamp de Go, como en el siguiente fragmento de código:

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

Ahora que se completó la carga de datos, verificaremos los valores que acabamos de escribir en nuestras tablas nuevas en la sección de Cloud Spanner de Cloud Console. Primero, selecciona la base de datos leaderboard y, luego, la tabla Players. Haz clic en la pestaña Data. Debería ver que tiene datos en las columnas PlayerId y PlayerName de la tabla.

7bc2c96293c31c49.png

A continuación, verifiquemos la tabla de puntuaciones. Para ello, haz clic en la tabla Scores y selecciona la pestaña Data. Deberías ver que tiene datos en las columnas PlayerId, Timestamp y Score de la tabla.

d8a4ee4f13244c19.png

¡Bien hecho! Actualicemos nuestra app para ejecutar algunas consultas que podemos usar a fin de crear una tabla de clasificación de videojuegos.

Ahora que configuramos nuestra base de datos y cargamos información en nuestras tablas, vamos a crear una tabla de clasificación con estos datos. Para ello, debemos responder las siguientes cuatro preguntas:

  1. ¿Cuáles son los diez principales jugadores de todos los tiempos?
  2. ¿Cuáles son los diez jugadores principales de este año?
  3. ¿Cuáles son los diez jugadores principales del mes?
  4. ¿Cuáles son los diez jugadores principales de la semana?

Actualicemos nuestra aplicación para ejecutar las consultas de SQL que responderán estas preguntas.

Agregaremos un comando query y un comando queryWithTimespan que proporcionará una forma de ejecutar las consultas a fin de responder las preguntas que producirán la información requerida en nuestra tabla de clasificación.

Edita el archivo leaderboard.go en el editor de Cloud Shell a fin de actualizar la aplicación para agregar un comando query y un comando queryWithTimespan. También agregaremos una función auxiliar formatWithCommas para dar formato a nuestras puntuaciones con comas.

Primero, actualiza la sección imports en la parte superior del archivo leaderboard.go. Para ello, reemplaza el contenido actual de modo que, una vez que termines, se vea de la siguiente manera:

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

A continuación, agrega las siguientes dos funciones y la función auxiliar debajo del método insertScores existente:

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

En la parte superior del archivo leaderboard.go, agrega “query” como una opción de comando en la variable commands, justo debajo de la opción “insertscores": insertScores”, de modo que la variable commands se vea de la siguiente manera:

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

A continuación, agregue “queryWithTimespan” como opción de comando dentro de la función run, debajo de la sección del comando “createdatabase” y sobre la sección de control del comando “insert y 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
        }

Cuando termines, la función run debería verse de la siguiente manera:

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
}

Luego, para que el comando queryWithTimespan funcione, actualiza el bloque de código flag.Parse() en el método “main” de tu aplicación para que se vea de la siguiente manera:

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

El último paso a fin de que puedas agregar la funcionalidad de “query” a tu aplicación es agregar texto de ayuda para los comandos “query” y “querywithtimespan” a la función flag.Usage(). Agrega las siguientes líneas de código a la función flag.Usage() a fin de incluir texto de ayuda para los comandos de consulta:

Agrega los dos comandos “query” a la lista de comandos posibles:

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

Agrega este texto de ayuda adicional debajo del texto de ayuda para el comando 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.

Para guardar los cambios realizados en el archivo leaderboard.go, selecciona “Guardar” en el menú “Archivo” de Cloud Shell.

Puedes usar el archivo leaderboard.go en el directorio golang-samples/spanner/spanner_leaderboard para ver un ejemplo de cómo debe verse tu archivo leaderboard.go después de agregar el código para habilitar los comandos query y querywithtimespan.

Ahora, compila y ejecuta la aplicación para confirmar que los nuevos comandos query y querywithtimespan estén incluidos en la lista de comandos posibles de la aplicación.

En Cloud Shell, ejecuta el siguiente comando para compilar la aplicación:

go build leaderboard.go

Ingresa el siguiente comando para ejecutar la aplicación resultante en Cloud Shell:

./leaderboard

Los comandos query y querywithtimespan ahora se incluyen en el resultado predeterminado de la aplicación como una nueva opción de comando:

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.

Desde la respuesta, podemos ver que podemos usar el comando query para obtener una lista de los “diez mejores” jugadores de todos los tiempos. También podemos ver que el comando querywithtimespan nos permite especificar un intervalo de horas para usar en el filtrado de registros según su valor en la columna Timestamp de la tabla Scores.

Ejecutemos el comando query con los mismos valores de argumento que usamos cuando ejecutamos el comando create. Asegúrate de reemplazar el my-project por el ID del proyecto que creaste al comienzo de este codelab.

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

Deberías ver una respuesta que incluya los “diez mejores” jugadores de todos los tiempos, como se indica a continuación:

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

Ahora, ejecutemos el comando querywithtimespan con los argumentos necesarios a fin de consultar los “diez mejores” jugadores del año. Para ello, especifica un “intervalo” igual a la cantidad de horas en un año, que es 8,760. Asegúrate de reemplazar el my-project por el ID del proyecto que creaste al comienzo de este codelab.

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

Deberías ver una respuesta que incluya a los “diez mejores” jugadores del año, como se muestra a continuación:

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

Ahora, ejecutemos el comando querywithtimespan a fin de consultar a los “diez mejores” jugadores del mes. Para ello, especifica un “intervalo” igual a la cantidad de horas de un mes, que es 730. Asegúrate de reemplazar el my-project por el ID del proyecto que creaste al comienzo de este codelab.

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

Deberías ver una respuesta que incluya a los “diez mejores” jugadores del mes, como se muestra a continuación:

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

Ahora, ejecutemos el comando querywithtimespan a fin de consultar a los “diez mejores” jugadores de la semana. Para ello, especifica un “intervalo” igual a la cantidad de horas de una semana, que es 168. Asegúrate de reemplazar el my-project por el ID del proyecto que creaste al comienzo de este codelab.

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

Deberías ver una respuesta que incluya a los “diez mejores” jugadores de la semana, como se muestra a continuación:

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

¡Excelente trabajo!

Ahora a medida que agregues los registros, Cloud Spanner escalará tu base de datos del tamaño que necesites que sea. No importa cuánto crezca tu base de datos, la tabla de clasificación de tu juego puede escalar con exactitud con Cloud Spanner y su tecnología Truetime.

Después de la diversión cuando se juega con Spanner, debemos limpiar nuestro zona de prueba para ahorrar dinero y recursos valiosos. Por suerte, este paso es sencillo. Ve a la sección de Cloud Spanner de Cloud Console y borra la instancia que creamos en el paso de codelab denominado “Configurar una instancia de Cloud Spanner”.

Temas abordados:

  • Instancias, bases de datos y esquemas de tablas de Google Cloud Spanner para una tabla de clasificación
  • Cómo crear una aplicación de la consola de Go
  • Cómo crear una base de datos y tablas de Spanner con la biblioteca cliente de Go
  • Cómo cargar datos en una base de datos de Spanner con la biblioteca cliente de Go
  • Cómo consultar los resultados de los “Diez mejores” de tus datos mediante las marcas de tiempo de confirmación de Spanner y la biblioteca cliente de Go

Próximos pasos:

Envíanos tus comentarios

  • Tómate un momento para completar nuestra breve encuesta