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

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

تذكَّر رقم تعريف المشروع، وهو اسم فريد في جميع مشاريع 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 gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A (يستغرق توفير البيئة والاتصال بها بضع لحظات فقط).

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

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:

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

يضبط 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

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

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

dceb68e9ed3801e8.png

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

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

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

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

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

git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

بعد ذلك، غيِّر الدليل إلى دليل "التطبيقات" (applications) الذي ستنشئ فيه تطبيقك.

cd java-docs-samples/spanner/leaderboard

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

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

mkdir codelab && cd $_

أنشِئ تطبيق Java أساسيًا جديدًا باسم "Leaderboard" باستخدام أمر Maven (mvn) التالي:

mvn -B archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.google.codelabs -DartifactId=leaderboard -DarchetypeVersion=1.4

ينشئ هذا الأمر تطبيقًا بسيطًا لوحدة التحكّم يتألف من ملفَين أساسيَين، وهما ملف إعداد تطبيق Maven pom.xml وملف تطبيق Java App.java.

بعد ذلك، انتقِل إلى دليل قائمة الصدارة الذي تم إنشاؤه للتو وأدرِج محتوياته:

cd leaderboard && ls

من المفترض أن يظهر لك الملف pom.xml والدليل src على النحو التالي:

pom.xml  src

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

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

73cf70e05f653ca.png

افتح ملف pom.xml ضِمن مجلد leaderboard. افتح ملف pom.xml الموجود في المجلد java-docs-samples\ spanner\leaderboard\codelab\leaderboard.يضبط هذا الملف نظام التصميم Maven لإنشاء تطبيقنا في ملف jar، بما في ذلك جميع الاعتماديات.

أضِف قسم إدارة التبعيات الجديد التالي مباشرةً أسفل العنصر </properties> الحالي:

<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-bom</artifactId>
        <version>0.83.0-alpha</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

أضِف أيضًا اعتمادًا جديدًا واحدًا في قسم <dependencies> الحالي، ما سيؤدي إلى إضافة مكتبة برامج Cloud Spanner Java إلى التطبيق.

    <dependency>
      <!-- Version auto-managed by BOM -->
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner</artifactId>
    </dependency>  

بعد ذلك، استبدِل قسم <build> الحالي في ملف pom.xml بقسم <build> التالي:

 <build>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>2.5.5</version>
        <configuration>
          <finalName>leaderboard</finalName>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>com.google.codelabs.App</mainClass>
            </manifest>
          </archive>
          <appendAssemblyId>false</appendAssemblyId>
          <attach>false</attach>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>3.0.0-M3</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M3</version>
        <configuration>
            <useSystemClassLoader>false</useSystemClassLoader>
        </configuration>
      </plugin>  
    </plugins>
  </build>

احفظ التغييرات التي أجريتها على ملف pom.xml من خلال النقر على "حفظ" في القائمة "ملف" في "محرّر Cloud Shell" أو من خلال الضغط على مفتاحَي "Ctrl" و"S" معًا على لوحة المفاتيح.

بعد ذلك، افتح الملف App.java في "محرِّر Cloud Shell" ضِمن المجلد src/main/java/com/google/codelabs/. استبدِل الرمز الحالي للملف بالرمز المطلوب لإنشاء قاعدة بيانات leaderboard وجولتَي Players وScores عن طريق لصق رمز Java التالي في ملف App.java:

package com.google.codelabs;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;

/**
 * Example code for using the Cloud Spanner API with the Google Cloud Java client library
 * to create a simple leaderboard.
 * 
 * This example demonstrates:
 *
 * <p>
 *
 * <ul>
 *   <li>Creating a Cloud Spanner database.
 * </ul>
 */
public class App {

  static void create(DatabaseAdminClient dbAdminClient, DatabaseId db) {
    OperationFuture<Database, CreateDatabaseMetadata> op =
        dbAdminClient.createDatabase(
            db.getInstanceId().getInstance(),
            db.getDatabase(),
            Arrays.asList(
                "CREATE TABLE Players(\n"
                    + "  PlayerId INT64 NOT NULL,\n"
                    + "  PlayerName STRING(2048) NOT NULL\n"
                    + ") PRIMARY KEY(PlayerId)",
                "CREATE TABLE Scores(\n"
                    + "  PlayerId INT64 NOT NULL,\n"
                    + "  Score INT64 NOT NULL,\n"
                    + "  Timestamp TIMESTAMP NOT NULL\n"
                    + "  OPTIONS(allow_commit_timestamp=true)\n"
                    + ") PRIMARY KEY(PlayerId, Timestamp),\n"
                    + "INTERLEAVE IN PARENT Players ON DELETE NO ACTION"));
    try {
      // Initiate the request which returns an OperationFuture.
      Database dbOperation = op.get();
      System.out.println("Created database [" + dbOperation.getId() + "]");
    } catch (ExecutionException e) {
      // If the operation failed during execution, expose the cause.
      throw (SpannerException) e.getCause();
    } catch (InterruptedException e) {
      // Throw when a thread is waiting, sleeping, or otherwise occupied,
      // and the thread is interrupted, either before or during the activity.
      throw SpannerExceptionFactory.propagateInterrupt(e);
    }
  }

  static void printUsageAndExit() {
    System.out.println("Leaderboard 1.0.0");
    System.out.println("Usage:");
    System.out.println("  java -jar leaderboard.jar "
        + "<command> <instance_id> <database_id> [command_option]");
    System.out.println("");
    System.out.println("Examples:");
    System.out.println("  java -jar leaderboard.jar create my-instance example-db");
    System.out.println("      - Create a sample Cloud Spanner database along with "
        + "sample tables in your project.\n");
    System.exit(1);
  }

  public static void main(String[] args) throws Exception {
    if (!(args.length == 3 || args.length == 4)) {
      printUsageAndExit();
    }
    SpannerOptions options = SpannerOptions.newBuilder().build();
    Spanner spanner = options.getService();
    try {
      String command = args[0];
      DatabaseId db = DatabaseId.of(options.getProjectId(), args[1], args[2]);
      DatabaseClient dbClient = spanner.getDatabaseClient(db);
      DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient();
      switch (command) {
        case "create":
          create(dbAdminClient, db);
          break;
        default:
          printUsageAndExit();
      }
    } finally {
      spanner.close();
    }
    System.out.println("Closed client");
  }
}

احفظ التغييرات التي أجريتها على ملف App.java من خلال النقر على "حفظ" ضمن قائمة "ملف" في "محرّر Cloud Shell".

يمكنك استخدام الملف App.java في الدليل java-docs-samples/spanner/leaderboard/step4/src للاطّلاع على مثال حول الشكل الذي يجب أن يبدو عليه الملف App.java بعد إضافة الرمز لتفعيل الأمر create.

لإنشاء تطبيقك، شغِّل حزمة mvn من الدليل الذي يوجد فيه pom.xml:

mvn package

بعد إنشاء ملف Java jar بنجاح، شغِّل التطبيق الناتج في Cloud Shell عن طريق إدخال الأمر التالي:

java -jar target/leaderboard.jar

من المفترض أن تظهر لك نتيجة مثل ما يلي:

Leaderboard 1.0.0
Usage:
  java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option]

Examples:
  java -jar leaderboard.jar create my-instance example-db
      - Create a sample Cloud Spanner database along with sample tables in your project.

من هذه الاستجابة، يمكننا أن نرى أنّ هذا هو تطبيق Leaderboard الذي يتضمّن حاليًا أمرًا واحدًا محتملاً: create. يمكننا أن نرى أنّ الوسيطتَين المتوقّعتَين للأمر create هما معرّف المثيل ومعرّف قاعدة البيانات.

الآن، شغِّل الأمر التالي.

java -jar target/leaderboard.jar create cloudspanner-leaderboard leaderboard

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

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

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

ba9008bb84cb90b0.png

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

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

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

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

ef49fcbaaed19024.png

بعد ذلك، عدِّل ملف App.java في Cloud Shell Editor لإضافة أمر insert يمكن استخدامه لإدراج 100 لاعب في جدول Players أو لإدراج 4 نتائج عشوائية في جدول Scores لكل لاعب في جدول Players.

عليك أولاً تعديل القسم imports في أعلى ملف التطبيق، واستبدال المحتوى الحالي بما يلي:

package com.google.codelabs;

import static com.google.cloud.spanner.TransactionRunner.TransactionCallable;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TransactionContext;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;

بعد ذلك، أضِف طرق insert وinsertPlayers وinsertScores التالية أسفل طريقة create() الحالية وفوق طريقة printUsageAndExit() الحالية:

  static void insert(DatabaseClient dbClient, String insertType) {
    try {
      insertType = insertType.toLowerCase();
    } catch (Exception e) {
      // Invalid input received, set insertType to empty string.
      insertType = "";
    }
    if (insertType.equals("players")) {
      // Insert players.
      insertPlayers(dbClient);
    } else if (insertType.equals("scores")) {
      // Insert scores.
      insertScores(dbClient);
    } else {
      // Invalid input.
      System.out.println("Invalid value for 'type of insert'. "
          + "Specify a valid value: 'players' or 'scores'.");
      System.exit(1);
    }
  }

  static void insertPlayers(DatabaseClient dbClient) {
    dbClient
        .readWriteTransaction()
        .run(
            new TransactionCallable<Void>() {
              @Override
              public Void run(TransactionContext transaction) throws Exception {
                // Get the number of players.
                String sql = "SELECT Count(PlayerId) as PlayerCount FROM Players";
                ResultSet resultSet = transaction.executeQuery(Statement.of(sql));
                long numberOfPlayers = 0;
                if (resultSet.next()) {
                  numberOfPlayers = resultSet.getLong("PlayerCount");
                }
                // Insert 100 player records into the Players table.
                List<Statement> stmts = new ArrayList<Statement>();
                long randomId;
                for (int x = 1; x <= 100; x++) {
                  numberOfPlayers++;
                  randomId = (long) Math.floor(Math.random() * 9_000_000_000L) + 1_000_000_000L;
                  Statement statement =
                      Statement
                        .newBuilder(
                            "INSERT INTO Players (PlayerId, PlayerName) "
                            + "VALUES (@PlayerId, @PlayerName) ")
                        .bind("PlayerId")
                        .to(randomId)
                        .bind("PlayerName")
                        .to("Player " + numberOfPlayers)
                        .build();
                  stmts.add(statement);
                }
                transaction.batchUpdate(stmts);
                return null;
              }
            });
    System.out.println("Done inserting player records...");
  }

  static void insertScores(DatabaseClient dbClient) {
    boolean playerRecordsFound = false;
    ResultSet resultSet =
        dbClient
            .singleUse()
            .executeQuery(Statement.of("SELECT * FROM Players"));
    while (resultSet.next()) {
      playerRecordsFound = true;
      final long playerId = resultSet.getLong("PlayerId");
      dbClient
          .readWriteTransaction()
          .run(
              new TransactionCallable<Void>() {
                @Override
                public Void run(TransactionContext transaction) throws Exception {
                  // Initialize objects for random Score and random Timestamp.
                  LocalDate endDate = LocalDate.now();
                  long end = endDate.toEpochDay();
                  int startYear = endDate.getYear() - 2;
                  int startMonth = endDate.getMonthValue();
                  int startDay = endDate.getDayOfMonth();
                  LocalDate startDate = LocalDate.of(startYear, startMonth, startDay);
                  long start = startDate.toEpochDay();
                  Random r = new Random();
                  List<Statement> stmts = new ArrayList<Statement>();
                  // Insert 4 score records into the Scores table 
                  // for each player in the Players table.
                  for (int x = 1; x <= 4; x++) {
                    // Generate random score between 1,000,000 and 1,000
                    long randomScore = r.nextInt(1000000 - 1000) + 1000;
                    // Get random day within the past two years.
                    long randomDay = ThreadLocalRandom.current().nextLong(start, end);
                    LocalDate randomDayDate = LocalDate.ofEpochDay(randomDay);
                    LocalTime randomTime = LocalTime.of(
                        r.nextInt(23), r.nextInt(59), r.nextInt(59), r.nextInt(9999));
                    LocalDateTime randomDate = LocalDateTime.of(randomDayDate, randomTime);
                    Instant randomInstant = randomDate.toInstant(ZoneOffset.UTC);
                    Statement statement =
                        Statement
                        .newBuilder(
                          "INSERT INTO Scores (PlayerId, Score, Timestamp) "
                          + "VALUES (@PlayerId, @Score, @Timestamp) ")
                        .bind("PlayerId")
                        .to(playerId)
                        .bind("Score")
                        .to(randomScore)
                        .bind("Timestamp")
                        .to(randomInstant.toString())
                        .build();
                    stmts.add(statement);
                  }
                  transaction.batchUpdate(stmts);
                  return null;
                }
              });

    }
    if (!playerRecordsFound) {
      System.out.println("Parameter 'scores' is invalid since "
          + "no player records currently exist. First insert players "
          + "then insert scores.");
      System.exit(1);
    } else {
      System.out.println("Done inserting score records...");
    }
  }

بعد ذلك، لجعل الأمر insert يعمل، أضِف الرمز التالي إلى طريقة "main" في تطبيقك ضمن عبارة switch (command) :

        case "insert":
          String insertType;
          try {
            insertType = args[3];
          } catch (ArrayIndexOutOfBoundsException exception) {
            insertType = "";
          }
          insert(dbClient, insertType);
          break;

بعد الانتهاء، من المفترض أن يبدو بيان switch (command) على النحو التالي:

      switch (command) {
        case "create":
          create(dbAdminClient, db);
          break;
        case "insert":
          String insertType;
          try {
            insertType = args[3];
          } catch (ArrayIndexOutOfBoundsException exception) {
            insertType = "";
          }
          insert(dbClient, insertType);
          break;
        default:
          printUsageAndExit();
      }

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

    System.out.println("  java -jar leaderboard.jar insert my-instance example-db players");
    System.out.println("      - Insert 100 sample Player records into the database.\n");
    System.out.println("  java -jar leaderboard.jar insert my-instance example-db scores");
    System.out.println("      - Insert sample score data into Scores sample Cloud Spanner "
        + "database table.\n");

احفظ التغييرات التي أجريتها على ملف App.java من خلال النقر على "حفظ" ضمن قائمة "ملف" في "محرّر Cloud Shell".

يمكنك استخدام الملف App.java في الدليل java-docs-samples/spanner/leaderboard/step5/src للاطّلاع على مثال حول الشكل الذي يجب أن يبدو عليه الملف App.java بعد إضافة الرمز لتفعيل الأمر insert.

لنعد الآن إنشاء التطبيق وتشغيله للتأكّد من تضمين الأمر الجديد insert في قائمة الأوامر المحتملة للتطبيق.

لإنشاء تطبيقك، شغِّل mvn package من الدليل الذي يوجد فيه ملف pom.xml:

mvn package

بعد إنشاء ملف Java jar بنجاح، شغِّل الأمر التالي:

java -jar target/leaderboard.jar

من المفترض أن يظهر الأمر insert الآن مضمّنًا في الناتج التلقائي للتطبيق:

Leaderboard 1.0.0
Usage:
  java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option]

Examples:
  java -jar leaderboard.jar create my-instance example-db
      - Create a sample Cloud Spanner database along with sample tables in your project.

  java -jar leaderboard.jar insert my-instance example-db players
      - Insert 100 sample Player records into the database.

  java -jar leaderboard.jar insert my-instance example-db scores
      - Insert sample score data into Scores sample Cloud Spanner database table.

يمكنك أن ترى من الردّ أنّه بالإضافة إلى معرّف المثيل ومعرّف قاعدة البيانات، هناك وسيطة أخرى يمكن أن تكون قيمتها "اللاعبون" أو "النتائج".

لننفّذ الآن الأمر insert باستخدام قيم الوسيطات نفسها التي استخدمناها عند تنفيذ الأمر create، مع إضافة "players" كنوع إضافي من وسيطات "نوع الإدراج".

java -jar target/leaderboard.jar insert cloudspanner-leaderboard leaderboard players

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

Done inserting player records...

لنستخدِم الآن مكتبة برامج Java للعميل لملء جدول 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) طالما أنّك تُدرج طابعًا زمنيًا بقيمة في الماضي، وهو ما سنفعله لأغراض هذا الدرس العملي.

لننفّذ الآن الأمر insert باستخدام قيم الوسيطات نفسها التي استخدمناها عند تنفيذ الأمر create، مع إضافة "scores" كنوع إضافي من وسيطات "نوع الإدراج".

java -jar target/leaderboard.jar insert cloudspanner-leaderboard leaderboard scores

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

Done inserting score records...

يؤدي تنفيذ insert مع تحديد "نوع الإدراج" على أنّه scores إلى استدعاء الطريقة insertScores التي تستخدم مقتطفات الرمز البرمجي التالية لإدراج طابع زمني تم إنشاؤه عشوائيًا مع تاريخ ووقت حدثا في الماضي:

          LocalDate endDate = LocalDate.now();
          long end = endDate.toEpochDay();
          int startYear = endDate.getYear() - 2;
          int startMonth = endDate.getMonthValue();
          int startDay = endDate.getDayOfMonth();
          LocalDate startDate = LocalDate.of(startYear, startMonth, startDay);
          long start = startDate.toEpochDay();
...
            long randomDay = ThreadLocalRandom.current().nextLong(start, end);
            LocalDate randomDayDate = LocalDate.ofEpochDay(randomDay);
            LocalTime randomTime = LocalTime.of(
                        r.nextInt(23), r.nextInt(59), r.nextInt(59), r.nextInt(9999));
            LocalDateTime randomDate = LocalDateTime.of(randomDayDate, randomTime);
            Instant randomInstant = randomDate.toInstant(ZoneOffset.UTC);

...
               .bind("Timestamp")
               .to(randomInstant.toString())

لملء العمود Timestamp تلقائيًا بالطابع الزمني لوقت إجراء المعاملة "إدراج" بالضبط، يمكنك بدلاً من ذلك إدراج الثابت Java Value.COMMIT_TIMESTAMP كما في مقتطف الرمز التالي:

               .bind("Timestamp")
               .to(Value.COMMIT_TIMESTAMP)

بعد إكمال عملية تحميل البيانات، لنتحقّق من القيم التي كتبناها للتو في جداولنا الجديدة. اختَر أولاً قاعدة بيانات leaderboard ثم اختَر جدول Players. انقر على علامة التبويب Data. يجب أن تظهر لك بيانات في العمودَين PlayerId وPlayerName في الجدول.

7bc2c96293c31c49.png

بعد ذلك، لننتحقق من أنّ جدول "النتائج" يتضمّن أيضًا بيانات من خلال النقر على الجدول Scores واختيار علامة التبويب Data. من المفترض أن تظهر لك بيانات في أعمدة PlayerId وTimestamp وScore في الجدول.

d8a4ee4f13244c19.png

أحسنت! لنحدّث تطبيقنا لتنفيذ بعض طلبات البحث التي يمكننا استخدامها لإنشاء قائمة صدارة للألعاب.

6. تنفيذ طلبات البحث في قائمة الصدارة

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

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

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

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

عدِّل الملف App.java في "محرّر Cloud Shell" لتعديل التطبيق من أجل إضافة الأمر query. يتألف الأمر query من طريقتَين query، إحداهما لا تقبل سوى وسيطة DatabaseClient، والأخرى تقبل وسيطة timespan إضافية لتسهيل فلترة النتائج حسب فترة زمنية محدّدة بالساعات.

أضِف طريقتَي query التاليتَين أسفل طريقة insertScores() الحالية وفوق طريقة printUsageAndExit() الحالية:

  static void query(DatabaseClient dbClient) {
    String scoreDate;
    String score;
    ResultSet resultSet =
        dbClient
            .singleUse()
            .executeQuery(
                Statement.of(
                    "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"));
    while (resultSet.next()) {
      scoreDate = String.valueOf(resultSet.getTimestamp("Timestamp"));
      score = String.format("%,d", resultSet.getLong("Score"));
      System.out.printf(
          "PlayerId: %d  PlayerName: %s  Score: %s  Timestamp: %s\n",
          resultSet.getLong("PlayerId"), resultSet.getString("PlayerName"), score,
          scoreDate.substring(0,10));
    }
  }

  static void query(DatabaseClient dbClient, int timespan) {
    String scoreDate;
    String score;
    Statement statement =
        Statement
            .newBuilder(
              "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")
            .bind("Timespan")
            .to(timespan)
            .build();
    ResultSet resultSet =
        dbClient
            .singleUse()
            .executeQuery(statement);
    while (resultSet.next()) {
      scoreDate = String.valueOf(resultSet.getTimestamp("Timestamp"));
      score = String.format("%,d", resultSet.getLong("Score"));
      System.out.printf(
          "PlayerId: %d  PlayerName: %s  Score: %s  Timestamp: %s\n",
          resultSet.getLong("PlayerId"), resultSet.getString("PlayerName"), score,
          scoreDate.substring(0,10));
    }
  }

بعد ذلك، لجعل الأمر query يعمل، أضِف الرمز التالي إلى عبارة switch(command) في طريقة "main" في تطبيقك:

        case "query":
          if (args.length == 4) {
            int timespan = 0;
            try {
              timespan = Integer.parseInt(args[3]);
            } catch (NumberFormatException e) {
              System.err.println("query command's 'timespan' parameter must be a valid integer.");
              System.exit(1);
            }
            query(dbClient, timespan);
          } else {
            query(dbClient);
          }
          break;

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

    System.out.println("  java -jar leaderboard.jar query my-instance example-db");
    System.out.println("      - Query players with top ten scores of all time.\n");
    System.out.println("  java -jar leaderboard.jar query my-instance example-db 168");
    System.out.println("      - Query players with top ten scores within a timespan "
        + "specified in hours.\n");

احفظ التغييرات التي أجريتها على ملف App.java من خلال النقر على "حفظ" ضمن قائمة "ملف" في "محرّر Cloud Shell".

يمكنك استخدام الملف App.java في الدليل dotnet-docs-samples/applications/leaderboard/step6/src للاطّلاع على مثال حول الشكل الذي يجب أن يبدو عليه الملف App.java بعد إضافة الرمز لتفعيل الأمر query.

لإنشاء تطبيقك، شغِّل mvn package من الدليل الذي يوجد فيه ملف pom.xml:

mvn package

لننفّذ التطبيق الآن للتأكّد من تضمين الأمر الجديد query في قائمة الأوامر المحتملة للتطبيق. نفِّذ الأمر التالي:

java -jar target/leaderboard.jar

من المفترض أن يظهر لك الأمر query الآن مضمّنًا في الناتج التلقائي للتطبيق كخيار أمر جديد:

Leaderboard 1.0.0
Usage:
  java -jar leaderboard.jar <command> <instance_id> <database_id> [command_option]

Examples:
  java -jar leaderboard.jar create my-instance example-db
      - Create a sample Cloud Spanner database along with sample tables in your project.

  java -jar leaderboard.jar insert my-instance example-db players
      - Insert 100 sample Player records into the database.

  java -jar leaderboard.jar insert my-instance example-db scores
      - Insert sample score data into Scores sample Cloud Spanner database table.

  java -jar leaderboard.jar query my-instance example-db
      - Query players with top ten scores of all time.

  java -jar leaderboard.jar query my-instance example-db 168
      - Query players with top ten scores within a timespan specified in hours.

يمكنك أن ترى من الردّ أنّه بالإضافة إلى وسيطتَي "معرّف المثيل" و"معرّف قاعدة البيانات"، يتيح لنا الأمر query تحديد فترة زمنية اختيارية بعدد الساعات لاستخدامها في فلترة السجلات استنادًا إلى قيمتها في العمود Timestamp من الجدول Scores. بما أنّ وسيطة الفترة الزمنية اختيارية، يعني ذلك أنّه في حال عدم تضمين وسيطة الفترة الزمنية، لن يتم فلترة أي سجلّات حسب الطوابع الزمنية. وبالتالي، يمكننا استخدام الأمر query بدون قيمة "فترة زمنية" للحصول على قائمة بأفضل عشرة لاعبين على الإطلاق.

لننفّذ الأمر query بدون تحديد "الفترة الزمنية"، وذلك باستخدام قيم الوسيطة نفسها التي استخدمناها عند تنفيذ الأمر create.

java -jar target/leaderboard.jar query cloudspanner-leaderboard 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

لننفّذ الآن الأمر query مع الوسيطات اللازمة للاستعلام عن أفضل عشرة لاعبين في العام من خلال تحديد "الفترة الزمنية" التي تساوي عدد الساعات في السنة، أي 8760 ساعة.

java -jar target/leaderboard.jar query cloudspanner-leaderboard 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

لننفّذ الآن الأمر query للاستعلام عن أفضل عشرة لاعبين في الشهر من خلال تحديد "الفترة الزمنية" التي تساوي عدد الساعات في الشهر، أي 730 ساعة.

java -jar target/leaderboard.jar query cloudspanner-leaderboard 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

لننفّذ الآن الأمر query للاستعلام عن أفضل عشرة لاعبين في الأسبوع من خلال تحديد "فترة زمنية" تساوي عدد الساعات في الأسبوع، أي 168 ساعة.

java -jar target/leaderboard.jar query cloudspanner-leaderboard 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 للوحات الصدارة
  • كيفية إنشاء تطبيق وحدة تحكّم Java
  • كيفية إنشاء قاعدة بيانات وجداول Spanner باستخدام مكتبة برامج Java
  • كيفية تحميل البيانات إلى قاعدة بيانات Spanner باستخدام مكتبة برامج Java
  • كيفية طلب نتائج "أفضل عشرة" من بياناتك باستخدام الطوابع الزمنية لعمليات الإرسال في Spanner ومكتبة برامج Java

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

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

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