Cloud Spanner: crie um placar de jogos com Go

O Google Cloud Spanner é um serviço de banco de dados relacional horizontalmente escalonável e distribuído globalmente que fornece transações ACID e semântica SQL sem deixar de oferecer desempenho e alta disponibilidade.

Neste laboratório, você aprenderá a configurar uma instância do Cloud Spanner. Você conhecerá as etapas para criar um banco de dados e um esquema que possam ser usados em um placar de jogos. Primeiro, crie uma tabela de jogadores para armazenar informações sobre o jogador e uma tabela de pontuações para armazenar as pontuações dos jogadores.

Em seguida, preencha as tabelas com dados de amostra. Para concluir o laboratório, execute algumas consultas de exemplo do Top 10 e, por fim, exclua a instância para liberar recursos.

O que você aprenderá

  • Como configurar uma instância do Cloud Spanner.
  • Como criar um banco de dados e tabelas.
  • Como usar uma coluna de carimbo de data/hora de confirmação.
  • Como carregar dados em uma tabela de banco de dados do Cloud Spanner com carimbos de data/hora.
  • Como consultar o banco de dados do Cloud Spanner.
  • Como excluir uma instância do Cloud Spanner.

Pré-requisitos

Como você usará este tutorial?

Apenas leitura Leitura e exercícios

Como você classificaria sua experiência com o Google Cloud Platform?

Iniciante Intermediário Proficiente

Configuração de ambiente personalizada

Se você ainda não tem uma Conta do Google (Gmail ou Google Apps), crie uma. Faça login no Console do Google Cloud Platform ( console.cloud.google.com) e crie um novo projeto.

Se você já tiver um projeto, clique no menu suspenso de seleção no canto superior esquerdo do console:

6c9406d9b014760.png

e clique no botão "NEW PROJECT" na caixa de diálogo exibida para criar um novo projeto:

f708315ae07353d0.png

Se você ainda não tiver um projeto, uma caixa de diálogo como esta será exibida para criar seu primeiro:

870a3cbd6541ee86.png

A caixa de diálogo de criação de projeto subsequente permite que você insira os detalhes do novo projeto:

6a92c57d3250a4b3.png

Lembre-se do código do projeto, um nome exclusivo em todos os projetos do Google Cloud. O nome acima já foi escolhido e não servirá para você. Faremos referência a ele mais adiante neste codelab como PROJECT_ID.

Em seguida, será preciso ativar o faturamento no Developers Console para usar os recursos do Google Cloud e ativar a API Cloud Spanner, caso ainda não tenha feito isso.

15d0ef27a8fbab27.png

A execução por meio deste codelab terá um custo baixo, mas poderá ser mais se você decidir usar mais recursos ou se deixá-los em execução. Consulte a seção "limpeza" no final deste documento. Os preços do Google Cloud Spanner estão documentados neste link.

Novos usuários do Google Cloud Platform estão qualificados para uma avaliação gratuita de US$ 300, o que torna este codelab totalmente gratuito.

Configuração do Google Cloud Shell

Embora o Google Cloud e o Spanner possam ser operados remotamente do seu laptop, neste codelab usaremos o Google Cloud Shell, um ambiente de linha de comando executado no Cloud.

O Cloud Shell é uma máquina virtual com base em Debian que contém todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Isso significa que tudo que você precisa para este codelab é um navegador (sim, funciona em um Chromebook).

  1. Para ativar o Cloud Shell no Console do Cloud, basta clicar em Ativar o Cloud Shell gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A. Leva apenas alguns instantes para provisionar e se conectar ao ambiente.

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

Screen Shot 2017-06-14 às 10.13.43 PM.png

Depois de se conectar ao Cloud Shell, você já estará autenticado e o projeto estará configurado com seu PROJECT_ID.

gcloud auth list

Resposta ao comando

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

Resposta ao comando

[core]
project = <PROJECT_ID>

Se, por algum motivo, o projeto não estiver definido, basta emitir o seguinte comando:

gcloud config set project <PROJECT_ID>

Quer encontrar seu PROJECT_ID? Veja qual ID você usou nas etapas de configuração ou procure-o no painel do Console do Cloud:

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

O Cloud Shell também define algumas variáveis de ambiente por padrão, o que pode ser útil ao executar comandos futuros.

echo $GOOGLE_CLOUD_PROJECT

Resposta ao comando

<PROJECT_ID>
  1. Defina a zona padrão e a configuração do projeto:
gcloud config set compute/zone us-central1-f

É possível escolher uma variedade de zonas diferentes. Para mais informações, consulte Regiões e zonas.

Resumo

Nesta etapa, você configurará seu ambiente.

A seguir

Agora configure uma instância do Cloud Spanner.

Nesta etapa, configuramos a instância do Cloud Spanner para este codelab. Pesquise a entrada do Spanner 1a6580bd3d3e6783.png no menu de navegação superior esquerdo 3129589f7bc9e5ce.png ou procure o Spanner pressionando "/" e digite "Spanner".

36e52f8df8e13b99.png

Depois, clique em 95269e75bc8c3e4d.png e preencha o formulário inserindo o nome da instância cloudspanner-leaderboard da sua instância, escolhendo uma configuração (selecione uma instância regional) e defina o número de nós para este codelab só precisaremos de um nó. Para instâncias de produção e se qualificar para o SLA do Cloud Spanner, você precisará executar três ou mais nós na sua instância do Cloud Spanner.

Por fim, mas não menos importante, clique em "Criar" e, em segundos, uma instância do Cloud Spanner está à sua disposição.

dceb68e9ed3801e8.png

Na próxima etapa, usaremos a biblioteca do cliente Go para criar um banco de dados e esquema em nossa nova instância.

Nesta etapa, criaremos nosso banco de dados e esquema de amostra.

Vamos usar a biblioteca de cliente Go para criar duas tabelas, uma tabela de jogadores para informações do jogador e uma tabela de pontuações para armazenar pontuações de jogadores. Para isso, mostraremos as etapas para criar um aplicativo de console Go no Cloud Shell.

Primeiro, clone o código de amostra do codelab digitando o seguinte comando no Cloud Shell:

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

Altere o diretório para o diretório "leaderboard" onde você criará seu aplicativo.

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

Todo o código necessário para este codelab está localizado no diretório golang-samples/spanner/spanner_leaderboard/ existente como um aplicativo Go executável como leaderboard para servir como referência enquanto você avança no codelab. Criaremos um novo diretório e criaremos uma cópia do aplicativo Leaderboard em etapas.

Crie um novo diretório chamado "codelab" para o aplicativo e altere o diretório nele para ele usando o seguinte comando:

mkdir codelab && cd $_

Agora vamos atualizar um aplicativo básico do Go chamado "Cebçalho", que usa a biblioteca de cliente do Spanner para criar um placar de formação de duas tabelas, jogadores e pontuações. É possível fazer isso diretamente no editor do Cloud Shell:

Abra o editor do Cloud Shell clicando no ícone destacado abaixo:

73cf70e05f653ca.png

Crie um arquivo chamado "leaderboard.go" na pasta ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab.

  • Primeiro, verifique se a pasta "codelab" está selecionada na lista de pastas do editor do Cloud Shell.
  • Em seguida, selecione "Novo arquivo" no menu "Arquivo" do editor do Cloud Shell.
  • Insira "leaderboard.go" como o nome do novo arquivo.

Este é o arquivo principal do aplicativo que conterá o nosso código e referências para incluir as dependências.

Para criar o banco de dados leaderboard e as tabelas Players e Scores, copie (Ctrl + P) e cole (Ctrl + V) o seguinte código Go no arquivo 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)
        }
}

Salve as alterações feitas no arquivo leaderboard.go selecionando "Salvar" no menu "Arquivo" do editor do Cloud Shell.

Use o arquivo leaderboard.go no diretório golang-samples/spanner/spanner_leaderboard para ver um exemplo de como seu arquivo leaderboard.go deve ficar depois de você adicionar o código para ativar o comando createdatabase.

Para criar seu aplicativo no Cloud Shell, execute "go build" no diretório codelab em que seu arquivo leaderboard.go está localizado:

go build leaderboard.go

Após a criação do aplicativo, execute o seguinte comando no Cloud Shell:

./leaderboard

O resultado será assim:

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.

Nesta resposta, vemos que este é o aplicativo Leaderboard que atualmente tem um comando possível: createdatabase. O argumento esperado do comando createdatabase é uma string que contém um ID de instância específico e um ID de banco de dados.

Agora, execute o seguinte comando. Verifique se você substitui my-project pelo ID do projeto que você criou no início deste codelab.

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

Após alguns segundos, será exibida uma resposta semelhante a esta:

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

Na seção Cloud Spanner do Console do Cloud, você verá o novo banco de dados e as tabelas que aparecem no menu do lado esquerdo.

ba9008bb84cb90b0.png

Na próxima etapa, atualizaremos o nosso aplicativo para carregar alguns dados no novo banco de dados.

Agora temos um banco de dados chamado leaderboard contendo duas tabelas; Players e Scores. Agora, usaremos a biblioteca de cliente Go para preencher a tabela Players com os jogadores e nossa tabela Scores com pontuações aleatórias para cada jogador.

Se ele ainda não estiver aberto, clique no ícone destacado abaixo para abrir o editor do Cloud Shell:

ef49fcbaaed19024.png

Em seguida, edite o arquivo leaderboard.go no editor do Cloud Shell para adicionar um comando insertplayers que pode ser usado para inserir 100 jogadores na tabela Players. Também adicionaremos um comando insertscores que pode ser usado para inserir quatro pontuações aleatórias na tabela Scores para cada jogador na tabela Players.

Primeiro, atualize a seção imports na parte superior do arquivo leaderboard.go, substituindo o que está lá de modo que, assim que terminar, você tenha a seguinte aparência:

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

Em seguida, adicione um novo tipo de comando com uma lista de comandos na parte superior do arquivo, logo abaixo da linha que começa com "type adminCommand ...". Assim, quando você terminar, o resultado será semelhante a este:

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

Em seguida, adicione as seguintes funções insertPlayers e insertScores abaixo da função createdatabase():

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
}

Em seguida, para fazer o comando insert funcionar, adicione o seguinte código à função "run" do seu aplicativo abaixo da instrução de manipulação createdatabase, substituindo a instrução 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

Quando você terminar, a função run será semelhante a esta:

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
}

A etapa final para adicionar a funcionalidade "inserir" ao seu aplicativo é adicionar o texto de ajuda aos comandos "insertplayers" e "insertscores" à função flag.Usage(). Adicione o texto de ajuda a seguir à função flag.Usage() para incluir o texto de ajuda para os comandos de inserção:

Adicione os dois comandos à lista de comandos possíveis:

Command can be one of: createdatabase, insertplayers, insertscores

E adicione este texto de ajuda abaixo do texto de ajuda para o 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.

Salve as alterações feitas no arquivo leaderboard.go selecionando "Salvar" no menu "Arquivo" do editor do Cloud Shell.

Use o arquivo leaderboard.go no diretório golang-samples/spanner/spanner_leaderboard para ver um exemplo de como seu arquivo leaderboard.go deve ficar depois de você adicionar o código para ativar os comandos insertplayers e insertscores.

Agora, vamos criar e executar o aplicativo para confirmar se os novos comandos insertplayers e insertscores estão incluídos na lista de comandos possíveis do aplicativo. Execute o seguinte comando para criar o aplicativo:

go build leaderboard.go

Execute o aplicativo resultante no Cloud Shell inserindo o seguinte comando:

./leaderboard

Você verá os comandos insertplayers e insertscores agora incluídos na saída padrão do aplicativo:

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.

Agora, vamos executar o comando insertplayers com os mesmos valores de argumento que usamos quando chamamos o comando createdatabase. Verifique se você substitui my-project pelo ID do projeto que você criou no início deste codelab.

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

Após alguns segundos, será exibida uma resposta semelhante a esta:

Inserted players

Agora, vamos usar a biblioteca de cliente Go para preencher nossa tabela Scores com quatro pontuações aleatórias com carimbos de data/hora de cada jogador na tabela Players.

A coluna Timestamp da tabela Scores foi definida como uma coluna de "carimbo de data/hora de confirmação" por meio da seguinte instrução SQL que foi executada quando executamos o comando create anteriormente:

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

Observe o atributo OPTIONS(allow_commit_timestamp=true). Isso torna Timestamp uma coluna de "timestamp de confirmação" e permite que ela seja preenchida automaticamente com o carimbo de data/hora exato da transação para operações INSERT e UPDATE em uma determinada linha da tabela.

Também é possível inserir seus próprios valores de carimbo de data/hora em uma coluna de "carimbo de data/hora de confirmação", desde que você insira um carimbo de data/hora com um valor que esteja no passado, que é o que vamos fazer para este fim de codelab.

Agora, vamos executar o comando insertscores com os mesmos valores de argumento que usamos quando chamamos o comando insertplayers. Verifique se você substitui my-project pelo ID do projeto que você criou no início deste codelab.

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

Após alguns segundos, será exibida uma resposta semelhante a esta:

Inserted scores

A execução da função insertScores usa o seguinte snippet de código para inserir um carimbo de data/hora gerado aleatoriamente com uma data/hora anterior ao passado:

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 preencher automaticamente a coluna Timestamp com o carimbo de data/hora do momento exato em que a transação "Inserir" ocorre, insira a constante Go spanner.CommitTimestamp como no snippet de código a seguir:

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

Agora que concluímos o carregamento de dados, vamos verificar os valores que acabamos de gravar nas nossas novas tabelas na seção Cloud Spanner do Console do Cloud. Primeiro, selecione o banco de dados leaderboard e depois a tabela Players. Clique na guia Data. Você verá dados nas colunas PlayerId e PlayerName da tabela.

7bc2c96293c31c49.png

Em seguida, para verificar se a tabela "Scores" também tem dados, clique na tabela Scores e selecione a guia Data. Você deve ter dados nas colunas PlayerId, Timestamp e Score da tabela.

d8a4ee4f13244c19.png

Parabéns! Vamos atualizar nosso app para executar algumas consultas que podemos usar para criar um placar de jogos.

Agora que configuramos o banco de dados e carregamos as informações nas tabelas, vamos criar um placar usando esses dados. Para fazer isso, precisamos responder a estas quatro perguntas:

  1. Quais jogadores são o "Top 10" de todos os tempos?
  2. Quais jogadores são os "Top 10" do ano?
  3. Quais jogadores são o "Top 10" do mês?
  4. Quais jogadores são os "Top 10" da semana?

Vamos atualizar nosso aplicativo para executar as consultas SQL que responderão a essas perguntas.

Adicionaremos os comandos query e queryWithTimespan que darão uma maneira de executar as consultas para responder às perguntas que produzirão as informações necessárias para nosso placar.

Edite o arquivo leaderboard.go no editor do Cloud Shell para atualizar o aplicativo e adicionar um comando query e um comando queryWithTimespan. Também adicionaremos uma função auxiliar formatWithCommas para formatar nossas pontuações com vírgulas.

Primeiro, atualize a seção imports na parte superior do arquivo leaderboard.go, substituindo o que está lá de modo que, assim que terminar, você tenha a seguinte aparência:

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

Em seguida, adicione as duas funções a seguir e a função auxiliar abaixo do método insertScores atual:

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

Na parte superior do arquivo leaderboard.go, adicione "consulta" como uma opção de comando na variável commands, logo abaixo da opção "insertscores": insertScores" para que a variável commands fique assim:

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

Adicione "queryWithTimespan" como uma opção de comando na função run, abaixo da seção de comando "createdatabase" e acima da seção de manipulação de comandos "insert and query":

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

Quando você terminar, a função run será semelhante a esta:

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
}

Em seguida, para tornar o comando queryWithTimespan funcional, atualize o bloco de código flag.Parse() no método "main" do aplicativo para que ele fique assim:

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

A etapa final para adicionar a funcionalidade "query" ao seu aplicativo é adicionar o texto de ajuda aos comandos "query" e "querywithtimespan" à função flag.Usage(). Adicione as linhas de código a seguir à função flag.Usage() para incluir o texto de ajuda para os comandos de consulta:

Adicione os dois comandos "query" à lista de comandos possíveis:

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

E adicione este texto de ajuda abaixo do texto de ajuda para o 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.

Salve as alterações feitas no arquivo leaderboard.go selecionando "Salvar" no menu "Arquivo" do editor do Cloud Shell.

Use o arquivo leaderboard.go no diretório golang-samples/spanner/spanner_leaderboard para ver um exemplo de como seu arquivo leaderboard.go deve ficar depois de você adicionar o código para ativar os comandos query e querywithtimespan.

Agora, vamos criar e executar o aplicativo para confirmar se os novos comandos query e querywithtimespan estão incluídos na lista de comandos possíveis do aplicativo.

Execute o seguinte comando no Cloud Shell para criar o aplicativo:

go build leaderboard.go

Execute o aplicativo resultante no Cloud Shell inserindo o seguinte comando:

./leaderboard

Você verá os comandos query e querywithtimespan agora incluídos na saída padrão do app como uma nova opção 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.

Você pode ver na resposta que é possível usar o comando query para ver uma lista dos nossos "Top 10 jogadores" de todos os tempos. Também é possível ver que o comando querywithtimespan nos permite especificar um período em número de horas a ser usado para filtrar registros com base no valor deles na coluna Timestamp da tabela Scores.

Execute o comando query usando os mesmos valores de argumento que usamos quando executamos o comando create Verifique se você substitui my-project pelo ID do projeto que você criou no início deste codelab.

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

Você deverá ver uma resposta que inclui os players "Top 10" de todos os tempos, como a seguinte:

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

Agora, vamos executar o comando querywithtimespan com os argumentos necessários para consultar os "Top 10" do ano especificando um "timespan" igual ao número de horas em um ano que é 8760. Verifique se você substitui my-project pelo ID do projeto que você criou no início deste codelab.

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

Você deverá ver uma resposta que inclui os "Top 10" do ano, como a seguir:

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

Agora, vamos executar o comando querywithtimespan para consultar os players "Top 10" do mês, especificando um "timespan" igual ao número de horas em um mês que é 730. Verifique se você substitui my-project pelo ID do projeto que você criou no início deste codelab.

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

Você verá uma resposta que inclui os "Top 10" do mês, como a seguinte:

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

Agora execute o comando querywithtimespan para consultar os "Top 10" da semana, especificando um "timespan" igual ao número de horas em uma semana de 168. Verifique se você substitui my-project pelo ID do projeto que você criou no início deste codelab.

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

Você verá uma resposta que inclui os jogadores "Top 10" da semana, como a seguinte:

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

Ótimo trabalho!

Agora, conforme você adicionar registros, o Cloud Spanner fará o escalonamento do seu banco de dados para o tamanho que for necessário. Não importa o quanto seu banco de dados aumente, o placar do jogo pode continuar a ser dimensionado com a precisão do Cloud Spanner e da tecnologia Truetime.

Depois de se divertir com o Spanner, precisamos limpar nosso playground, economizando recursos preciosos e dinheiro. Felizmente, esta é uma etapa fácil, basta acessar a seção Cloud Spanner do Console do Cloud e excluir a instância que criamos na etapa do codelab chamada "Setup a Cloud Spanner Instance".

O que vimos:

  • Instâncias, bancos de dados e esquema de tabelas do Google Cloud Spanner para um placar
  • Como criar o aplicativo Go Console
  • Como criar um banco de dados do Spanner e tabelas usando a biblioteca de cliente do Go
  • Como carregar dados em um banco de dados do Spanner usando a biblioteca de cliente Go
  • Como consultar os "Top 10" dos seus dados usando carimbos de data/hora de confirmação do Spanner e a biblioteca de cliente Go

Próximas etapas:

Envie um feedback

  • Reserve um momento para completar nossa pesquisa curta