Cloud Spanner: יוצרים לידרבורד של גיימינג באמצעות Go

1. סקירה כללית

Google Cloud Spanner הוא שירות מנוהל של מסד נתונים רלציוני בקנה מידה אופקי שאפשר להתאים לעומס, שזמין בכל העולם. השירות מספק טרנזקציות ACID וסמנטיקה של SQL בלי לוותר על ביצועים וזמינות גבוהה.

בשיעור ה-Lab הזה תלמדו איך להגדיר מכונת Cloud Spanner. תלמדו על השלבים ליצירת מסד נתונים וסכימה שניתן להשתמש בהם ליצירת לוח הישגי השחקנים המובילים של גיימינג. כדי להתחיל, צריך ליצור טבלת שחקנים לאחסון פרטי השחקנים וטבלת תוצאות לשמירת תוצאות של שחקנים.

בשלב הבא תאכלס את הטבלאות בנתונים לדוגמה. לאחר מכן, כשמסיימים את שיעור ה-Lab, מריצים כמה עשרת שאילתות מובילות לדוגמה, ולבסוף מוחקים את המכונה כדי לפנות משאבים.

מה תלמדו

  • איך מגדירים מכונה של Cloud Spanner.
  • איך ליצור מסד נתונים וטבלאות
  • איך משתמשים בעמודה של חותמת זמן של שמירה.
  • איך לטעון נתונים לטבלה של מסד הנתונים ב-Cloud Spanner עם חותמות זמן.
  • איך שולחים שאילתות על מסד הנתונים ב-Cloud Spanner.
  • איך למחוק את המכונה של Cloud Spanner.

W****כובע

איך תשתמשו במדריך הזה?

לקריאה בלבד לקרוא אותו ולבצע את התרגילים

איזה דירוג מגיע לחוויה שלך עם Google Cloud Platform?

מתחילים בינונית בקיאים

2. הגדרה ודרישות

הגדרת סביבה בקצב עצמאי

אם אין לכם עדיין חשבון Google (Gmail או Google Apps), עליכם ליצור חשבון. נכנסים למסוף Google Cloud Platform ( console.cloud.google.com) ויוצרים פרויקט חדש.

אם כבר יש לכם פרויקט, לוחצים על התפריט הנפתח לבחירת פרויקט בפינה השמאלית העליונה של המסוף:

6c9406d9b014760.png

ולוחצים על 'פרויקט חדש'. בתיבת הדו-שיח שמתקבלת כדי ליצור פרויקט חדש:

f708315ae07353d0.png

אם עדיין אין לכם פרויקט, אמורה להופיע תיבת דו-שיח כזו כדי ליצור את הפרויקט הראשון:

870a3cbd6541ee86.png

בתיבת הדו-שיח הבאה ליצירת פרויקט תוכלו להזין את פרטי הפרויקט החדש:

6a92c57d3250a4b3.png

חשוב לזכור את מזהה הפרויקט, שהוא שם ייחודי בכל הפרויקטים ב-Google Cloud (השם שלמעלה כבר תפוס ולא מתאים לכם, סליחה). בהמשך ב-Codelab הזה, היא תיקרא PROJECT_ID.

בשלב הבא, אם עדיין לא עשית זאת, יהיה עליך להפעיל את החיוב ב-Developers Console כדי להשתמש במשאבים של Google Cloud ולהפעיל את Cloud Spanner API.

15d0ef27a8fbab27.png

ההרצה של קוד ה-Codelab הזה לא אמורה לעלות לך יותר מכמה דולרים, אבל הוא יכול להיות גבוה יותר אם תחליטו להשתמש ביותר משאבים או אם תשאירו אותם פועלים (עיינו בקטע 'ניקוי' בסוף המסמך). התמחור ב-Google Cloud Spanner מופיע כאן.

משתמשים חדשים ב-Google Cloud Platform זכאים לתקופת ניסיון בחינם בשווי 300$, שמאפשרת ל-Codelab הזה להיות בחינם לחלוטין.

הגדרת Google Cloud Shell

אומנם אפשר להפעיל את Google Cloud ואת Spanner מרחוק מהמחשב הנייד, אבל ב-Codelab הזה נשתמש ב-Google Cloud Shell, סביבת שורת הפקודה שפועלת ב-Cloud.

המכונה הווירטואלית הזו שמבוססת על Debian נטענת עם כל הכלים למפתחים שדרושים לכם. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר משמעותית את ביצועי הרשת והאימות. כלומר, כל מה שדרוש ל-Codelab הזה הוא דפדפן (כן, הוא פועל ב-Chromebook).

  1. כדי להפעיל את Cloud Shell ממסוף Cloud, לוחצים על Activate Cloud Shell a8460e837e9f5fda.png (ההקצאה וההתחברות לסביבה אמורות להימשך כמה דקות).

b532b2f19ab85dda.png

צילום מסך מתאריך 2017-06-14 בשעה 22:13.43.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 שלך? אתם יכולים לבדוק באיזה מזהה השתמשתם בשלבי ההגדרה או לחפש אותו במרכז הבקרה של מסוף 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 ל-Codelab הזה. מחפשים את הערך Spanner 1a6580bd3d3e6783.png בתפריט ההמבורגר השמאלי 3129589f7bc9e5ce.png או מחפשים את Spanner על ידי הקשה על "/". ומקלידים 'Spanner'

36e52f8df8e13b99.png

בשלב הבא, לוחצים על 19b9864067757cb.png וממלאים את הטופס: מזינים את שם המכונה cloudspanner-leaderboard למכונה, בוחרים את ההגדרה (בחירת מכונה אזורית) ומגדירים את מספר הצמתים. בשביל Codelab הזה נצטרך רק צומת אחד. כדי לעמוד בדרישות של הסכם רמת השירות של Cloud Spanner למכונות בסביבת ייצור, צריך להריץ 3 צמתים או יותר במכונה של Cloud Spanner.

לסיום, לחצו על 'יצירה' ותוך שניות יש לכם מכונה של Cloud Spanner.

dceb68e9ed3801e8.png

בשלב הבא נשתמש בספריית הלקוח של Go כדי ליצור מסד נתונים וסכימה במכונה החדשה.

4. יצירת מסד נתונים וסכימה

בשלב הזה ניצור סכימה ומסד נתונים לדוגמה.

נשתמש בספריית הלקוח של Go כדי ליצור שתי טבלאות: טבלת שחקנים עם פרטי השחקנים וטבלת תוצאות לשמירת תוצאות של שחקנים. כדי לעשות את זה, נעבור על השלבים ליצירת אפליקציה במסוף Go ב-Cloud Shell.

קודם כול משכפלים מ-GitHub את הקוד לדוגמה של Codelab הזה באמצעות הקלדת הפקודה הבאה ב-Cloud Shell:

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

לאחר מכן יש לשנות את הספרייה ל-"Leaderboard" שבו תיצרו את האפליקציה שלכם.

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

כל הקוד הנדרש ל-Codelab הזה נמצא בספריית golang-samples/spanner/spanner_leaderboard/ הקיימת כאפליקציית Go שניתנת להרצה בשם leaderboard, ומשמשת כחומר עזר עם ההתקדמות ב-Codelab. אנחנו ניצור ספרייה חדשה ונבנה עותק של האפליקציה מסוג Leaderboard בשלבים.

יוצרים ספרייה חדשה בשם Codelab עבור האפליקציה ושינוי הספרייה אליה, באמצעות הפקודה הבאה:

mkdir codelab && cd $_

עכשיו ניצור אפליקציה בסיסית ב-Go בשם 'Leaderboard' שמשתמשת בספריית הלקוח Spanner כדי ליצור לידרבורד שמורכב משתי טבלאות; שחקנים ותוצאות. אפשר לעשות את זה ב-Cloud Shell Editor:

כדי לפתוח את Cloud Shell Editor, לוחצים על האפשרות Open Editor (פתיחת העורך). הסמל שמודגש למטה:

7519d016b96ca51b.png

יוצרים קובץ בשם 'Leaderboard.go' בתיקייה ~/gopath/src/github.com/GoogleCloudPlatform/golang-Sample/spanner/codelab.

  • קודם כל, צריך לוודא שמשתמשים ב-Codelab שנבחרה ברשימת התיקיות של Cloud Shell Editor.
  • לאחר מכן בוחרים באפשרות 'קובץ חדש' מתחת לקטע 'File' של Cloud Shell Editor. תפריט
  • מזינים 'Leaderboard.go'. בתור השם של הקובץ החדש.

זהו הקובץ הראשי של האפליקציה שיכיל את קוד האפליקציה שלנו והפניות שכוללות יחסי תלות.

כדי ליצור את מסד הנתונים leaderboard ואת הטבלאות Players ו-Scores, מעתיקים (Ctrl + P) ומדביקים (Ctrl + V) את קוד 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, יש ללחוץ על 'שמירה'. מתחת לקטע 'File' של Cloud Shell Editor. תפריט

אפשר להשתמש בקובץ leaderboard.go בספרייה golang-samples/spanner/spanner_leaderboard כדי לראות דוגמה לאופן שבו קובץ leaderboard.go אמור להיראות אחרי הוספת הקוד כדי להפעיל את הפקודה createdatabase.

כדי לפתח אפליקציות ב-Cloud Shell, צריך להריץ 'go build' מהספרייה codelab שבה נמצא הקובץ leaderboard.go:

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 הוא מחרוזת שמכילה מזהה מכונה ומזהה מסד נתונים ספציפיים.

עכשיו מריצים את הפקודה הבאה. חשוב להחליף את my-project במזהה הפרויקט שיצרתם בתחילת השיעור הזה ב-Codelab.

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

אחרי כמה שניות אתם אמורים לראות תגובה שדומה לזו:

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

בקטע הסקירה הכללית של מסדי נתונים ב-Cloud Spanner במסוף Cloud תוכלו לראות את הטבלאות ומסד הנתונים החדשים מופיעים בתפריט שבצד שמאל.

a12fa65e352836b1.png

בשלב הבא נעדכן את האפליקציה שלנו כדי לטעון חלק מהנתונים למסד הנתונים החדש.

5. טען נתונים

יש לנו עכשיו מסד נתונים בשם leaderboard שמכיל שתי טבלאות; Players וגם Scores עכשיו נשתמש בספריית הלקוח של Go כדי לאכלס את הטבלה Players בשחקנים ואת הטבלה Scores בניקוד אקראי לכל שחקן.

אם עדיין לא פתחתם את Cloud Shell Editor, לוחצים על הסמל המודגש בהמשך כדי לפתוח את ה-Cloud Shell Editor:

7519d016b96ca51b.png

בשלב הבא, עורכים את הקובץ leaderboard.go ב-Cloud Shell Editor כדי להוסיף פקודת insertplayers שאפשר להשתמש בה כדי להוסיף 100 נגנים לטבלה Players. נוסיף גם פקודת insertscores שניתן להשתמש בה כדי להוסיף 4 ניקוד אקראי לטבלה Scores עבור כל שחקן בטבלה Players.

קודם מעדכנים את הקטע imports בחלק העליון של קובץ leaderboard.go, ומחליפים את הקטע הנוכחי כך שבסיום התהליך הוא ייראה כך:

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, ומוסיפים ניקוד מתחת לפונקציה 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
}

השלב האחרון להשלמת הוספת "insert" פונקציונליות לאפליקציה שלך היא הוספת טקסט עזרה עבור "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, יש ללחוץ על 'שמירה'. מתחת לקטע 'File' של Cloud Shell Editor. תפריט

אפשר להשתמש בקובץ leaderboard.go בספרייה golang-samples/spanner/spanner_leaderboard כדי לראות דוגמה לאופן שבו קובץ leaderboard.go ייראה אחרי הוספת הקוד כדי להפעיל את הפקודות insertplayers ו-insertscores.

עכשיו נבנה ונריץ את האפליקציה, כדי לאשר שהפקודות החדשות insertplayers ו-insertscores נכללות ברשימת הפקודות האפשריות של האפליקציה. מריצים את הפקודה הבאה כדי ליצור את האפליקציה:

go build leaderboard.go

מריצים את האפליקציה שמתקבלת ב-Cloud Shell על ידי הזנת הפקודה הבאה:

./leaderboard

הפקודות insertplayers ו-insertscores אמורות להופיע עכשיו בפלט ברירת המחדל של האפליקציה:

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.

עכשיו נריץ את הפקודה insertplayers עם אותם ערכי ארגומנטים שבהם השתמשנו כשקראנו לפקודה createdatabase. חשוב להחליף את my-project במזהה הפרויקט שיצרתם בתחילת השיעור הזה ב-Codelab.

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

אחרי כמה שניות אתם אמורים לראות תגובה שדומה לזו:

Inserted players

עכשיו נשתמש בספריית הלקוח של Go כדי לאכלס את הטבלה Scores בארבעה ציונים אקראיים יחד עם חותמות זמן לכל שחקן בטבלה Players.

העמודה Timestamp בטבלה Scores הוגדרה בתור 'חותמת זמן של התחייבות' באמצעות הצהרת ה-SQL הבאה שהרצתנו כשהפעלנו בעבר את הפקודה 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

שימו לב למאפיין OPTIONS(allow_commit_timestamp=true). הפעולה הזו הופכת את Timestamp ל'חותמת זמן של ההתחייבות' מאפשר לאכלס אותה באופן אוטומטי בחותמת הזמן המדויקת של העסקה עבור פעולות INSERT ו-UPDATE בשורה נתונה בטבלה.

אתם יכולים גם להוסיף ערכים של חותמת זמן משלכם ל'חותמת זמן של ההתחייבות'. כל עוד מזינים חותמת זמן עם ערך שכבר עבר. זה מה שנעשה במסגרת ה-Codelab הזה.

עכשיו נריץ את הפקודה insertscores עם אותם ערכי ארגומנטים שבהם השתמשנו כשקראנו לפקודה insertplayers. חשוב להחליף את my-project במזהה הפרויקט שיצרתם בתחילת השיעור הזה ב-Codelab.

./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' מתבצעת עסקה, אפשר במקום זאת להוסיף את הקבוע spanner.CommitTimestamp של Go, כמו בקטע הקוד הבא:

...
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 Spanner ב-Cloud Console. קודם צריך לבחור את מסד הנתונים leaderboard ואז לבחור את הטבלה Players. לוחצים על הכרטיסייה Data. בעמודות PlayerId ו-PlayerName של הטבלה אמורים להופיע נתונים.

86dc5b927809a4ec.png

עכשיו נוודא שגם הטבלה 'ציונים' כוללת נתונים. לשם כך, לוחצים על הטבלה Scores ובוחרים בכרטיסייה Data. אמורים לראות שיש נתונים בעמודות PlayerId, Timestamp ו-Score של הטבלה.

87c8610c92d3c612.png

כל הכבוד! נעדכן את האפליקציה שלנו כך שנריץ כמה שאילתות שבהן נוכל להשתמש כדי ליצור לוח הישגי השחקנים המובילים של משחקים.

6. הפעלת שאילתות בלוח הישגי השחקנים המובילים

עכשיו, אחרי שהגדרנו את מסד הנתונים וטען מידע בטבלאות שלנו, בואו ניצור Leaderboard על סמך הנתונים האלה. לשם כך, עלינו לענות על ארבע השאלות הבאות:

  1. אילו שחקנים הם "עשרת המובילים" מכל הזמנים?
  2. אילו שחקנים הם "עשרת המובילים" של השנה?
  3. אילו שחקנים הם "עשרת המובילים" בחודש?
  4. אילו שחקנים הם "עשרת המובילים" בשבוע?

נעדכן את האפליקציה שלנו ונריץ את שאילתות ה-SQL שיענו על השאלות האלה.

אנחנו נוסיף פקודת query ופקודת queryWithTimespan שתספק דרך להריץ את השאילתות כדי לענות על השאלות שיספקו את המידע הנדרש ל-Leaderboard שלנו.

כדי לעדכן את האפליקציה ולהוסיף פקודת query ופקודת queryWithTimespan, עורכים את הקובץ leaderboard.go ב-Cloud Shell Editor. נוסיף גם פונקציית עזר מסוג formatWithCommas כדי לעצב את הניקוד שלנו באמצעות פסיקים.

קודם מעדכנים את הקטע imports בחלק העליון של קובץ leaderboard.go, ומחליפים את הקטע הנוכחי כך שבסיום התהליך הוא ייראה כך:

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

לאחר מכן מוסיפים את שתי הפונקציות הבאות ואת פונקציית העזר מתחת ל-method 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, מוסיפים 'שאילתה' בתור אפשרויות של פקודה אחת במשתנה 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 תפעל, מעדכנים את בלוק הקוד דגל.Parse() בקטע 'main' של האפליקציה כדי שהיא תיראה כך:

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

השלב האחרון בתהליך הוספת "query" הפונקציונליות של האפליקציה היא להוסיף טקסט עזרה עבור ה"שאילתה" ו-"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, יש ללחוץ על 'שמירה'. מתחת לקטע 'File' של Cloud Shell Editor. תפריט

אפשר להשתמש בקובץ leaderboard.go בספרייה golang-samples/spanner/spanner_leaderboard כדי לראות דוגמה לאופן שבו קובץ leaderboard.go ייראה אחרי הוספת הקוד כדי להפעיל את הפקודות query ו-querywithtimespan.

עכשיו נבנה ונריץ את האפליקציה, כדי לאשר שהפקודות החדשות query ו-querywithtimespan נכללות ברשימת הפקודות האפשריות של האפליקציה.

כדי ליצור את האפליקציה, מריצים את הפקודה הבאה ב-Cloud Shell:

go build leaderboard.go

מריצים את האפליקציה שמתקבלת ב-Cloud Shell על ידי הזנת הפקודה הבאה:

./leaderboard

הפקודות query ו-querywithtimespan אמורות להופיע עכשיו בפלט ברירת המחדל של האפליקציה כאפשרות פקודה חדשה:

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 מאפשרת לנו לציין טווח זמן במספר שעות שישמש לסינון רשומות לפי הערך שלהן בעמודה Timestamp בטבלה Scores.

נריץ את הפקודה query עם אותם ערכי ארגומנטים ששימשו אותנו כשהרצנו את הפקודה create. חשוב להחליף את my-project במזהה הפרויקט שיצרתם בתחילת השיעור הזה ב-Codelab.

./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 במזהה הפרויקט שיצרתם בתחילת השיעור הזה ב-Codelab.

./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 במזהה הפרויקט שיצרתם בתחילת השיעור הזה ב-Codelab.

./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 במזהה הפרויקט שיצרתם בתחילת השיעור הזה ב-Codelab.

./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, אנחנו צריכים לנקות את מגרש המשחקים שלנו ולחסוך משאבים יקרים וכסף. למרבה המזל, זה שלב פשוט: נכנסים לקטע של Cloud Spanner במסוף Cloud ומוחקים את המכונה שיצרנו בשלב ה-Codelab בשם 'Setup a Cloud Spanner Instance'.

8. מעולה!

נושאים שטיפלנו בנושא:

  • מכונות של Google Cloud Spanner, מסדי נתונים וסכימת טבלאות ל-Leaderboard
  • איך יוצרים אפליקציה במסוף Go
  • איך ליצור מסד נתונים וטבלאות של Spanner באמצעות ספריית הלקוח של Go
  • איך לטעון נתונים למסד נתונים Spanner באמצעות ספריית הלקוח של Go
  • איך שולחים שאילתה על 'עשרת המובילים' תוצאות מהנתונים שלך באמצעות חותמות זמן של שמירה ב-Spanner וספריית הלקוח של Go

השלבים הבאים:

נשמח לקבל ממך משוב

  • כדאי למלא את הסקר הקצר מאוד שלנו