Cloud Spanner: إنشاء لوحة صدارة للألعاب باستخدام Go

1. نظرة عامة

‫Google Cloud Spanner هي خدمة قواعد بيانات ارتباطية مُدارة بالكامل وقابلة للتوسّع أفقيًا وموزّعة على مستوى العالم، وتوفّر معاملات متوافقة مع ACID ودلالات SQL بدون التنازل عن الأداء العالي ومدى التوفّر العالي.

في هذا الدرس التطبيقي، ستتعرّف على كيفية إعداد مثيل Cloud Spanner. ستتّبع خطوات إنشاء قاعدة بيانات ومخطط يمكن استخدامهما في قائمة الصدارة الخاصة بالألعاب. ستبدأ بإنشاء جدول "اللاعبون" لتخزين معلومات اللاعبين وجدول "النتائج" لتخزين نتائج اللاعبين.

بعد ذلك، ستملأ الجداول ببيانات نموذجية. بعد ذلك، ستنهي الدرس التطبيقي من خلال تنفيذ بعض نماذج طلبات البحث العشرة الأوائل، ثم حذف الجهاز الظاهري لإتاحة الموارد.

ما ستتعلمه

  • كيفية إعداد مثيل 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 (الاسم أعلاه مستخدَم حاليًا ولن يكون متاحًا لك، نأسف لذلك). سيتم الإشارة إليه لاحقًا في هذا الدرس العملي باسم 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، وهي بيئة سطر أوامر تعمل في السحابة الإلكترونية.

يتم تحميل هذا الجهاز الافتراضي المستند إلى Debian بجميع أدوات التطوير التي تحتاج إليها. توفّر هذه الخدمة دليلًا رئيسيًا دائمًا بسعة 5 غيغابايت وتعمل في Google Cloud، ما يؤدي إلى تحسين أداء الشبكة والمصادقة بشكل كبير. وهذا يعني أنّ كل ما تحتاجه لهذا الدرس التطبيقي حول الترميز هو متصفّح (نعم، يمكن استخدامه على جهاز Chromebook).

  1. لتفعيل Cloud Shell من Cloud Console، ما عليك سوى النقر على تفعيل 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؟ يمكنك الاطّلاع على المعرّف الذي استخدمته في خطوات الإعداد أو البحث عنه في لوحة بيانات Cloud Console:

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 لهذا الدرس العملي. ابحث عن إدخال Spanner 1a6580bd3d3e6783.pngفي قائمة الهمبرغر في أعلى يمين الصفحة 3129589f7bc9e5ce.png أو ابحث عن Spanner بالضغط على "/" وكتابة "Spanner".

36e52f8df8e13b99.png

بعد ذلك، انقر على 19bb9864067757cb.png واملأ النموذج عن طريق إدخال اسم المثيل cloudspanner-leaderboard للمثيل، واختيار إعداد (اختر مثيلاً إقليميًا)، واضبط عدد العُقد، ولن نحتاج في هذا الدرس التطبيقي حول الترميز إلا إلى عقدة واحدة. بالنسبة إلى الآلات الافتراضية المخصّصة للإنتاج وللتأهّل لاتفاقية مستوى الخدمة في Cloud Spanner، عليك تشغيل 3 عُقد أو أكثر في آلة Cloud Spanner الافتراضية.

أخيرًا، انقر على "إنشاء" وستصبح لديك في غضون ثوانٍ مثيل Cloud Spanner تحت تصرّفك.

dceb68e9ed3801e8.png

في الخطوة التالية، سنستخدم مكتبة برامج Go لإنشاء قاعدة بيانات ومخطط في مثيلنا الجديد.

4. إنشاء قاعدة بيانات ومخطط

في هذه الخطوة، سننشئ قاعدة البيانات والنموذج الخاصين بنا.

لنستخدِم مكتبة برامج Go لإنشاء جدولَين: جدول Players لمعلومات اللاعبين وجدول Scores لتخزين نتائج اللاعبين. لإجراء ذلك، سنشرح خطوات إنشاء تطبيق وحدة تحكّم Go في Cloud Shell.

أولاً، استنسِخ الرمز النموذجي لهذا الدرس التطبيقي حول الترميز من GitHub عن طريق كتابة الأمر التالي في Cloud Shell:

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

بعد ذلك، غيِّر الدليل إلى دليل "قائمة الصدارة" حيث ستنشئ تطبيقك.

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

يقع كل الرمز البرمجي المطلوب لهذا الدرس التطبيقي حول الترميز في الدليل golang-samples/spanner/spanner_leaderboard/ الحالي كتطبيق Go قابل للتنفيذ باسم leaderboard ليكون مرجعًا لك أثناء تقدّمك في الدرس التطبيقي حول الترميز. سننشئ دليلاً جديدًا وننشئ نسخة من تطبيق "قائمة الصدارة" على مراحل.

أنشئ دليلاً جديدًا باسم "codelab" للتطبيق وانتقِل إلى الدليل باستخدام الأمر التالي:

mkdir codelab && cd $_

لننشئ الآن تطبيق Go أساسيًا باسم "Leaderboard" يستخدم مكتبة برامج Spanner لإنشاء قائمة صدارة تتألف من جدولَين: "اللاعبون" و"النتائج". يمكنك إجراء ذلك مباشرةً في "محرِّر Cloud Shell" باتّباع الخطوات التالية:

افتح "محرّر Cloud Shell" من خلال النقر على رمز "فتح المحرّر" المميّز أدناه:

7519d016b96ca51b.png

أنشئ ملفًا باسم "leaderboard.go" في المجلد ‎~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab.

  • تأكَّد أولاً من اختيار مجلد "codelab " في قائمة المجلدات في"محرّر Cloud Shell".
  • بعد ذلك، انقر على "ملف جديد" (New File) ضمن قائمة "ملف" (File) في "محرّر Cloud Shell" (Cloud Shell Editor).
  • أدخِل "leaderboard.go" كاسم للملف الجديد.

هذا هو الملف الرئيسي للتطبيق الذي سيحتوي على رمز التطبيق ومراجع لتضمين أي تبعيات.

لإنشاء قاعدة بيانات leaderboard وجدولَي Players وScores، انسخ (Ctrl + P) رمز Go التالي والصقه (Ctrl + V) في الملف 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".

يمكنك استخدام الملف 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 برقم تعريف المشروع الذي أنشأته في بداية هذا الدرس العملي.

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

بعد بضع ثوانٍ، من المفترض أن يظهر لك ردّ على النحو التالي:

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

في قسم "نظرة عامة على قواعد بيانات Cloud Spanner" في Cloud Console، من المفترض أن تظهر قاعدة البيانات والجداول الجديدة في القائمة على الجانب الأيمن.

a12fa65e352836b1.png

في الخطوة التالية، سنعدّل تطبيقنا لتحميل بعض البيانات إلى قاعدة البيانات الجديدة.

5- تحميل البيانات

لدينا الآن قاعدة بيانات باسم leaderboard تحتوي على جدولَين هما Players وScores. لنستخدِم الآن مكتبة برامج Go لملء الجدول Players باللاعبين والجدول Scores بالنتائج العشوائية لكل لاعب.

إذا لم يكن مفتوحًا من قبل، افتح "محرِّر Cloud Shell" من خلال النقر على الرمز المميّز أدناه:

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 و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 يعمل، أضِف الرمز التالي إلى دالة "التشغيل" في تطبيقك أسفل عبارة معالجة 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".

يمكنك استخدام الملف 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 برقم تعريف المشروع الذي أنشأته في بداية هذا الدرس العملي.

./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 في صف جدول معيّن.

يمكنك أيضًا إدراج قيم الطابع الزمني الخاصة بك في عمود "الطابع الزمني للتثبيت" (commit timestamp) طالما أنّك تُدرج طابعًا زمنيًا بقيمة في الماضي، وهو ما سنفعله لأغراض هذا الدرس البرمجي.

لننفّذ الآن الأمر insertscores باستخدام قيم الوسيطات نفسها التي استخدمناها عند استدعاء الأمر insertplayers. تأكَّد من استبدال my-project برقم تعريف المشروع الذي أنشأته في بداية هذا الدرس العملي.

./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 تلقائيًا بالطابع الزمني لوقت إجراء المعاملة "إدراج" بالضبط، يمكنك بدلاً من ذلك إدراج الثابت 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. تنفيذ طلبات البحث في قائمة الصدارة

بعد أن أعددنا قاعدة البيانات وحمّلنا المعلومات في جداولنا، لننشئ الآن قائمة صدارة باستخدام هذه البيانات. ولإجراء ذلك، علينا الإجابة عن الأسئلة الأربعة التالية:

  1. ما هي قائمة "أفضل عشرة لاعبين" على الإطلاق؟
  2. ما هي قائمة "أفضل عشرة لاعبين" لهذا العام؟
  3. ما هي "أفضل عشرة" لاعبين في الشهر؟
  4. ما هي "أفضل عشرة" لاعبين في الأسبوع؟

لنعدّل تطبيقنا لتنفيذ طلبات بحث SQL التي ستجيب عن هذه الأسئلة.

سنضيف الأمر query والأمر queryWithTimespan اللذين سيوفران طريقة لتنفيذ طلبات البحث للإجابة عن الأسئلة التي ستنتج المعلومات المطلوبة لقائمة الصدارة.

عدِّل الملف leaderboard.go في "محرّر Cloud Shell" لتعديل التطبيق من أجل إضافة الأمر query والأمر queryWithTimespan. سنضيف أيضًا دالة مساعدة 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"
)

بعد ذلك، أضِف الدالتَين التاليتَين ودالة المساعد أدناه إلى طريقة 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 يعمل، عدِّل مجموعة الرموز flag.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)
        }

الخطوة الأخيرة لإكمال إضافة وظيفة "طلب البحث" إلى تطبيقك هي إضافة نص مساعدة للأمرين "طلب البحث" و "طلب البحث مع نطاق زمني" إلى الدالة flag.Usage(). أضِف أسطر الرمز التالية إلى الدالة flag.Usage() لتضمين نص المساعدة لأوامر طلب البحث:

أضِف الأمرَين "query" إلى قائمة الأوامر المحتملة:

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

يمكنك استخدام الملف 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 برقم تعريف المشروع الذي أنشأته في بداية هذا الدرس العملي.

./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 برقم تعريف المشروع الذي أنشأته في بداية هذا الدرس العملي.

./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 برقم تعريف المشروع الذي أنشأته في بداية هذا الدرس العملي.

./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 برقم تعريف المشروع الذي أنشأته في بداية هذا الدرس العملي.

./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 Console وحذف المثيل الذي أنشأناه في خطوة codelab المسماة "إعداد مثيل Cloud Spanner".

8. تهانينا!

المواضيع التي تناولناها:

  • مثيلات وقواعد بيانات ومخطط جداول Google Cloud Spanner للوحات الصدارة
  • كيفية إنشاء تطبيق وحدة تحكّم Go
  • كيفية إنشاء قاعدة بيانات وجداول Spanner باستخدام مكتبة برامج Go للعملاء
  • كيفية تحميل البيانات إلى قاعدة بيانات Spanner باستخدام مكتبة برامج Go
  • كيفية طلب نتائج "أفضل 10" من بياناتك باستخدام الطوابع الزمنية لعمليات الإكمال في Spanner ومكتبة برامج Go

الخطوات التالية:

تقديم ملاحظاتك

  • يُرجى تخصيص بعض الوقت لإكمال الاستطلاع القصير جدًا.