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

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

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

15d0ef27a8fbab27.png

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

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

Google Cloud Shell 設定

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

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

  1. 如要透過 Cloud 控制台啟用 Cloud Shell,只要點選「啟用 Cloud Shell」 圖示 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 嗎?查看您在設定步驟中使用的 ID,或在 Cloud 控制台資訊主頁查詢:

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 執行個體。請在左上方的漢堡選單 1a6580bd3d3e6783.png3129589f7bc9e5ce.png 搜尋 Spanner 項目,或按下「/」搜尋 Spanner然後輸入「Spanner」

36e52f8df8e13b99.png

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

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

dceb68e9ed3801e8.png

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

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

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

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

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

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

然後,將目錄變更為「applications」,以便建立應用程式。

cd java-docs-samples/spanner/leaderboard

本程式碼研究室所需的所有程式碼都位於現有的 java-docs-samples/spanner/leaderboard/complete 目錄中,做為名為 Leaderboard 的可執行 C# 應用程式,可讓您在參與本程式碼研究室的過程中做為參考。我們將分階段建立新的目錄,並分階段建立排行榜應用程式副本。

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

mkdir codelab && cd $_

建立名為「Leaderboard」(排行榜) 的新基本 Java 應用程式使用下列 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。開啟位於 java-docs-samples\ spanner\leaderboard\codelab\leaderboard 資料夾中的 pom.xml 檔案。這個檔案會設定 Maven 建構系統,以將應用程式建構為 jar 檔案,包括所有依附元件。

在現有的 </properties> 下方新增下列 1 個新的依附元件管理部分元素:

<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> 中新增 1 個新的依附元件一節,將 Cloud Spanner Java 用戶端程式庫新增至應用程式。

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

然後替換 pom.xml 檔案現有的 <build>內含以下 <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」同時用鍵盤和鍵盤按鍵。

接著,透過位於 src/main/java/com/google/codelabs/ 資料夾的 Cloud Shell 編輯器開啟 App.java 檔案。將下列 Java 程式碼貼到 App.java 檔案,以建立 leaderboard 資料庫和 PlayersScores 資料表所需的程式碼,取代檔案現有的程式碼:

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 編輯器的或前往 Google 試算表選單

您可以在 java-docs-samples/spanner/leaderboard/step4/src 目錄中使用 App.java 檔案,查看啟用 create 指令的程式碼後,App.java 檔案應如何呈現。

如要建構應用程式,請從 pom.xml 所在的目錄執行 mvn 套件:

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 指令的預期引數是執行個體 ID 和資料庫 ID。

接著執行下列指令。

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

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

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

在 Cloud 控制台的 Cloud Spanner 部分,您應該會在左側選單中看到新的資料庫和資料表。

ba9008bb84cb90b0.png

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

5. 載入資料

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

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

ef49fcbaaed19024.png

接著,在 Cloud Shell 編輯器中編輯 App.java 檔案,新增 insert 指令,該指令可用於在 Players 資料表中插入 100 位玩家,也可以用該指令在 Players 資料表中為每位玩家插入 4 個隨機分數。Scores

首先,更新應用程式檔案頂端的 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;

接下來,在現有 create() 方法下方和現有 printUsageAndExit() 方法的上方,新增下列 insert、insertPlayers 和 insertScores 方法:

  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() 方法中新增以下這行程式碼,加入用於 insert 指令的說明文字:

    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 編輯器的或前往 Google 試算表選單

您可以在 java-docs-samples/spanner/leaderboard/step5/src 目錄中使用 App.java 檔案,查看啟用 insert 指令的程式碼後,App.java 檔案應如何呈現。

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

如要建構應用程式,請從 pom.xml 所在的目錄執行 mvn package

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.

您可以在回應中看到,除了執行個體 ID 和資料庫 ID 之外,還有另一個可以含有「players」值的引數或「分數」

現在,使用我們呼叫 create 指令時採用的相同引數值執行 insert 指令,新增「players」做為額外的「插入類型」引數。

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

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

Done inserting player records...

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

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

CREATE TABLE Scores(
  PlayerId INT64 NOT NULL,
  Score INT64 NOT NULL,
  Timestamp TIMESTAMP NOT NULL OPTIONS(allow_commit_timestamp=true)
) PRIMARY KEY(PlayerId, Timestamp),
    INTERLEAVE IN PARENT Players ON DELETE NO ACTION

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

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

現在,使用我們呼叫 create 指令新增「scores」時使用的相同引數值執行 insert 指令做為額外的「插入類型」引數。

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 欄中自動填入「Insert」的確切時間交易發生,您可以改為插入 Java 常數 Value.COMMIT_TIMESTAMP,如以下程式碼片段所示:

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

資料載入已完成,現在讓我們驗證剛才寫入新資料表的值。請先選取 leaderboard 資料庫,然後選取 Players 資料表。按一下「Data」分頁標籤。您應該會在表格的 PlayerIdPlayerName 欄中看到資料。

7bc2c96293c31c49.png

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

d8a4ee4f13244c19.png

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

6. 執行排行榜查詢

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

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

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

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

在 Cloud Shell 編輯器中編輯 App.java 檔案,藉此更新應用程式來新增 query 指令。query 指令包含兩個 query 方法,其中一個只接受 DatabaseClient 引數,另一個則使用額外的 timespan 引數,可根據以小時為單位的指定時間範圍來篩選結果。

在現有 insertScores() 方法下方和現有 printUsageAndExit() 方法的上方新增下列兩個 query 方法:

  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 指令正常運作,請將下列程式碼加入應用程式「main」的 switch(command) 陳述式中方法:

        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() 方法中加入下列程式碼,以納入「查詢」的說明文字指令:

    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 編輯器的或前往 Google 試算表選單

您可以在 dotnet-docs-samples/applications/leaderboard/step6/src 目錄中使用 App.java 檔案,查看啟用 query 指令的程式碼後,App.java 檔案應如何呈現。

如要建構應用程式,請從 pom.xml 所在的目錄執行 mvn package

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.

您可以在回應中看到,除了執行個體 ID 和資料庫 ID 引數以外,您還可以使用 query 指令,根據 Scores 資料表的 Timestamp 欄中的值,指定用於篩選記錄的時間範圍 (以小時為單位)。由於以選用的時間引數引數表示,如果不包含時間範圍引數,則不會依時間戳記篩選任何記錄。所以可以在沒有「時間範圍」的情況下使用 query 指令值,即可取得「前十大」名單隨時都有可能獲得玩家青睞

讓我們使用執行 create 指令時採用的相同引數值,在不指定「時距」的情況下執行 query 指令。

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

8. 恭喜!

本文涵蓋內容:

  • 排行榜的 Google Cloud Spanner 執行個體、資料庫和資料表結構定義
  • 如何建立 Java 控制台應用程式
  • 如何使用 Java 用戶端程式庫建立 Spanner 資料庫和資料表
  • 如何使用 Java 用戶端程式庫將資料載入 Spanner 資料庫
  • 如何查詢「前十名」資料來源:使用 Spanner 修訂時間戳記和 Java 用戶端程式庫

後續步驟:

請提供您寶貴的意見