Cloud Spanner:使用 Go 建立遊戲排行榜

1. 總覽

Google Cloud Spanner 是全代管且遍及全球的關聯資料庫服務,提供 ACID 交易和 SQL 語意,但不會犧牲效能及高可用性。

在本研究室中,您將瞭解如何設定 Cloud Spanner 執行個體。並逐步建立可用於遊戲排行榜的資料庫和結構定義。第一步是建立用於儲存玩家資訊的「玩家」表格,以及用來儲存玩家得分的「得分」表格。

接下來要將範例資料填入資料表。接著,您將於課程結束之前執行一些前十大範例查詢,最後刪除執行個體來釋出資源。

課程內容

  • 如何設定 Cloud Spanner 執行個體。
  • 如何建立資料庫和資料表。
  • 如何使用修訂時間戳記欄。
  • 如何將含有時間戳記的資料載入 Cloud Spanner 資料庫資料表。
  • 如何查詢 Cloud Spanner 資料庫。
  • 如何刪除 Cloud Spanner 執行個體。

祝一切順心!

您會如何使用這個教學課程?

僅供閱讀 閱讀並完成練習

您對 Google Cloud Platform 的使用體驗有何評價?

新手 中級 還算容易

2. 設定和需求

自修環境設定

如果您還沒有 Google 帳戶 (Gmail 或 Google Apps),請先建立帳戶。登入 Google Cloud Platform 控制台 ( console.cloud.google.com),並建立新專案。

如果您已有專案,請按一下控制台左上方的專案選取下拉式選單:

6c9406d9b014760.png

並點選 [新增專案]按鈕,用於建立新專案:

f708315ae07353d0.png

如果您還沒有專案,系統會顯示如下的對話方塊,讓您建立第一個專案:

870a3cbd6541ee86.png

後續的專案建立對話方塊可讓您輸入新專案的詳細資料:

6a92c57d3250a4b3.png

請記住,專案 ID 在所有的 Google Cloud 專案中是不重複的名稱 (已經有人使用上述名稱,目前無法為您解決問題!)。稍後在本程式碼研究室中會稱為 PROJECT_ID

接下來,如果您尚未在 Developers Console 中啟用計費功能,必須完成此步驟,才能使用 Google Cloud 資源並啟用 Cloud Spanner API

15d0ef27a8fbab27.png

執行本程式碼研究室所需的費用不應超過數美元,但如果您決定使用更多資源,或讓這些資源繼續運作,費用會增加 (請參閱本文件結尾的「清理」一節)。如需 Google Cloud Spanner 的定價資訊,請參閱這裡

Google Cloud Platform 的新使用者符合 $300 美元的免費試用資格,應該可以免費使用本程式碼研究室。

Google Cloud Shell 設定

雖然 Google Cloud 和 Spanner 可以在筆記型電腦上遠端運作,但在本程式碼研究室中,我們會使用 Google Cloud Shell,這是一種在 Cloud 中執行的指令列環境。

這種以 Debian 為基礎的虛擬機器,搭載各種您需要的開發工具。提供永久的 5 GB 主目錄,而且在 Google Cloud 中運作,大幅提高網路效能和驗證能力。換言之,本程式碼研究室只需要在 Chromebook 上運作即可。

  1. 如要透過 Cloud 控制台啟用 Cloud Shell,只要點選「啟用 Cloud Shell」 圖示 a8460e837e9f5fda.png 即可 (整個佈建作業只需幾分鐘的時間,操作完畢即可)。

b532b2f19ab85dda.png

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

連線至 Cloud Shell 後,您應會發現自己通過驗證,且專案已設為 PROJECT_ID

gcloud auth list

指令輸出

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

指令輸出

[core]
project = <PROJECT_ID>

如因故未設定專案,請直接發出以下指令:

gcloud config set project <PROJECT_ID>

正在尋找 PROJECT_ID 嗎?查看您在設定步驟中使用的 ID,或在 Cloud 控制台資訊主頁查詢:

2485e00c1223af09.png

根據預設,Cloud Shell 也會設定一些環境變數,方便您之後執行指令。

echo $GOOGLE_CLOUD_PROJECT

指令輸出

<PROJECT_ID>
  1. 最後,進行預設可用區和專案設定。
gcloud config set compute/zone us-central1-f

您可以選擇各種不同的可用區。詳情請參閱「區域與可用區

摘要

在這個步驟中,您會設定環境。

下一步

接下來,您將設定 Cloud Spanner 執行個體。

3. 設定 Cloud Spanner 執行個體

在這個步驟中,我們會為本程式碼研究室設定 Cloud Spanner 執行個體。請在左上方的漢堡選單 1a6580bd3d3e6783.png3129589f7bc9e5ce.png 搜尋 Spanner 項目,或按下「/」搜尋 Spanner然後輸入「Spanner」

36e52f8df8e13b99.png

接下來,按一下 19bb9864067757cb.png,然後為執行個體輸入執行個體名稱 cloudspanner-leaderboard、選擇設定 (選取區域執行個體),並設定節點數量,藉此填寫表單。這個程式碼研究室只需要 1 個節點。如需實際工作環境執行個體,以及符合 Cloud Spanner 服務水準協議的資格,您必須在 Cloud Spanner 執行個體中執行 3 個以上的節點。

最後,點選 [建立]而且您能在幾秒內使用 Cloud Spanner 執行個體

dceb68e9ed3801e8.png

在下一個步驟中,我們將使用 Go 用戶端程式庫,在新的執行個體中建立資料庫和結構定義。

4. 建立資料庫和結構定義

在這個步驟中,我們會建立範例資料庫和結構定義。

讓我們使用 Go 用戶端程式庫建立兩個資料表:玩家資訊表以及儲存玩家分數的得分錶格。因此,我們將逐步介紹在 Cloud Shell 中建立 Go 控制台應用程式的步驟。

請先在 Cloud Shell 輸入下列指令,從 GitHub 複製本程式碼研究室的程式碼範例:

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

然後將目錄變更為「leaderboard」(排行榜),以便建立應用程式。

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

本程式碼研究室所需的所有程式碼都位於現有的 golang-samples/spanner/spanner_leaderboard/ 目錄中,做為名為 leaderboard 的可執行 Go 應用程式,可在您進行本程式碼研究室時做為參考。我們將分階段建立新的目錄,並分階段建立排行榜應用程式副本。

建立名為「codelab」的新目錄,並使用下列指令將目錄變更為該目錄:

mkdir codelab && cd $_

現在,請建立名為「Leaderboard」(排行榜) 的基本 Go 應用程式。並使用 Spanner 用戶端程式庫建立由兩個資料表組成的排行榜。玩家和分數:您可以直接在 Cloud Shell 編輯器中執行這項操作:

按一下「開啟編輯器」下方醒目顯示的圖示:

7519d016b96ca51b.png

建立名為「leaderboard.go」的檔案,該檔案位於 ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab 資料夾中。

  • 首先,請確認您具備「程式碼研究室」已選取資料夾。
  • 接著選取「新增檔案」查看 Cloud Shell 編輯器的或前往 Google 試算表選單
  • 輸入「leaderboard.go」做為新檔案的名稱。

這是應用程式的主要檔案,內含應用程式程式碼,以及可納入所有依附元件的參照。

如要建立 leaderboard 資料庫以及 PlayersScores 資料表,請複製 (Ctrl + P) 並將下列 Go 程式碼貼到 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)
        }
}

選取「儲存」,儲存您對 leaderboard.go 檔案所做的變更查看 Cloud Shell 編輯器的或前往 Google 試算表選單

您可以在 golang-samples/spanner/spanner_leaderboard 目錄中使用 leaderboard.go 檔案,查看啟用 createdatabase 指令的程式碼後,leaderboard.go 檔案應如何呈現。

如要在 Cloud Shell 中建構應用程式,請執行「go build」從 leaderboard.go 檔案所在的 codelab 目錄中:

go build leaderboard.go

成功建構應用程式後,請輸入下列指令,在 Cloud Shell 中執行產生的應用程式:

./leaderboard

您會看到如下所示的輸出:

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.

從此回應中,我們可以看到這是 Leaderboard 應用程式,目前有一個可能的指令:createdatabase。我們可以看到 createdatabase 指令的預期引數是包含特定執行個體 ID 和資料庫 ID 的字串。

接著執行下列指令。請確認您將 my-project 替換成您在本程式碼研究室一開始建立的專案 ID。

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

幾秒後,您應該會看到類似下方的回應:

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

在 Cloud 控制台的 Cloud Spanner 資料庫總覽專區,左側選單中應會顯示新的資料庫和資料表,

a12fa65e352836b1.png

在下一個步驟中,我們會更新應用程式,將一些資料載入新的資料庫。

5. 載入資料

我們現在有一個名為 leaderboard 的資料庫,內含兩份資料表;PlayersScores。現在,我們要使用 Go 用戶端程式庫,在 Players 資料表中填入玩家和 Scores 資料表,其中含有每位玩家的隨機分數。

如果 Cloud Shell 編輯器尚未開啟,請點選下方醒目標示的圖示開啟 Cloud Shell 編輯器:

7519d016b96ca51b.png

接著,請在 Cloud Shell 編輯器中編輯 leaderboard.go 檔案,新增 insertplayers 指令,該指令可用於在 Players 資料表中插入 100 個玩家。我們也會新增 insertscores 指令,用於在 Scores 資料表中為每位玩家插入 4 個隨機得分。Players

首先,更新 leaderboard.go 檔案頂端的 imports 部分,取代目前的部分,完成後完成後看起來會像這樣:

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

接下來請新增一個指令類型,並在檔案頂端顯示指令清單,也就是該行開頭為「type adminCommand ...」的那一行。完成後,程式碼會如下所示:

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

接下來,新增下列 insertPlayers 和 insertScores 在現有 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
        }
        // Initialize values for random PlayerId
        rand.Seed(time.Now().UnixNano())
        min := 1000000000
        max := 9000000000
        // Insert 100 player records into the Players table
        _, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
                stmts := []spanner.Statement{}
                for i := 1; i <= 100; i++ {
                        numberOfPlayers++
                        playerID := rand.Intn(max-min) + min
                        playerName := fmt.Sprintf("Player %d", numberOfPlayers)
                        stmts = append(stmts, spanner.Statement{
                                SQL: `INSERT INTO Players
                                                (PlayerId, PlayerName)
                                                VALUES (@playerID, @playerName)`,
                                Params: map[string]interface{}{
                                        "playerID":   playerID,
                                        "playerName": playerName,
                                },
                        })
                }
                _, err := txn.BatchUpdate(ctx, stmts)
                if err != nil {
                        return err
                }
                return nil
        })
        fmt.Fprintf(w, "Inserted players \n")
        return nil
}

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

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

接著,為了讓 insert 指令正常運作,請將下列程式碼加到應用程式的「run」中函式 (位於 createdatabase 處理陳述式下方),取代 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

完成之後,run 函式應如下所示:

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
}

最後一步是新增「插入」那就是為「insertplayers」加入說明文字和「insertscores」提供給 flag.Usage() 函式的指令。在 flag.Usage() 函式中加入下列說明文字,加入用於插入指令的說明文字:

將兩個指令加入可能的指令清單:

Command can be one of: createdatabase, insertplayers, insertscores

並在 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.

選取「儲存」,儲存您對 leaderboard.go 檔案所做的變更查看 Cloud Shell 編輯器的或前往 Google 試算表選單

您可以透過 golang-samples/spanner/spanner_leaderboard 目錄中的 leaderboard.go 檔案,查看啟用 insertplayersinsertscores 指令的程式碼後,leaderboard.go 檔案應如何呈現的範例。

現在,請建構並執行應用程式,確認新的 insertplayersinsertscores 指令已納入應用程式的可能指令清單中。執行下列指令來建構應用程式:

go build leaderboard.go

輸入下列指令,在 Cloud Shell 中執行產生的應用程式:

./leaderboard

應用程式的預設輸出內容應會顯示 insertplayersinsertscores 指令:

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.

現在,使用我們呼叫 createdatabase 指令時使用的相同引數值執行 insertplayers 指令。請確認您將 my-project 替換成您在本程式碼研究室一開始建立的專案 ID。

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

幾秒後,您應該會看到類似下方的回應:

Inserted players

現在,我們要使用 Go 用戶端程式庫,在 Scores 資料表中填入四個隨機分數,以及 Players 資料表中每位玩家的時間戳記。

Scores 資料表的「Timestamp」欄定義為「修訂時間戳記」如下所示,該陳述式為在我們之前執行 create 指令時執行的下列 SQL 陳述式:

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

請注意 OPTIONS(allow_commit_timestamp=true) 屬性。這會讓 Timestamp 成為「修訂時間戳記」資料欄,並在特定表格列上自動填入 INSERT 和 UPDATE 作業的確切交易時間戳記。

您也可以將自己的時間戳記值插入「修訂時間戳記」欄中,只要您插入時間戳記值在過去的時間戳記,我們就會針對本程式碼研究室的用途。

現在,使用我們呼叫 insertplayers 指令時使用的相同引數值執行 insertscores 指令。請確認您將 my-project 替換成您在本程式碼研究室一開始建立的專案 ID。

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

幾秒後,您應該會看到類似下方的回應:

Inserted scores

執行 insertScores 函式時,使用下列程式碼片段,插入隨機產生的時間戳記,且時間戳記必須在過去:

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

Timestamp 欄中自動填入「Insert」的確切時間交易發生,您可以改為插入 Go 常數 spanner.CommitTimestamp,如以下程式碼片段所示:

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

資料載入完畢後,接著請前往 Cloud 控制台的 Cloud Spanner 部分,驗證剛才寫入新資料表的值。請先選取 leaderboard 資料庫,然後選取 Players 資料表。按一下「Data」分頁標籤。您應該會在表格的 PlayerIdPlayerName 欄中看到資料。

86dc5b927809a4ec.png

接著,請按一下 Scores 資料表並選取「Data」分頁標籤,確認「分數」表格也有資料。您應該會在表格的 PlayerIdTimestampScore 欄中看到資料。

87c8610c92d3c612.png

非常好!讓我們更新應用程式,以便執行一些查詢,以便建立遊戲排行榜。

6. 執行排行榜查詢

資料庫設定完成,並將資訊載入表格後,我們就來使用這些資料建立排行榜吧。為此,請回答下列四個問題:

  1. 哪些玩家是「前十名」而非時間
  2. 哪些玩家是「前十名」該怎麼辦?
  3. 哪些玩家是「前十名」每月?
  4. 哪些玩家是「前十名」一週?

讓我們更新應用程式,執行 SQL 查詢來回答這些問題。

我們將新增 query 指令和 queryWithTimespan 指令,藉此執行查詢,找出會產生排行榜所需資訊的問題。

在 Cloud Shell 編輯器中編輯 leaderboard.go 檔案,藉此更新應用程式,加入 query 指令和 queryWithTimespan 指令。我們也要新增 formatWithCommas 輔助函式,以便以半形逗號分隔分數格式。

首先,更新 leaderboard.go 檔案頂端的 imports 部分,取代目前的部分,完成後完成後看起來會像這樣:

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

接下來,在現有 insertScores 方法的下方新增下列兩個函式和輔助函式:

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

接著在 leaderboard.go 檔案的頂端新增「query」做為 commands 變數中的一個指令選項,位於「insertscores": insertScores」選項的下方,因此 commands 變數如下所示:

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

接著,新增「queryWithTimespan」做為指令選項,位於 run 函式的「createdatabase」下方位於「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
        }

完成之後,run 函式應如下所示:

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
}

接著,為了讓 queryWithTimespan 指令正常運作,請更新應用程式「main」中的旗標.Parse() 程式碼區塊方法,使其如下所示:

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

最後一個步驟是新增「查詢」新增「查詢」說明文字和「querywithtimespan」提供給 flag.Usage() 函式的指令。在 flag.Usage() 函式中加入以下這行程式碼,即可加入查詢指令的說明文字:

將兩個「查詢」相加指令傳送至可能的指令清單:

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

並在 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.

選取「儲存」,儲存您對 leaderboard.go 檔案所做的變更查看 Cloud Shell 編輯器的或前往 Google 試算表選單

您可以透過 golang-samples/spanner/spanner_leaderboard 目錄中的 leaderboard.go 檔案,查看啟用 queryquerywithtimespan 指令的程式碼後,leaderboard.go 檔案應如何呈現的範例。

現在,請建構並執行應用程式,確認新的 queryquerywithtimespan 指令已納入應用程式的可能指令清單中。

在 Cloud Shell 中執行下列指令來建構應用程式:

go build leaderboard.go

輸入下列指令,在 Cloud Shell 中執行產生的應用程式:

./leaderboard

應用程式的預設輸出內容應會顯示 queryquerywithtimespan 指令,做為新指令選項:

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.

您可以在回應中看到,我們可以使用 query 指令取得「前十大」的清單隨時都有可能獲得玩家青睞我們還可以看到 querywithtimespan 指令能讓我們根據 Scores 資料表 Timestamp 欄中的值,指定要用於篩選記錄的時間範圍 (以小時為單位)。

我們使用執行 create 指令時使用的相同引數值執行 query 指令。請確認您將 my-project 替換成您在本程式碼研究室一開始建立的專案 ID。

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

您應該會看到一則包含「前十大」的回覆例如:

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

現在使用必要引數執行 querywithtimespan 指令,查詢「前十名」方法是指定「時間範圍」等於一年中的時數為 8760。請確認您將 my-project 替換成您在本程式碼研究室一開始建立的專案 ID。

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

您應該會看到一則包含「前十大」的回覆年度玩家名單:

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

現在,執行 querywithtimespan 指令來查詢「前十名」方法是指定「時間範圍」等於當月的小時數 (730)。請確認您將 my-project 替換成您在本程式碼研究室一開始建立的專案 ID。

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

您應該會看到一則包含「前十大」的回覆每月玩家的名單如下:

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

現在,執行 querywithtimespan 指令來查詢「前十名」方法是指定「時間範圍」等於一週的小時數 (168)。請確認您將 my-project 替換成您在本程式碼研究室一開始建立的專案 ID。

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

您應該會看到一則包含「前十大」的回覆每週的玩家,如下所示:

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

太棒了!

現在您新增記錄時,Cloud Spanner 就會視需求擴充資料庫的資源配置。無論資料庫數量增加,遊戲排行榜都能透過 Cloud Spanner 和 Truetime 技術,持續擴充準確率。

7. 清除

享受到 Spanner 帶來的樂趣後,我們需要清理 Playground,節省寶貴的資源和費用。別擔心,這個步驟很簡單,只要前往 Cloud 控制台的 Cloud Spanner 部分,然後刪除我們在「設定 Cloud Spanner 執行個體」程式碼研究室步驟中建立的執行個體即可。

8. 恭喜!

本文涵蓋內容:

  • 排行榜的 Google Cloud Spanner 執行個體、資料庫和資料表結構定義
  • 如何建立 Go 主控台應用程式
  • 如何使用 Go 用戶端程式庫建立 Spanner 資料庫和資料表
  • 如何使用 Go 用戶端程式庫將資料載入 Spanner 資料庫
  • 如何查詢「前十名」運用 Spanner 修訂時間戳記和 Go 用戶端程式庫,從資料中取得結果

後續步驟:

請提供您寶貴的意見