Cloud Spanner: C#으로 게임 리더보드 만들기

1. 개요

Google Cloud Spanner는 성능 및 고가용성을 희생하지 않고도 ACID 트랜잭션 및 SQL 시맨틱스를 제공하는 수평 확장이 가능하고, 전역으로 분산되었고, 완전히 관리되는 관계형 데이터베이스 서비스입니다.

이 실습에서는 Cloud Spanner 인스턴스를 설정하는 방법을 알아봅니다. 게임 리더보드에 사용할 수 있는 데이터베이스 및 스키마를 만드는 단계를 진행합니다. 먼저 플레이어 정보를 저장하기 위한 플레이어 테이블을 만들고 플레이어 점수를 저장하기 위한 점수 테이블을 만듭니다.

그런 후 테이블에 샘플 데이터를 채웁니다. 그러고 나서 몇 가지 상위 10개 샘플 쿼리를 실행하고 마지막으로 리소스를 비우기 위해 인스턴스를 삭제함으로써 이 실습을 마칩니다.

학습 내용

  • Cloud Spanner 인스턴스 설정 방법
  • 데이터베이스 및 테이블을 만드는 방법
  • 커밋 타임스탬프 열 사용 방법
  • 타임스탬프가 있는 Cloud Spanner 데이터베이스 테이블에 데이터를 로드하는 방법
  • Cloud Spanner 데이터베이스를 쿼리하는 방법
  • Cloud Spanner 인스턴스를 삭제하는 방법

필요한 사항

본 가이드를 어떻게 사용하실 계획인가요?

읽기만 할 계획입니다 읽은 다음 연습 활동을 완료할 계획입니다

귀하의 Google Cloud Platform 사용 경험을 평가해 주세요.

<ph type="x-smartling-placeholder"></ph> 초보자 중급 숙련도

2. 설정 및 요구사항

자습형 환경 설정

아직 Google 계정(Gmail 또는 Google Apps)이 없으면 계정을 만들어야 합니다. Google Cloud Platform Console(console.cloud.google.com)에 로그인하고 새 프로젝트를 만듭니다.

프로젝트가 이미 있으면 Console 왼쪽 위에서 프로젝트 선택 풀다운 메뉴를 클릭합니다.

6c9406d9b014760.png

그리고 표시된 대화상자에서 '새 프로젝트' 버튼을 클릭하여 새 프로젝트를 만듭니다.

f708315ae07353d0.png

아직 프로젝트가 없으면 첫 번째 프로젝트를 만들기 위해 다음과 비슷한 대화상자가 표시됩니다.

870a3cbd6541ee86.png

이후의 프로젝트 만들기 대화상자에서 새 프로젝트의 세부정보를 입력할 수 있습니다.

6a92c57d3250a4b3.png

모든 Google Cloud 프로젝트에서 고유한 이름인 프로젝트 ID를 기억하세요(위의 이름은 이미 사용되었으므로 사용할 수 없습니다). 이 ID는 나중에 이 Codelab에서 PROJECT_ID라고 부릅니다.

그런 다음 Google Cloud 리소스를 사용하고 Cloud Spanner API를 사용 설정하기 위해서는 아직 완료하지 않은 경우 Developers Console에서 결제를 사용 설정해야 합니다.

15d0ef27a8fbab27.png

이 codelab을 실행하는 과정에는 많은 비용이 들지 않지만 더 많은 리소스를 사용하려고 하거나 실행 중일 경우 비용이 더 들 수 있습니다(이 문서 마지막의 '삭제' 섹션 참조). Google Cloud Spanner 가격 책정은 여기를 참조하세요.

Google Cloud Platform 신규 사용자는 $300 상당의 무료 체험판을 사용할 수 있으므로, 이 Codelab을 완전히 무료로 사용할 수 있습니다.

Google Cloud Shell 설정

Google Cloud 및 Spanner를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용합니다.

이 Debian 기반 가상 머신에는 필요한 모든 개발 도구가 로드되어 있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 즉, 이 Codelab에 필요한 것은 브라우저뿐입니다(Chromebook에서도 작동 가능).

  1. Cloud Console에서 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 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 인스턴스 설정

이 단계에서는 이 Codelab을 위해 Cloud Spanner 인스턴스를 설정합니다. 왼쪽 위에 있는 햄버거 메뉴 3129589f7bc9e5ce.png에서 Spanner 항목 1a6580bd3d3e6783.png을 검색하거나 '/'를 누르고 'Spanner'를 입력하여 Spanner를 검색합니다.

36e52f8df8e13b99.png

그런 후 95269e75bc8c3e4d.png를 클릭하고 해당 인스턴스 이름에 cloudspanner-leaderboard를 입력하고, 구성을 선택(리전 인스턴스 선택)하여 양식을 작성하고, 노드 수를 설정합니다. 이 Codelab에서는 1개 노드만 필요합니다. 프로덕션 인스턴스의 경우 그리고 Cloud Spanner SLA를 받기 위해서는 Cloud Spanner 인스턴스에서 노드를 3개 이상 실행해야 합니다.

마지막으로 '만들기'를 클릭하면 몇 초 지나지 않아 Cloud Spanner 인스턴스가 준비됩니다.

dceb68e9ed3801e8.png

다음 단계에서는 C# 클라이언트 라이브러리를 사용하여 새 인스턴스에 데이터베이스와 스키마를 만듭니다.

4. 데이터베이스 및 스키마 만들기

이 단계에서는 샘플 데이터베이스 및 스키마를 만듭니다.

C# 클라이언트 라이브러리를 사용하여 두 개의 테이블을 만들어 보겠습니다. 플레이어 정보를 위한 플레이어 테이블 및 플레이어 점수를 저장하는 점수 테이블입니다. 이를 위해 Cloud Shell에서 C# 콘솔 애플리케이션을 만드는 단계를 살펴보겠습니다.

먼저 Cloud Shell에 다음 명령어를 입력하여 GitHub에서 이 Codelab의 샘플 코드를 클론합니다.

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

그런 후 애플리케이션을 만들려는 'applications' 디렉터리로 변경합니다.

cd dotnet-docs-samples/applications/

이 Codelab에 필요한 모든 코드는 이 Codelab을 진행할 때 참조할 수 있도록 기존 dotnet-docs-samples/applications/leaderboard 디렉터리에 Leaderboard라는 실행 가능한 C# 애플리케이션으로 제공되어 있습니다. 여기에서는 새 디렉터리를 만들고 각 단계에서 Leaderboard 애플리케이션의 복사본을 빌드합니다.

애플리케이션에 대해 'codelab'이라는 새 디렉터리를 만들고 다음 명령어를 사용하여 여기로 디렉터리를 변경합니다.

mkdir codelab && cd $_

'Leaderboard'라는 새 .NET C# 콘솔 애플리케이션을 만듭니다. 사용하여 다음 명령어를 실행합니다.

dotnet new console -n Leaderboard

이 명령어는 프로젝트 파일 Leaderboard.csproj과 프로그램 파일 Program.cs라는 두 개의 파일 기본 파일로 구성된 간단한 콘솔 애플리케이션을 만듭니다.

실행해 보겠습니다. 디렉터리를 애플리케이션이 있는 새로 만든 리더보드 디렉터리로 변경합니다.

cd Leaderboard

다음 명령어를 입력하여 실행합니다.

dotnet run

애플리케이션 출력 'Hello World!'가 표시됩니다.

이제 C# Spanner 클라이언트 라이브러리를 사용하여 두 개의 테이블 플레이어와 점수로 구성된 리더보드를 만들도록 Program.cs를 수정하여 콘솔 앱을 업데이트해 보겠습니다. 이 작업은 Cloud Shell 편집기에서 바로 수행할 수 있습니다.

아래에서 강조표시된 아이콘을 클릭하여 Cloud Shell 편집기를 엽니다.

73cf70e05f653ca.png

그런 다음 Cloud Shell 편집기에서 Program.cs 파일을 열고 Program.cs 파일에 다음 C# 애플리케이션 코드를 붙여넣어 파일의 기존 코드를 leaderboard 데이터베이스와 PlayersScores 테이블을 만드는 데 필요한 코드로 바꿉니다.

using System;
using System.Threading.Tasks;
using Google.Cloud.Spanner.Data;
using CommandLine;

namespace GoogleCloudSamples.Leaderboard
{
    [Verb("create", HelpText = "Create a sample Cloud Spanner database "
        + "along with sample 'Players' and 'Scores' tables in your project.")]
    class CreateOptions
    {
        [Value(0, HelpText = "The project ID of the project to use "
            + "when creating Cloud Spanner resources.", Required = true)]
        public string projectId { get; set; }
        [Value(1, HelpText = "The ID of the instance where the sample database "
            + "will be created.", Required = true)]
        public string instanceId { get; set; }
        [Value(2, HelpText = "The ID of the sample database to create.",
            Required = true)]
        public string databaseId { get; set; }
    }

    public class Program
    {
        enum ExitCode : int
        {
            Success = 0,
            InvalidParameter = 1,
        }

        public static object Create(string projectId,
            string instanceId, string databaseId)
        {
            var response =
                CreateAsync(projectId, instanceId, databaseId);
            Console.WriteLine("Waiting for operation to complete...");
            response.Wait();
            Console.WriteLine($"Operation status: {response.Status}");
            Console.WriteLine($"Created sample database {databaseId} on "
                + $"instance {instanceId}");
            return ExitCode.Success;
        }

        public static async Task CreateAsync(
            string projectId, string instanceId, string databaseId)
        {
            // Initialize request connection string for database creation.
            string connectionString =
                $"Data Source=projects/{projectId}/instances/{instanceId}";
            using (var connection = new SpannerConnection(connectionString))
            {
                string createStatement = $"CREATE DATABASE `{databaseId}`";
                string[] createTableStatements = new string[] {
                  // Define create table statement for Players table.
                  @"CREATE TABLE Players(
                    PlayerId INT64 NOT NULL,
                    PlayerName STRING(2048) NOT NULL
                  ) PRIMARY KEY(PlayerId)",
                  // Define create table statement for Scores table.
                  @"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" };
                // Make the request.
                var cmd = connection.CreateDdlCommand(
                    createStatement, createTableStatements);
                try
                {
                    await cmd.ExecuteNonQueryAsync();
                }
                catch (SpannerException e) when
                    (e.ErrorCode == ErrorCode.AlreadyExists)
                {
                    // OK.
                }
            }
        }

        public static int Main(string[] args)
        {
            var verbMap = new VerbMap<object>();
            verbMap
                .Add((CreateOptions opts) => Create(
                    opts.projectId, opts.instanceId, opts.databaseId))
                .NotParsedFunc = (err) => 1;
            return (int)verbMap.Run(args);
        }
    }
}

다음은 프로그램 코드를 보다 명확하게 설명하기 위해 주요 구성요소에 라벨이 붙은 프로그램 다이어그램입니다.

b70b1b988ea3ac8a.png

dotnet-docs-samples/applications/leaderboard/step4 디렉터리에서 Program.cs 파일을 사용하여 create 명령어를 사용 설정하기 위해 코드를 추가한 후 Program.cs 파일에 필요한 결과 예시를 확인할 수 있습니다.

그런 다음 Cloud Shell 편집기를 사용하여 프로그램의 프로젝트 파일 Leaderboard.csproj을 열고 수정한 후 다음 코드와 같이 업데이트합니다. '파일'을 사용하여 모든 변경사항을 저장해야 합니다. Cloud Shell 편집기 메뉴에서 Cloud Shell을 실행하세요

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Google.Cloud.Spanner.Data" Version="3.3.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\..\commandlineutil\Lib\CommandLineUtil.csproj" />
  </ItemGroup>

</Project>

이 변경사항으로 Cloud Spanner API와 상호작용하는 데 필요한 C# Spanner Nuget 패키지 Google.Cloud.Spanner.Data에 대한 참조가 추가되었습니다. 이렇게 변경하면 dotnet-doc-samples GitHub 저장소의 일부이며 유용한 'verbmap'을 제공하는 CommandLineUtil 프로젝트에 대한 참조도 추가됩니다. 오픈소스 CommandLineParser로 확장 콘솔 애플리케이션의 명령줄 입력을 처리하기 위한 편리한 라이브러리입니다.

dotnet-docs-samples/applications/leaderboard/step4 디렉터리에서 Leaderboard.csproj 파일을 사용하여 create 명령어를 사용 설정하기 위해 코드를 추가한 후 Leaderboard.csproj 파일에 필요한 결과 예시를 확인할 수 있습니다.

이제 업데이트된 샘플을 실행할 준비가 되었습니다. 다음을 입력하여 업데이트된 애플리케이션의 기본 응답을 확인합니다.

dotnet run

다음과 같은 출력이 표시되어야 합니다.

Leaderboard 1.0.0
Copyright (C) 2018 Leaderboard

ERROR(S):
  No verb selected.

  create     Create a sample Cloud Spanner database along with sample 'Players' and 'Scores' tables in your project.

  help       Display more information on a specific command.

  version    Display version information.

이 응답에서 가능한 세 가지 명령어(create, help, version) 중 하나로 실행할 수 있는 Leaderboard 애플리케이션임을 알 수 있습니다.

create 명령어를 사용하여 Spanner 데이터베이스와 테이블을 만들어 보겠습니다. 인수 없이 명령어를 실행하여 명령어의 예상 인수를 확인합니다.

dotnet run create

다음과 같은 응답이 표시됩니다.

Leaderboard 1.0.0
Copyright (C) 2018 Leaderboard

ERROR(S):
  A required value not bound to option name is missing.

  --help          Display this help screen.

  --version       Display version information.

  value pos. 0    Required. The project ID of the project to use when creating Cloud Spanner resources.

  value pos. 1    Required. The ID of the instance where the sample database will be created.

  value pos. 2    Required. The ID of the sample database to create.

여기서 create 명령어의 예상 인수는 프로젝트 ID, 인스턴스 ID, 데이터베이스 ID입니다.

이제 다음 명령어를 실행해 보세요. PROJECT_ID를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.

dotnet run create PROJECT_ID cloudspanner-leaderboard leaderboard

몇 초 후 다음과 같은 응답이 표시됩니다.

Waiting for operation to complete...
Operation status: RanToCompletion
Created sample database leaderboard on instance cloudspanner-leaderboard

Cloud Console의 Cloud Spanner 섹션에서 새 데이터베이스 및 테이블이 왼쪽 측면 메뉴에 표시됩니다.

ba9008bb84cb90b0.png

다음 단계에서 일부 데이터를 새 데이터베이스에 로드하도록 애플리케이션을 업데이트합니다.

5. 데이터 로드

이제 leaderboard라는 데이터베이스에 PlayersScores라는 두 테이블이 포함되었습니다. 이제 C# 클라이언트 라이브러리를 사용하여 Players 테이블에 플레이어를 채우고 Scores 테이블에 각 플레이어의 무작위 점수를 채워 보겠습니다.

아래에서 강조표시된 아이콘을 클릭하여 Cloud Shell 편집기를 엽니다.

4d17840699d8e7ce.png

그런 후 Cloud Shell 편집기에서 Program.cs 파일을 수정하여 Players 테이블에 100명의 플레이어를 삽입하기 위해 사용하거나 Players 테이블에 있는 각 플레이어에 대해 Scores 테이블에 4개의 무작위 점수를 삽입하기 위해 사용할 수 있는 insert 명령어를 추가합니다.

먼저 'Verbmap'에 새 insert 명령어 블록을 추가합니다. 프로그램 상단의 기존 create 명령어 블록 아래에 있습니다.

[Verb("insert", HelpText = "Insert sample 'players' records or 'scores' records "
        + "into the database.")]
    class InsertOptions
    {
        [Value(0, HelpText = "The project ID of the project to use "
            + "when managing Cloud Spanner resources.", Required = true)]
        public string projectId { get; set; }
        [Value(1, HelpText = "The ID of the instance where the sample database resides.",
            Required = true)]
        public string instanceId { get; set; }
        [Value(2, HelpText = "The ID of the database where the sample database resides.",
            Required = true)]
        public string databaseId { get; set; }
        [Value(3, HelpText = "The type of insert to perform, 'players' or 'scores'.",
            Required = true)]
        public string insertType { get; set; }
    }

다음으로 기존 CreateAsync 메서드 아래에 다음 Insert, InsertPlayersAsync, InsertScoresAsync 메서드를 추가합니다.

        public static object Insert(string projectId,
            string instanceId, string databaseId, string insertType)
        {
            if (insertType.ToLower() == "players")
            {
                var responseTask =
                    InsertPlayersAsync(projectId, instanceId, databaseId);
                Console.WriteLine("Waiting for insert players operation to complete...");
                responseTask.Wait();
                Console.WriteLine($"Operation status: {responseTask.Status}");
            }
            else if (insertType.ToLower() == "scores")
            {
                var responseTask =
                    InsertScoresAsync(projectId, instanceId, databaseId);
                Console.WriteLine("Waiting for insert scores operation to complete...");
                responseTask.Wait();
                Console.WriteLine($"Operation status: {responseTask.Status}");
            }
            else
            {
                Console.WriteLine("Invalid value for 'type of insert'. "
                    + "Specify 'players' or 'scores'.");
                return ExitCode.InvalidParameter;
            }
            Console.WriteLine($"Inserted {insertType} into sample database "
                + $"{databaseId} on instance {instanceId}");
            return ExitCode.Success;
        }

       public static async Task InsertPlayersAsync(string projectId,
            string instanceId, string databaseId)
        {
            string connectionString =
                $"Data Source=projects/{projectId}/instances/{instanceId}"
                + $"/databases/{databaseId}";

            long numberOfPlayers = 0;
            using (var connection = new SpannerConnection(connectionString))
            {
                await connection.OpenAsync();
                await connection.RunWithRetriableTransactionAsync(async (transaction) =>
                {
                    // Execute a SQL statement to get current number of records
                    // in the Players table to use as an incrementing value 
                    // for each PlayerName to be inserted.
                    var cmd = connection.CreateSelectCommand(
                        @"SELECT Count(PlayerId) as PlayerCount FROM Players");
                    numberOfPlayers = await cmd.ExecuteScalarAsync<long>();
                    // Insert 100 player records into the Players table.
                    SpannerBatchCommand cmdBatch = connection.CreateBatchDmlCommand();
                    for (int i = 0; i < 100; i++)
                    {
                        numberOfPlayers++;
                        SpannerCommand cmdInsert = connection.CreateDmlCommand(
                            "INSERT INTO Players "
                            + "(PlayerId, PlayerName) "
                            + "VALUES (@PlayerId, @PlayerName)",
                                new SpannerParameterCollection {
                                    {"PlayerId", SpannerDbType.Int64},
                                    {"PlayerName", SpannerDbType.String}});
                        cmdInsert.Parameters["PlayerId"].Value =
                            Math.Abs(Guid.NewGuid().GetHashCode());
                        cmdInsert.Parameters["PlayerName"].Value =
                            $"Player {numberOfPlayers}";
                        cmdBatch.Add(cmdInsert);
                    }
                    await cmdBatch.ExecuteNonQueryAsync();
                });
            }
            Console.WriteLine("Done inserting player records...");
        }

        public static async Task InsertScoresAsync(
            string projectId, string instanceId, string databaseId)
        {
            string connectionString =
            $"Data Source=projects/{projectId}/instances/{instanceId}"
            + $"/databases/{databaseId}";

            // Insert 4 score records into the Scores table for each player
            // in the Players table.
            using (var connection = new SpannerConnection(connectionString))
            {
                await connection.OpenAsync();
                await connection.RunWithRetriableTransactionAsync(async (transaction) =>
                {
                    Random r = new Random();
                    bool playerRecordsFound = false;
                    SpannerBatchCommand cmdBatch =
                                connection.CreateBatchDmlCommand();
                    var cmdLookup =
                    connection.CreateSelectCommand("SELECT * FROM Players");
                    using (var reader = await cmdLookup.ExecuteReaderAsync())
                    {
                        while (await reader.ReadAsync())
                        {
                            playerRecordsFound = true;
                            for (int i = 0; i < 4; i++)
                            {
                                DateTime randomTimestamp = DateTime.Now
                                        .AddYears(r.Next(-2, 1))
                                        .AddMonths(r.Next(-12, 1))
                                        .AddDays(r.Next(-28, 0))
                                        .AddHours(r.Next(-24, 0))
                                        .AddSeconds(r.Next(-60, 0))
                                        .AddMilliseconds(r.Next(-100000, 0));
                                SpannerCommand cmdInsert =
                                connection.CreateDmlCommand(
                                    "INSERT INTO Scores "
                                    + "(PlayerId, Score, Timestamp) "
                                    + "VALUES (@PlayerId, @Score, @Timestamp)",
                                    new SpannerParameterCollection {
                                        {"PlayerId", SpannerDbType.Int64},
                                        {"Score", SpannerDbType.Int64},
                                        {"Timestamp",
                                            SpannerDbType.Timestamp}});
                                cmdInsert.Parameters["PlayerId"].Value =
                                    reader.GetFieldValue<int>("PlayerId");
                                cmdInsert.Parameters["Score"].Value =
                                    r.Next(1000, 1000001);
                                cmdInsert.Parameters["Timestamp"].Value =
                                    randomTimestamp.ToString("o");
                                cmdBatch.Add(cmdInsert);
                            }
                        }
                        if (!playerRecordsFound)
                        {
                            Console.WriteLine("Parameter 'scores' is invalid "
                            + "since no player records currently exist. First "
                            + "insert players then insert scores.");
                            Environment.Exit((int)ExitCode.InvalidParameter);
                        }
                        else
                        {
                            await cmdBatch.ExecuteNonQueryAsync();
                            Console.WriteLine(
                                "Done inserting score records..."
                            );
                        }
                    }
                });
            }
        }

그런 다음 insert 명령어가 작동하도록 하려면 프로그램의 'Main'에 다음 코드를 추가합니다. 메서드를 사용하여 축소하도록 요청합니다.

                .Add((InsertOptions opts) => Insert(
                    opts.projectId, opts.instanceId, opts.databaseId, opts.insertType))

dotnet-docs-samples/applications/leaderboard/step5 디렉터리에서 Program.cs 파일을 사용하여 insert 명령어를 사용 설정하기 위해 코드를 추가한 후 Program.cs 파일에 필요한 결과 예시를 확인할 수 있습니다.

이제 프로그램을 실행하여 새 insert 명령어가 프로그램의 가능한 명령어 목록에 포함되어 있는지 확인합니다. 다음 명령어를 실행합니다.

dotnet run

이제 프로그램의 기본 출력에 포함된 insert 명령어가 표시됩니다.

Leaderboard 1.0.0
Copyright (C) 2018 Leaderboard

ERROR(S):
  No verb selected.

  create     Create a sample Cloud Spanner database along with sample 'Players' and 'Scores' tables in your project.

  insert     Insert sample 'players' records or 'scores' records into the database.

  help       Display more information on a specific command.

  version    Display version information.

이제 insert 명령어를 실행하여 입력 인수를 확인합니다. 다음 명령어를 입력합니다.

dotnet run insert

그러면 다음과 같은 응답이 반환됩니다.

Leaderboard 1.0.0
Copyright (C) 2018 Leaderboard

ERROR(S):
  A required value not bound to option name is missing.

  --help          Display this help screen.

  --version       Display version information.

  value pos. 0    Required. The project ID of the project to use when managing Cloud Spanner resources.

  value pos. 1    Required. The ID of the instance where the sample database resides.

  value pos. 2    Required. The ID of the database where the sample database resides.

  value pos. 3    Required. The type of insert to perform, 'players' or 'scores'.

응답에서 프로젝트 ID, 인스턴스 ID, 데이터베이스 ID 외에도 '삽입 유형'인 다른 인수 value pos. 3가 예상되는 것을 확인할 수 있습니다. 수행할 수 있습니다 이 인수는 'players' 값을 가질 수 있습니다. '점수'입니다.

이제 create 명령어를 호출할 때 사용한 것과 동일한 인수 값을 사용하여 insert 명령어를 실행하고, 추가적인 '삽입 유형' 인수로 'players'를 추가합니다. PROJECT_ID를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.

dotnet run insert PROJECT_ID cloudspanner-leaderboard leaderboard players

몇 초 후 다음과 같은 응답이 표시됩니다.

Waiting for insert players operation to complete...
Done inserting player records...
Operation status: RanToCompletion
Inserted players into sample database leaderboard on instance cloudspanner-leaderboard

이제 C# 클라이언트 라이브러리를 사용하여 Players 테이블의 각 플레이어에 대한 타임스탬프와 함께 임의의 4개의 점수로 Scores 테이블을 채워 보겠습니다.

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 작업에 대한 정확한 트랜잭션 타임스탬프로 자동으로 입력되도록 만듭니다.

또한 타임스탬프에 과거 시간의 값을 삽입하는 한 고유 타임스탬프 값을 '커밋 타임스탬프' 열에 삽입할 수 있습니다. 이 작업은 이 Codelab에서 수행됩니다.

이제 create 명령어를 호출할 때 사용한 것과 동일한 인수 값을 사용하여 insert 명령어를 실행하고, 추가적인 '삽입 유형' 인수로 'scores'를 추가합니다. PROJECT_ID를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.

dotnet run insert PROJECT_ID cloudspanner-leaderboard leaderboard scores

몇 초 후 다음과 같은 응답이 표시됩니다.

Waiting for insert players operation to complete...
Done inserting player records...
Operation status: RanToCompletion
Inserted players into sample database leaderboard on instance cloudspanner-leaderboard

'삽입 유형'을 scores로 지정하여 insert를 실행하면 과거의 날짜/시간으로 무작위로 생성된 타임스탬프를 삽입하기 위해 다음 코드 스니펫을 사용하는 InsertScoresAsync 메서드를 호출합니다.

DateTime randomTimestamp = DateTime.Now
    .AddYears(r.Next(-2, 1))
    .AddMonths(r.Next(-12, 1))
    .AddDays(r.Next(-28, 0))
    .AddHours(r.Next(-24, 0))
    .AddSeconds(r.Next(-60, 0))
    .AddMilliseconds(r.Next(-100000, 0));
...
 cmdInsert.Parameters["Timestamp"].Value = randomTimestamp.ToString("o");

Timestamp 열을 'Insert' 이벤트가 시작된 시점의 타임스탬프로 자동 채우려면 발생하는 경우 다음 코드 스니펫과 같이 C# 상수 SpannerParameter.CommitTimestamp를 대신 삽입할 수 있습니다.

cmd.Parameters["Timestamp"].Value = SpannerParameter.CommitTimestamp;

이제 데이터 로드가 완료되었으므로 새 테이블에 기록한 값을 확인합니다. 먼저 leaderboard 데이터베이스를 선택하고 Players 테이블을 선택합니다. Data 탭을 클릭합니다. 테이블의 PlayerIdPlayerName 열에 데이터가 포함된 것을 알 수 있습니다.

7bc2c96293c31c49.png

이제 Scores 테이블을 클릭하고 Data 탭을 선택하여 점수 테이블에도 데이터가 있는지 확인합니다. 테이블의 PlayerId, Timestamp, Score 열에 데이터가 포함된 것을 알 수 있습니다.

d8a4ee4f13244c19.png

잘하셨습니다. 게임 리더보드를 만드는 데 사용할 수 있는 쿼리를 실행하도록 프로그램을 업데이트해 보겠습니다.

6. 리더보드 쿼리 실행

이제 데이터베이스가 설정되었고 테이블에 정보가 로드되었으므로, 이 데이터를 사용하는 리더보드를 만듭니다. 이를 위해서는 다음 4개 질문에 답변해야 합니다.

  1. 항상 '상위 10대' 플레이어는 누구인가요?
  2. 올해 '상위 10대' 플레이어는 누구인가요?
  3. 이달의 '상위 10대' 플레이어는 누구인가요?
  4. 이번 주 '상위 10대' 플레이어는 누구인가요?

이러한 질문에 답할 SQL 쿼리를 실행하도록 프로그램을 업데이트해 보겠습니다.

리더보드에 필요한 정보를 생성하는 질문에 답변하도록 쿼리를 실행할 수 있는 방법을 제공하는 query 명령어를 추가합니다.

Cloud Shell 편집기에서 Program.cs 파일을 수정하여 query 명령어를 추가하도록 프로그램을 업데이트합니다.

먼저 'Verbmap'에 새 query 명령어 블록을 추가합니다. 프로그램 상단의 기존 insert 명령어 블록 아래에 있습니다.

    [Verb("query", HelpText = "Query players with 'Top Ten' scores within a specific timespan "
        + "from sample Cloud Spanner database table.")]
    class QueryOptions
    {
        [Value(0, HelpText = "The project ID of the project to use "
            + "when managing Cloud Spanner resources.", Required = true)]
        public string projectId { get; set; }
        [Value(1, HelpText = "The ID of the instance where the sample data resides.",
            Required = true)]
        public string instanceId { get; set; }
        [Value(2, HelpText = "The ID of the database where the sample data resides.",
            Required = true)]
        public string databaseId { get; set; }
        [Value(3, Default = 0, HelpText = "The timespan in hours that will be used to filter the "
            + "results based on a record's timestamp. The default will return the "
            + "'Top Ten' scores of all time.")]
        public int timespan { get; set; }
    }

다음으로 기존 InsertScoresAsync 메서드 아래에 다음 QueryQueryAsync 메서드를 추가합니다.

public static object Query(string projectId,
            string instanceId, string databaseId, int timespan)
        {
            var response = QueryAsync(
                projectId, instanceId, databaseId, timespan);
            response.Wait();
            return ExitCode.Success;
        }        

public static async Task QueryAsync(
            string projectId, string instanceId, string databaseId, int timespan)
        {
            string connectionString =
            $"Data Source=projects/{projectId}/instances/"
            + $"{instanceId}/databases/{databaseId}";
            // Create connection to Cloud Spanner.
            using (var connection = new SpannerConnection(connectionString))
            {
                string sqlCommand;
                if (timespan == 0)
                {
                    // No timespan specified. Query Top Ten scores of all time.
                    sqlCommand =
                        @"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";
                }
                else
                {
                    // Query Top Ten scores filtered by the timepan specified.
                    sqlCommand =
                        $@"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.ToString()} HOUR)
                            ORDER BY s.Score DESC LIMIT 10";
                }
                var cmd = connection.CreateSelectCommand(sqlCommand);
                using (var reader = await cmd.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        Console.WriteLine("PlayerId : "
                          + reader.GetFieldValue<string>("PlayerId")
                          + " PlayerName : "
                          + reader.GetFieldValue<string>("PlayerName")
                          + " Score : "
                          + string.Format("{0:n0}",
                            Int64.Parse(reader.GetFieldValue<string>("Score")))
                          + " Timestamp : "
                          + reader.GetFieldValue<string>("Timestamp").Substring(0, 10));
                    }
                }
            }
        }

그런 다음 query 명령어가 작동하도록 하려면 프로그램의 'Main'에 다음 코드를 추가합니다. 메서드를 사용하여 축소하도록 요청합니다.

                .Add((QueryOptions opts) => Query(
                    opts.projectId, opts.instanceId, opts.databaseId, opts.timespan))

dotnet-docs-samples/applications/leaderboard/step6 디렉터리에서 Program.cs 파일을 사용하여 query 명령어를 사용 설정하기 위해 코드를 추가한 후 Program.cs 파일에 필요한 결과 예시를 확인할 수 있습니다.

이제 프로그램을 실행하여 새 query 명령어가 프로그램의 가능한 명령어 목록에 포함되어 있는지 확인합니다. 다음 명령어를 실행합니다.

dotnet run

이제 query 명령어가 프로그램의 기본 출력에 새 명령어 옵션으로 포함됩니다.

Leaderboard 1.0.0
Copyright (C) 2018 Leaderboard

ERROR(S):
  No verb selected.

  create     Create a sample Cloud Spanner database along with sample 'Players' and 'Scores' tables in your project.

  insert     Insert sample 'players' records or 'scores' records into the database.

  query      Query players with 'Top Ten' scores within a specific timespan from sample Cloud Spanner database table.

  help       Display more information on a specific command.

  version    Display version information.

이제 query 명령어를 실행하여 입력 인수를 확인합니다. 다음 명령어를 입력합니다.

dotnet run query

그러면 다음과 같은 응답이 반환됩니다.

Leaderboard 1.0.0
Copyright (C) 2018 Leaderboard

ERROR(S):
  A required value not bound to option name is missing.

  --help          Display this help screen.

  --version       Display version information.

  value pos. 0    Required. The project ID of the project to use when managing Cloud Spanner resources.

  value pos. 1    Required. The ID of the instance where the sample data resides.

  value pos. 2    Required. The ID of the database where the sample data resides.

  value pos. 3    (Default: 0) The timespan in hours that will be used to filter the results based on a record's timestamp. The default will return the 'Top Ten' scores of all time.

응답에서 프로젝트 ID, 인스턴스 ID, 데이터베이스 ID 외에도 Scores 테이블의 Timestamp 열에 있는 값에 따라 레코드를 필터링하는 데 사용할 기간을 시간 단위로 지정할 수 있는 또 다른 예상 인수 value pos. 3가 있음을 알 수 있습니다. 이 인수의 기본값은 0이며, 이는 타임스탬프로 어떤 레코드도 필터링되지 않음을 의미합니다. 따라서 '기간' 값 없이 query 명령어를 사용하여 항상 '상위 10대' 플레이어 목록을 가져올 수 있습니다.

'기간'을 지정하지 않고 create 명령어를 실행했을 때 사용한 것과 동일한 인수 값을 사용하여 query 명령어를 실행합니다. PROJECT_ID를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.

dotnet run query PROJECT_ID cloudspanner-leaderboard leaderboard

다음과 같이 항상 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.

PlayerId : 1843159180 PlayerName : Player 87 Score : 998,955 Timestamp : 2016-03-23
PlayerId : 61891198 PlayerName : Player 19 Score : 998,720 Timestamp : 2016-03-26
PlayerId : 340906298 PlayerName : Player 48 Score : 993,302 Timestamp : 2015-08-27
PlayerId : 541473117 PlayerName : Player 22 Score : 991,368 Timestamp : 2018-04-30
PlayerId : 857460496 PlayerName : Player 68 Score : 988,010 Timestamp : 2015-05-25
PlayerId : 1826646419 PlayerName : Player 91 Score : 984,022 Timestamp : 2016-11-26
PlayerId : 1002199735 PlayerName : Player 35 Score : 982,933 Timestamp : 2015-09-26
PlayerId : 2002563755 PlayerName : Player 23 Score : 979,041 Timestamp : 2016-10-25
PlayerId : 1377548191 PlayerName : Player 2 Score : 978,632 Timestamp : 2016-05-02
PlayerId : 1358098565 PlayerName : Player 65 Score : 973,257 Timestamp : 2016-10-30

이제 1년 동안의 시간 수인 8760과 동일한 '기간'을 지정하여 1년 동안 '상위 10대' 플레이어를 쿼리하는 데 필요한 인수를 사용하여 query 명령어를 실행합니다. PROJECT_ID를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.

dotnet run query PROJECT_ID cloudspanner-leaderboard leaderboard 8760

다음과 같이 1년 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.

PlayerId : 541473117 PlayerName : Player 22 Score : 991,368 Timestamp : 2018-04-30
PlayerId : 228469898 PlayerName : Player 82 Score : 967,177 Timestamp : 2018-01-26
PlayerId : 1131343000 PlayerName : Player 26 Score : 944,725 Timestamp : 2017-05-26
PlayerId : 396780730 PlayerName : Player 41 Score : 929,455 Timestamp : 2017-09-26
PlayerId : 61891198 PlayerName : Player 19 Score : 921,251 Timestamp : 2018-05-01
PlayerId : 634269851 PlayerName : Player 54 Score : 909,379 Timestamp : 2017-07-24
PlayerId : 821111159 PlayerName : Player 55 Score : 908,402 Timestamp : 2017-05-25
PlayerId : 228469898 PlayerName : Player 82 Score : 889,040 Timestamp : 2017-12-26
PlayerId : 1408782275 PlayerName : Player 27 Score : 874,124 Timestamp : 2017-09-24
PlayerId : 1002199735 PlayerName : Player 35 Score : 864,758 Timestamp : 2018-04-24

이제 query 명령어를 실행하고 '기간'을 1개월 동안의 시간 수인 730과 동일하게 지정하여 1개월 동안 '상위 10'대 플레이어를 쿼리합니다. PROJECT_ID를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.

dotnet run query PROJECT_ID cloudspanner-leaderboard leaderboard 730

다음과 같이 1개월 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.

PlayerId : 541473117 PlayerName : Player 22 Score : 991,368 Timestamp : 2018-04-30
PlayerId : 61891198 PlayerName : Player 19 Score : 921,251 Timestamp : 2018-05-01
PlayerId : 1002199735 PlayerName : Player 35 Score : 864,758 Timestamp : 2018-04-24
PlayerId : 1228490432 PlayerName : Player 11 Score : 682,033 Timestamp : 2018-04-26
PlayerId : 648239230 PlayerName : Player 92 Score : 653,895 Timestamp : 2018-05-02
PlayerId : 70762849 PlayerName : Player 77 Score : 598,074 Timestamp : 2018-04-22
PlayerId : 1671215342 PlayerName : Player 62 Score : 506,770 Timestamp : 2018-04-28
PlayerId : 1208850523 PlayerName : Player 21 Score : 216,008 Timestamp : 2018-04-30
PlayerId : 1587692674 PlayerName : Player 63 Score : 188,157 Timestamp : 2018-04-25
PlayerId : 992391797 PlayerName : Player 37 Score : 167,175 Timestamp : 2018-04-30

이제 query 명령어를 실행하고 '기간'을 1주일 동안의 시간 수인 168과 동일하게 지정하여 1주일 동안 '상위 10'대 플레이어를 쿼리합니다. PROJECT_ID를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.

dotnet run query PROJECT_ID cloudspanner-leaderboard leaderboard 168

다음과 같이 1주일 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.

PlayerId : 541473117 PlayerName : Player 22 Score : 991,368 Timestamp : 2018-04-30
PlayerId : 61891198 PlayerName : Player 19 Score : 921,251 Timestamp : 2018-05-01
PlayerId : 228469898 PlayerName : Player 82 Score : 853,602 Timestamp : 2018-04-28
PlayerId : 1131343000 PlayerName : Player 26 Score : 695,318 Timestamp : 2018-04-30
PlayerId : 1228490432 PlayerName : Player 11 Score : 682,033 Timestamp : 2018-04-26
PlayerId : 1408782275 PlayerName : Player 27 Score : 671,827 Timestamp : 2018-04-27
PlayerId : 648239230 PlayerName : Player 92 Score : 653,895 Timestamp : 2018-05-02
PlayerId : 816861444 PlayerName : Player 83 Score : 622,277 Timestamp : 2018-04-27
PlayerId : 162043954 PlayerName : Player 75 Score : 572,634 Timestamp : 2018-05-02
PlayerId : 1671215342 PlayerName : Player 62 Score : 506,770 Timestamp : 2018-04-28

훌륭합니다.

이제 레코드를 추가하면 Spanner가 필요한 만큼 데이터베이스를 확장합니다.

데이터베이스가 얼마나 증가하든 관계없이 게임의 리더보드는 Spanner 및 Truetime 기술을 통해 계속해서 정확하게 확장될 수 있습니다.

7. 삭제

Spanner에 대한 모든 학습이 완료되었으면 귀중한 리소스와 비용을 절약하기 위해 사용된 리소스를 삭제해야 합니다. 다행히 이 단계는 간단합니다. Developer Console로 이동하여 Codelab 단계에서 'Cloud Spanner 인스턴스 설정'이라는 이름의 인스턴스를 삭제하면 됩니다.

8. 축하합니다.

학습한 내용:

  • 리더보드에 대한 Google Cloud Spanner 인스턴스, 데이터베이스, 테이블 스키마
  • .NET Core C# 콘솔 애플리케이션을 만드는 방법
  • C# 클라이언트 라이브러리를 사용하여 Spanner 데이터베이스 및 테이블을 만드는 방법
  • C# 클라이언트 라이브러리를 사용하여 Spanner 데이터베이스에 데이터를 로드하는 방법
  • '상위 10개' 쿼리 방법 Spanner 커밋 타임스탬프와 C# 클라이언트 라이브러리를 사용하여 데이터의 결과

다음 단계:

Google에 의견 보내기

  • 잠시 시간을 내어 간단한 설문조사에 응해주시기 바랍니다.