Google Cloud Spanner는 성능 및 고가용성을 희생하지 않고도 ACID 트랜잭션 및 SQL 시맨틱스를 제공하는 수평 확장이 가능하고, 전역으로 분산되었고, 완전히 관리되는 관계형 데이터베이스 서비스입니다.
이 실습에서는 Cloud Spanner 인스턴스를 설정하는 방법을 알아봅니다. 게임 리더보드에 사용할 수 있는 데이터베이스 및 스키마를 만드는 단계를 진행합니다. 먼저 플레이어 정보를 저장하기 위한 플레이어 테이블을 만들고 플레이어 점수를 저장하기 위한 점수 테이블을 만듭니다.
그런 후 테이블에 샘플 데이터를 채웁니다. 그러고 나서 몇 가지 상위 10개 샘플 쿼리를 실행하고 마지막으로 리소스를 비우기 위해 인스턴스를 삭제함으로써 이 실습을 마칩니다.
학습 내용
- Cloud Spanner 인스턴스 설정 방법
- 데이터베이스 및 테이블을 만드는 방법
- 커밋 타임스탬프 열 사용 방법
- 타임스탬프가 있는 Cloud Spanner 데이터베이스 테이블에 데이터를 로드하는 방법
- Cloud Spanner 데이터베이스를 쿼리하는 방법
- Cloud Spanner 인스턴스를 삭제하는 방법
준비물
본 가이드를 어떻게 사용하실 계획인가요?
귀하의 Google Cloud Platform 사용 경험을 평가해 주세요.
자습형 환경 설정
아직 Google 계정(Gmail 또는 Google Apps)이 없으면 계정을 만들어야 합니다. Google Cloud Platform Console(console.cloud.google.com)에 로그인하고 새 프로젝트를 만듭니다.
프로젝트가 이미 있으면 Console 왼쪽 위에서 프로젝트 선택 풀다운 메뉴를 클릭합니다.
그리고 표시된 대화상자에서 '새 프로젝트' 버튼을 클릭하여 새 프로젝트를 만듭니다.
아직 프로젝트가 없으면 첫 번째 프로젝트를 만들기 위해 다음과 비슷한 대화상자가 표시됩니다.
이후의 프로젝트 만들기 대화상자에서 새 프로젝트의 세부정보를 입력할 수 있습니다.
모든 Google Cloud 프로젝트에서 고유한 이름인 프로젝트 ID를 기억하세요(위의 이름은 이미 사용되었으므로 사용할 수 없습니다). 이 ID는 나중에 이 Codelab에서 PROJECT_ID
라고 부릅니다.
그런 다음 Google Cloud 리소스를 사용하고 Cloud Spanner API를 사용 설정하기 위해서는 아직 완료하지 않은 경우 Developers Console에서 결제를 사용 설정해야 합니다.
이 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에서도 작동 가능).
- Cloud Console에서 Cloud Shell을 활성화하려면 단순히 Cloud Shell 활성화를 클릭합니다. 환경을 프로비저닝하고 연결하는 데 몇 정도만 소요됩니다.
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 대시보드에서 확인하세요.
또한 Cloud Shell은 기본적으로 이후 명령어를 실행할 때 유용할 수 있는 몇 가지 환경 변수를 설정합니다.
echo $GOOGLE_CLOUD_PROJECT
명령어 결과
<PROJECT_ID>
- 마지막으로 기본 영역 및 프로젝트 구성을 설정합니다.
gcloud config set compute/zone us-central1-f
다양한 영역을 선택할 수 있습니다. 자세한 내용은 리전 및 영역을 참조하세요.
요약
이 단계에서는 환경을 설정합니다.
다음 항목
이제 Cloud Spanner 인스턴스를 설정합니다.
이 단계에서는 이 Codelab을 위해 Cloud Spanner 인스턴스를 설정합니다. 왼쪽 위에 있는 햄버거 메뉴 에서 Spanner 항목 을 검색하거나 '/'를 누르고 'Spanner'를 입력하여 Spanner를 검색합니다.
그런 후 를 클릭하고 해당 인스턴스 이름에 cloudspanner-leaderboard를 입력하고, 구성을 선택(리전 인스턴스 선택)하여 양식을 작성하고, 노드 수를 설정합니다. 이 Codelab에서는 1개 노드만 필요합니다. 프로덕션 인스턴스의 경우 그리고 Cloud Spanner SLA를 받기 위해서는 Cloud Spanner 인스턴스에서 노드를 3개 이상 실행해야 합니다.
마지막으로 '만들기'를 클릭하면 몇 초 지나지 않아 Cloud Spanner 인스턴스가 준비됩니다.
다음 단계에서는 Go 클라이언트 라이브러리를 사용하여 새 인스턴스에 데이터베이스 및 스키마를 만듭니다.
이 단계에서는 샘플 데이터베이스 및 스키마를 만듭니다.
Go 클라이언트 라이브러리를 사용하여 테이블 2개를 만듭니다. 하나는 플레이어 정보를 위한 플레이어 테이블이고, 다른 하나는 플레이어 점수를 저장하기 위한 점수 테이블입니다. 이렇게 하기 위해 Cloud Shell에서 Go 콘솔 애플리케이션을 만드는 단계를 수행합니다.
먼저 Cloud Shell에 다음 명령어를 입력하여 GitHub에서 이 Codelab의 샘플 코드를 클론합니다.
go get -u github.com/GoogleCloudPlatform/golang-samples/spanner/...
그런 후 애플리케이션을 만들려는 'leaderboard' 디렉터리로 변경합니다.
cd gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/spanner_leaderboard
이 Codelab에 필요한 모든 코드는 이 Codelab을 진행할 때 참조할 수 있도록 기존 golang-samples/spanner/spanner_leaderboard/
디렉터리에 leaderboard
라는 실행 가능한 Go 애플리케이션으로 제공되어 있습니다. 여기에서는 새 디렉터리를 만들고 각 단계에서 Leaderboard 애플리케이션의 복사본을 빌드합니다.
애플리케이션에 대해 'codelab'이라는 새 디렉터리를 만들고 다음 명령어를 사용하여 여기로 디렉터리를 변경합니다.
mkdir codelab && cd $_
이제 Spanner 클라이언트 라이브러리를 사용하여 2개 테이블 즉, Players 및 Scores로 구성된 리더보드를 만드는 'Leaderboard'라는 기본적인 Go 애플리케이션을 만들도록 업데이트합니다. 이 작업은 Cloud Shell 편집기에서 바로 수행할 수 있습니다.
아래에서 강조표시된 아이콘을 클릭하여 Cloud Shell 편집기를 엽니다.
~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/spanner/codelab 폴더에 'leaderboard.go'라는 파일을 만듭니다.
- 먼저 Cloud Shell 편집기의 폴더 목록에서 'codelab' 폴더를 선택했는지 확인합니다.
- 그런 후 Cloud Shell 편집기의 '파일' 메뉴에서 '새 파일'을 선택합니다.
- 새 파일 이름으로 'leaderboard.go'를 입력합니다.
이것은 애플리케이션 코드 및 종속 항목을 포함하기 위한 참조가 포함된 애플리케이션의 기본 파일입니다.
leaderboard
데이터베이스와 Players
및 Scores
테이블을 만들기 위해 다음 Go 코드를 leaderboard.go
파일에 복사(Ctrl + P)하고 붙여넣습니다(Ctrl + V).
package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"os"
"regexp"
"time"
"cloud.google.com/go/spanner"
database "cloud.google.com/go/spanner/admin/database/apiv1"
adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)
type adminCommand func(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, database string) error
func createDatabase(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, db string) error {
matches := regexp.MustCompile("^(.*)/databases/(.*)$").FindStringSubmatch(db)
if matches == nil || len(matches) != 3 {
return fmt.Errorf("Invalid database id %s", db)
}
op, err := adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
Parent: matches[1],
CreateStatement: "CREATE DATABASE `" + matches[2] + "`",
ExtraStatements: []string{
`CREATE TABLE Players(
PlayerId INT64 NOT NULL,
PlayerName STRING(2048) NOT NULL
) PRIMARY KEY(PlayerId)`,
`CREATE TABLE Scores(
PlayerId INT64 NOT NULL,
Score INT64 NOT NULL,
Timestamp TIMESTAMP NOT NULL
OPTIONS(allow_commit_timestamp=true)
) PRIMARY KEY(PlayerId, Timestamp),
INTERLEAVE IN PARENT Players ON DELETE NO ACTION`,
},
})
if err != nil {
return err
}
if _, err := op.Wait(ctx); err != nil {
return err
}
fmt.Fprintf(w, "Created database [%s]\n", db)
return nil
}
func createClients(ctx context.Context, db string) (*database.DatabaseAdminClient, *spanner.Client) {
adminClient, err := database.NewDatabaseAdminClient(ctx)
if err != nil {
log.Fatal(err)
}
dataClient, err := spanner.NewClient(ctx, db)
if err != nil {
log.Fatal(err)
}
return adminClient, dataClient
}
func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
cmd string, db string, timespan int) error {
// createdatabase command
if cmd == "createdatabase" {
err := createDatabase(ctx, w, adminClient, db)
if err != nil {
fmt.Fprintf(w, "%s failed with %v", cmd, err)
}
return err
}
return nil
}
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `Usage: leaderboard <command> <database_name> [command_option]
Command can be one of: createdatabase
Examples:
leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db
- Create a sample Cloud Spanner database along with sample tables in your project.
`)
}
flag.Parse()
flagCount := len(flag.Args())
if flagCount != 2 {
flag.Usage()
os.Exit(2)
}
cmd, db := flag.Arg(0), flag.Arg(1)
// Set timespan to zero, as it's not currently being used
var timespan int = 0
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
adminClient, dataClient := createClients(ctx, db)
if err := run(ctx, adminClient, dataClient, os.Stdout, cmd, db, timespan); err != nil {
os.Exit(1)
}
}
Cloud Shell 편집기의 '파일' 메뉴에서 '저장'을 선택하여 변경사항을 leaderboard.go
파일에 저장합니다.
golang-samples/spanner/spanner_leaderboard
디렉터리에서 leaderboard.go
파일을 사용하여 createdatabase
명령어를 사용 설정하기 위해 코드를 추가한 후 leaderboard.go
파일에 필요한 결과 예시를 확인할 수 있습니다.
Cloud Shell에서 앱을 빌드하려면 leaderboard.go
파일이 있는 codelab
디렉터리에서 'go build'를 실행합니다.
go build leaderboard.go
애플리케이션이 성공적으로 빌드되면 다음 명령어를 입력하여 Cloud Shell에서 결과 애플리케이션을 실행합니다.
./leaderboard
다음과 같은 출력이 표시되어야 합니다.
Usage: leaderboard <command> <database_name> [command_option] Command can be one of: createdatabase Examples: leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db - Create a sample Cloud Spanner database along with sample tables in your project.
이 응답에서 현재 사용 가능한 명령어 createdatabase
하나를 포함하는 Leaderboard
애플리케이션을 확인할 수 있습니다. createdatabase
명령어에 필요한 인수가 특정 인스턴스 ID와 데이터베이스 ID를 포함하는 문자열인 것을 알 수 있습니다.
이제 다음 명령어를 실행해 보세요. my-project
를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.
./leaderboard createdatabase projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard
몇 초 후 다음과 같은 응답이 표시됩니다.
Created database [projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard]
Cloud Console의 Cloud Spanner 섹션에서 새 데이터베이스 및 테이블이 왼쪽 측면 메뉴에 표시됩니다.
다음 단계에서 일부 데이터를 새 데이터베이스에 로드하도록 애플리케이션을 업데이트합니다.
이제 leaderboard
라는 데이터베이스에 Players
및 Scores
라는 두 테이블이 포함되었습니다. 이제 Go 클라이언트 라이브러리를 사용하여 Players
테이블에 플레이어를 입력하고 Scores
테이블에 각 플레이어의 무작위 점수를 입력합니다.
아직 열려 있지 않으면 아래 강조 표시된 아이콘을 클릭하여 Cloud Shell 편집기를 엽니다.
그런 다음 Cloud Shell 편집기에서 leaderboard.go
파일을 수정하여 Players
테이블에 100명의 플레이어를 삽입하기 위해 사용될 수 있는 insertplayers
명령어를 추가합니다. 또한 Players
테이블에 있는 각 플레이어에 대해 Scores
테이블에 4개의 무작위 점수를 삽입하기 위해 사용될 수 있는 insertscores
명령어를 추가합니다.
먼저 leaderboard.go
파일의 상단에서 imports
섹션을 업데이트하고 현재 있는 항목을 바꿔서 다음과 같이 표시되도록 합니다.
import (
"context"
"flag"
"fmt"
"io"
"log"
"math/rand"
"os"
"regexp"
"time"
"cloud.google.com/go/spanner"
database "cloud.google.com/go/spanner/admin/database/apiv1"
"google.golang.org/api/iterator"
adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)
그런 후 파일 상단의 'type adminCommand ...'로 시작하는 줄 바로 아래에 명령어 목록과 함께 새 명령어 유형을 추가하면 다음과 같이 표시됩니다.
type adminCommand func(ctx context.Context, w io.Writer, adminClient *database.DatabaseAdminClient, database string) error
type command func(ctx context.Context, w io.Writer, client *spanner.Client) error
var (
commands = map[string]command{
"insertplayers": insertPlayers,
"insertscores": insertScores,
}
)
그런 후 다음 insertPlayers 및 insertScores 함수를 기존 createdatabase()
함수 아래에 추가합니다.
func insertPlayers(ctx context.Context, w io.Writer, client *spanner.Client) error {
// Get number of players to use as an incrementing value for each PlayerName to be inserted
stmt := spanner.Statement{
SQL: `SELECT Count(PlayerId) as PlayerCount FROM Players`,
}
iter := client.Single().Query(ctx, stmt)
defer iter.Stop()
row, err := iter.Next()
if err != nil {
return err
}
var numberOfPlayers int64 = 0
if err := row.Columns(&numberOfPlayers); err != nil {
return err
}
// Intialize values for random PlayerId
rand.Seed(time.Now().UnixNano())
min := 1000000000
max := 9000000000
// Insert 100 player records into the Players table
_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
stmts := []spanner.Statement{}
for i := 1; i <= 100; i++ {
numberOfPlayers++
playerID := rand.Intn(max-min) + min
playerName := fmt.Sprintf("Player %d", numberOfPlayers)
stmts = append(stmts, spanner.Statement{
SQL: `INSERT INTO Players
(PlayerId, PlayerName)
VALUES (@playerID, @playerName)`,
Params: map[string]interface{}{
"playerID": playerID,
"playerName": playerName,
},
})
}
_, err := txn.BatchUpdate(ctx, stmts)
if err != nil {
return err
}
return nil
})
fmt.Fprintf(w, "Inserted players \n")
return nil
}
func insertScores(ctx context.Context, w io.Writer, client *spanner.Client) error {
playerRecordsFound := false
// Create slice for insert statements
stmts := []spanner.Statement{}
// Select all player records
stmt := spanner.Statement{SQL: `SELECT PlayerId FROM Players`}
iter := client.Single().Query(ctx, stmt)
defer iter.Stop()
// Insert 4 score records into the Scores table for each player in the Players table
for {
row, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
playerRecordsFound = true
var playerID int64
if err := row.ColumnByName("PlayerId", &playerID); err != nil {
return err
}
// Intialize values for random score and date
rand.Seed(time.Now().UnixNano())
min := 1000
max := 1000000
for i := 0; i < 4; i++ {
// Generate random score between 1,000 and 1,000,000
score := rand.Intn(max-min) + min
// Generate random day within the past two years
now := time.Now()
endDate := now.Unix()
past := now.AddDate(0, -24, 0)
startDate := past.Unix()
randomDateInSeconds := rand.Int63n(endDate-startDate) + startDate
randomDate := time.Unix(randomDateInSeconds, 0)
// Add insert statement to stmts slice
stmts = append(stmts, spanner.Statement{
SQL: `INSERT INTO Scores
(PlayerId, Score, Timestamp)
VALUES (@playerID, @score, @timestamp)`,
Params: map[string]interface{}{
"playerID": playerID,
"score": score,
"timestamp": randomDate,
},
})
}
}
if !playerRecordsFound {
fmt.Fprintln(w, "No player records currently exist. First insert players then insert scores.")
} else {
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
// Commit insert statements for all scores to be inserted as a single transaction
_, err := txn.BatchUpdate(ctx, stmts)
return err
})
if err != nil {
return err
}
fmt.Fprintln(w, "Inserted scores")
}
return nil
}
그런 후 insert
명령어 작동을 위해 createdatabase
처리 문 아래의 애플리케이션의 'run' 함수에 다음 코드를 추가하고 return nil
문을 바꿉니다.
// insert and query commands
cmdFn := commands[cmd]
if cmdFn == nil {
flag.Usage()
os.Exit(2)
}
err := cmdFn(ctx, w, dataClient)
if err != nil {
fmt.Fprintf(w, "%s failed with %v", cmd, err)
}
return err
완료되면 run
함수가 다음과 같이 표시됩니다.
func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
cmd string, db string, timespan int) error {
// createdatabase command
if cmd == "createdatabase" {
err := createDatabase(ctx, w, adminClient, db)
if err != nil {
fmt.Fprintf(w, "%s failed with %v", cmd, err)
}
return err
}
// insert and query commands
cmdFn := commands[cmd]
if cmdFn == nil {
flag.Usage()
os.Exit(2)
}
err := cmdFn(ctx, w, dataClient)
if err != nil {
fmt.Fprintf(w, "%s failed with %v", cmd, err)
}
return err
}
애플리케이션에 '삽입' 기능을 추가하기 위한 마지막 단계는 flag.Usage()
함수에 대한 'insertplayers' 및 'insertscores' 명령어에 대해 도움말 텍스트를 추가하는 것입니다. 삽입 명령어에 대해 도움말 텍스트를 포함하도록 flag.Usage()
함수에 다음 도움말 텍스트를 추가합니다.
가능한 명령어 목록에 2개 명령어를 추가합니다.
Command can be one of: createdatabase, insertplayers, insertscores
이 추가 도움말 텍스트를 createdatabase
명령어의 도움말 텍스트 아래에 추가합니다.
leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db
- Insert 100 sample Player records into the database.
leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db
- Insert sample score data into Scores sample Cloud Spanner database table.
Cloud Shell 편집기의 '파일' 메뉴에서 '저장'을 선택하여 변경사항을 leaderboard.go
파일에 저장합니다.
golang-samples/spanner/spanner_leaderboard
디렉터리에서 leaderboard.go
파일을 사용하여 insertplayers
및 insertscores
명령어를 사용 설정하기 위해 코드를 추가한 후 leaderboard.go
파일에 필요한 결과 예시를 확인할 수 있습니다.
이제 애플리케이션을 빌드하고 실행하여 새 insertplayers
및 insertscores
명령어가 애플리케이션의 사용 가능한 명령어 목록에 포함되었는지 확인합니다. 다음 명령어를 실행하여 애플리케이션을 빌드합니다.
go build leaderboard.go
다음 명령어를 입력하여 Cloud Shell에서 결과 애플리케이션을 실행합니다.
./leaderboard
이제 insertplayers
및 insertscores
명령어가 애플리케이션의 기본 출력에 포함된 것이 확인됩니다.
Usage: leaderboard <command> <database_name> [command_option] Command can be one of: createdatabase, insertplayers, insertscores Examples: leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db - Create a sample Cloud Spanner database along with sample tables in your project. leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db - Insert 100 sample Player records into the database. leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db - Insert sample score data into Scores sample Cloud Spanner database table.
이제 createdatabase
명령어를 호출할 때 사용한 것과 동일한 인수 값을 사용하여 insertplayers
명령어를 실행합니다. my-project
를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.
./leaderboard insertplayers projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard
몇 초 후 다음과 같은 응답이 표시됩니다.
Inserted players
이제 Go 클라이언트 라이브러리를 사용하여 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에서 수행됩니다.
이제 insertplayers
명령어를 호출할 때 사용한 것과 동일한 인수 값을 사용하여 insertscores
명령어를 실행합니다. my-project
를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.
./leaderboard insertscores projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard
몇 초 후 다음과 같은 응답이 표시됩니다.
Inserted scores
insertScores
함수를 실행하면 다음 코드 스니펫을 사용하여 과거의 날짜/시간으로 무작위로 생성된 타임스탬프를 삽입합니다.
now := time.Now()
endDate := now.Unix()
past := now.AddDate(0, -24, 0)
startDate := past.Unix()
randomDateInSeconds := rand.Int63n(endDate-startDate) + startDate
randomDate := time.Unix(randomDateInSeconds, 0)
stmts = append(stmts, spanner.Statement{
SQL: `INSERT INTO Scores
(PlayerId, Score, Timestamp)
VALUES (@playerID, @score, @timestamp)`,
Params: map[string]interface{}{
"playerID": playerID,
"score": score,
"timestamp": randomDate,
},
})
정확히 '삽입' 트랜잭션이 수행될 때의 타임스탬프를 Timestamp
열에 자동으로 입력하려면 다음 코드 스니펫과 같이 Go 상수 spanner.CommitTimestamp
를 대신 삽입할 수 있습니다.
...
stmts = append(stmts, spanner.Statement{
SQL: `INSERT INTO Scores
(PlayerId, Score, Timestamp)
VALUES (@playerID, @score, @timestamp)`,
Params: map[string]interface{}{
"playerID": playerID,
"score": score,
"timestamp": spanner.CommitTimestamp,
},
})
이제 데이터 로드가 완료되었으므로 Cloud Console의 Cloud Spanner 섹션에서 새 테이블에 기록한 값을 확인합니다. 먼저 leaderboard
데이터베이스를 선택하고 Players
테이블을 선택합니다. Data
탭을 클릭합니다. 테이블의 PlayerId
및 PlayerName
열에 데이터가 포함된 것을 알 수 있습니다.
이제 Scores
테이블을 클릭하고 Data
탭을 선택하여 점수 테이블에도 데이터가 있는지 확인합니다. 테이블의 PlayerId
, Timestamp
, Score
열에 데이터가 포함된 것을 알 수 있습니다.
잘 하셨습니다. 게임 리더보드를 만들기 위해 사용할 수 있는 몇 가지 쿼리를 실행하기 위해 앱을 업데이트해보겠습니다.
이제 데이터베이스가 설정되었고 테이블에 정보가 로드되었으므로, 이 데이터를 사용하는 리더보드를 만듭니다. 이를 위해서는 다음 4개 질문에 답변해야 합니다.
- 항상 '상위 10대' 플레이어는 누구인가요?
- 올해 '상위 10대' 플레이어는 누구인가요?
- 이달의 '상위 10대' 플레이어는 누구인가요?
- 이번 주 '상위 10대' 플레이어는 누구인가요?
이러한 질문에 답변하는 SQL 쿼리를 실행하도록 애플리케이션을 업데이트합니다.
리더보드에 필요한 정보를 생성하는 질문에 답변하도록 쿼리를 실행할 수 있는 방법을 제공하는 query
명령어 및 queryWithTimespan
명령어를 추가합니다.
Cloud Shell 편집기에서 leaderboard.go
파일을 편집하여 query
명령어 및 queryWithTimespan
명령어를 추가하도록 애플리케이션을 업데이트합니다. 또한 점수를 쉼표 형식으로 지정하기 위해 formatWithCommas
도우미 함수를 추가합니다.
먼저 leaderboard.go
파일의 상단에서 imports
섹션을 업데이트하고 현재 있는 항목을 바꿔서 다음과 같이 표시되도록 합니다.
import (
"bytes"
"context"
"flag"
"fmt"
"io"
"log"
"math/rand"
"os"
"regexp"
"strconv"
"time"
"cloud.google.com/go/spanner"
database "cloud.google.com/go/spanner/admin/database/apiv1"
"google.golang.org/api/iterator"
adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)
그런 후 다음 두 함수와 도우미 함수를 기존 insertScores
메서드 아래에 추가합니다.
func query(ctx context.Context, w io.Writer, client *spanner.Client) error {
stmt := spanner.Statement{
SQL: `SELECT p.PlayerId, p.PlayerName, s.Score, s.Timestamp
FROM Players p
JOIN Scores s ON p.PlayerId = s.PlayerId
ORDER BY s.Score DESC LIMIT 10`}
iter := client.Single().Query(ctx, stmt)
defer iter.Stop()
for {
row, err := iter.Next()
if err == iterator.Done {
return nil
}
if err != nil {
return err
}
var playerID, score int64
var playerName string
var timestamp time.Time
if err := row.Columns(&playerID, &playerName, &score, ×tamp); err != nil {
return err
}
fmt.Fprintf(w, "PlayerId: %d PlayerName: %s Score: %s Timestamp: %s\n",
playerID, playerName, formatWithCommas(score), timestamp.String()[0:10])
}
}
func queryWithTimespan(ctx context.Context, w io.Writer, client *spanner.Client, timespan int) error {
stmt := spanner.Statement{
SQL: `SELECT p.PlayerId, p.PlayerName, s.Score, s.Timestamp
FROM Players p
JOIN Scores s ON p.PlayerId = s.PlayerId
WHERE s.Timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL @Timespan HOUR)
ORDER BY s.Score DESC LIMIT 10`,
Params: map[string]interface{}{"Timespan": timespan},
}
iter := client.Single().Query(ctx, stmt)
defer iter.Stop()
for {
row, err := iter.Next()
if err == iterator.Done {
return nil
}
if err != nil {
return err
}
var playerID, score int64
var playerName string
var timestamp time.Time
if err := row.Columns(&playerID, &playerName, &score, ×tamp); err != nil {
return err
}
fmt.Fprintf(w, "PlayerId: %d PlayerName: %s Score: %s Timestamp: %s\n",
playerID, playerName, formatWithCommas(score), timestamp.String()[0:10])
}
}
func formatWithCommas(n int64) string {
numberAsString := strconv.FormatInt(n, 10)
numberLength := len(numberAsString)
if numberLength < 4 {
return numberAsString
}
var buffer bytes.Buffer
comma := []rune(",")
bufferPosition := numberLength % 3
if (bufferPosition) > 0 {
bufferPosition = 3 - bufferPosition
}
for i := 0; i < numberLength; i++ {
if bufferPosition == 3 {
buffer.WriteRune(comma[0])
bufferPosition = 0
}
bufferPosition++
buffer.WriteByte(numberAsString[i])
}
return buffer.String()
}
leaderboard.go
파일 상단에서 commands
변수가 다음과 같이 표시되도록 'insertscores": insertScores
' 옵션 바로 아래의 commands
변수에 하나의 명령어 옵션으로 'query'를 추가합니다.
var (
commands = map[string]command{
"insertplayers": insertPlayers,
"insertscores": insertScores,
"query": query,
}
)
그런 후 run
함수 내에서 'createdatabase' 함수 섹션 아래 그리고 'insert 및 query' 명령어 처리 섹션 위에 명령어 옵션으로 'queryWithTimespan'을 추가합니다.
// querywithtimespan command
if cmd == "querywithtimespan" {
err := queryWithTimespan(ctx, w, dataClient, timespan)
if err != nil {
fmt.Fprintf(w, "%s failed with %v", cmd, err)
}
return err
}
완료되면 run
함수가 다음과 같이 표시됩니다.
func run(ctx context.Context, adminClient *database.DatabaseAdminClient, dataClient *spanner.Client, w io.Writer,
cmd string, db string, timespan int) error {
// createdatabase command
if cmd == "createdatabase" {
err := createDatabase(ctx, w, adminClient, db)
if err != nil {
fmt.Fprintf(w, "%s failed with %v", cmd, err)
}
return err
}
// querywithtimespan command
if cmd == "querywithtimespan" {
if timespan == 0 {
flag.Usage()
os.Exit(2)
}
err := queryWithTimespan(ctx, w, dataClient, timespan)
if err != nil {
fmt.Fprintf(w, "%s failed with %v", cmd, err)
}
return err
}
// insert and query commands
cmdFn := commands[cmd]
if cmdFn == nil {
flag.Usage()
os.Exit(2)
}
err := cmdFn(ctx, w, dataClient)
if err != nil {
fmt.Fprintf(w, "%s failed with %v", cmd, err)
}
return err
}
그런 후 queryWithTimespan
명령어 작동을 위해 다음과 같이 표시되도록 애플리케이션의 'main' 메서드에서 flag.Parse() 코드 블록을 업데이트합니다.
flag.Parse()
flagCount := len(flag.Args())
if flagCount < 2 || flagCount > 3 {
flag.Usage()
os.Exit(2)
}
cmd, db := flag.Arg(0), flag.Arg(1)
// If query timespan flag is specified, parse to int
var timespan int = 0
if flagCount == 3 {
parsedTimespan, err := strconv.Atoi(flag.Arg(2))
if err != nil {
fmt.Println(err)
os.Exit(2)
}
timespan = parsedTimespan
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
adminClient, dataClient := createClients(ctx, db)
if err := run(ctx, adminClient, dataClient, os.Stdout, cmd, db, timespan); err != nil {
os.Exit(1)
}
애플리케이션에 'query' 기능을 추가하기 위한 마지막 단계는 'query' 및 'querywithtimespan' 명령어에 대한 도움말 텍스트를 flag.Usage()
함수에 추가하는 것입니다. 쿼리 명령어의 도움말 텍스트를 포함하도록 flag.Usage()
함수에 다음 코드 줄을 추가합니다.
가능한 명령어 목록에 2개의 'query' 명령어를 추가합니다.
Command can be one of: createdatabase, insertplayers, insertscores, query, querywithtimespan
이 추가 도움말 텍스트를 insertscores
명령어의 도움말 텍스트 아래에 추가합니다.
leaderboard query projects/my-project/instances/my-instance/databases/example-db
- Query players with top ten scores of all time.
leaderboard querywithtimespan projects/my-project/instances/my-instance/databases/example-db 168
- Query players with top ten scores within a timespan specified in hours.
Cloud Shell 편집기의 '파일' 메뉴에서 '저장'을 선택하여 변경사항을 leaderboard.go
파일에 저장합니다.
golang-samples/spanner/spanner_leaderboard
디렉터리에서 leaderboard.go
파일을 사용하여 query
및 querywithtimespan
명령어를 사용 설정하기 위해 코드를 추가한 후 leaderboard.go
파일에 필요한 결과 예시를 확인할 수 있습니다.
이제 애플리케이션을 빌드하고 실행하여 새 query
및 querywithtimespan
명령어가 애플리케이션의 사용 가능한 명령어 목록에 포함되었는지 확인합니다.
Cloud Shell에서 다음 명령어를 실행하여 애플리케이션을 빌드합니다.
go build leaderboard.go
다음 명령어를 입력하여 Cloud Shell에서 결과 애플리케이션을 실행합니다.
./leaderboard
이제 query
및 querywithtimespan
명령어가 앱의 기본 출력에서 새 명령어 옵션으로 포함된 것을 확인할 수 있습니다.
Usage: leaderboard <command> <database_name> [command_option] Command can be one of: createdatabase, insertplayers, insertscores, query, querywithtimespan Examples: leaderboard createdatabase projects/my-project/instances/my-instance/databases/example-db - Create a sample Cloud Spanner database along with sample tables in your project. leaderboard insertplayers projects/my-project/instances/my-instance/databases/example-db - Insert 100 sample Player records into the database. leaderboard insertscores projects/my-project/instances/my-instance/databases/example-db - Insert sample score data into Scores sample Cloud Spanner database table. leaderboard query projects/my-project/instances/my-instance/databases/example-db - Query players with top ten scores of all time. leaderboard querywithtimespan projects/my-project/instances/my-instance/databases/example-db 168 - Query players with top ten scores within a timespan specified in hours.
응답에서 query
명령어를 사용하여 항상 '상위 10대' 플레이어 목록을 가져올 수 있음을 알 수 있습니다. 또한 querywithtimespan
명령어를 사용하여 Scores
테이블의 Timestamp
열에 있는 값을 기준으로 레코드를 필터링하는 데 사용하기 위해 기간을 시간 단위로 지정할 수 있다는 것을 알 수 있습니다.
create
명령어를 실행할 때 사용한 것과 동일한 인수 값을 사용하여 query
명령어를 실행해보세요. my-project
를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.
./leaderboard query projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard
다음과 같이 항상 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.
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
이제 1년 동안의 시간 수인 8760과 동일한 '기간'을 지정하여 1년 동안 '상위 10대' 플레이어를 쿼리하는 데 필요한 인수를 사용하여 querywithtimespan
명령어를 실행합니다. my-project
를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.
./leaderboard querywithtimespan projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 8760
다음과 같이 1년 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.
PlayerId: 1981654448 PlayerName: Player 35 Score: 997,480 Timestamp: 2018-12-06
PlayerId: 4953940705 PlayerName: Player 87 Score: 995,184 Timestamp: 2018-09-14
PlayerId: 1788051688 PlayerName: Player 76 Score: 992,265 Timestamp: 2018-11-22
PlayerId: 6862349579 PlayerName: Player 30 Score: 990,877 Timestamp: 2018-09-14
PlayerId: 5529627211 PlayerName: Player 16 Score: 989,142 Timestamp: 2018-03-30
PlayerId: 9743904155 PlayerName: Player 1 Score: 988,765 Timestamp: 2018-05-30
PlayerId: 6809119884 PlayerName: Player 7 Score: 986,673 Timestamp: 2018-05-16
PlayerId: 2132710638 PlayerName: Player 54 Score: 983,108 Timestamp: 2018-09-11
PlayerId: 2320093590 PlayerName: Player 79 Score: 981,373 Timestamp: 2018-05-07
PlayerId: 9554181430 PlayerName: Player 80 Score: 981,087 Timestamp: 2018-06-21
이제 querywithtimespan
명령어를 실행하고 '기간'을 1개월 동안의 시간 수인 730과 동일하게 지정하여 1개월 동안 '상위 10'대 플레이어를 쿼리합니다. my-project
를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.
./leaderboard querywithtimespan projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 730
다음과 같이 1개월 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.
PlayerId: 3869829195 PlayerName: Player 69 Score: 949,686 Timestamp: 2019-02-19
PlayerId: 7448359883 PlayerName: Player 20 Score: 938,998 Timestamp: 2019-02-07
PlayerId: 1981654448 PlayerName: Player 35 Score: 929,003 Timestamp: 2019-02-22
PlayerId: 9336678658 PlayerName: Player 44 Score: 914,106 Timestamp: 2019-01-27
PlayerId: 6968576389 PlayerName: Player 40 Score: 898,041 Timestamp: 2019-02-21
PlayerId: 5529627211 PlayerName: Player 16 Score: 896,433 Timestamp: 2019-01-29
PlayerId: 9395039625 PlayerName: Player 59 Score: 879,495 Timestamp: 2019-02-09
PlayerId: 2094604854 PlayerName: Player 39 Score: 860,434 Timestamp: 2019-02-01
PlayerId: 9395039625 PlayerName: Player 59 Score: 849,955 Timestamp: 2019-02-21
PlayerId: 4285713246 PlayerName: Player 51 Score: 805,654 Timestamp: 2019-02-02
이제 querywithtimespan
명령어를 실행하고 '기간'을 1주일 동안의 시간 수인 168과 동일하게 지정하여 1주일 동안 '상위 10'대 플레이어를 쿼리합니다. my-project
를 이 Codelab 시작 부분에서 만든 프로젝트 ID로 바꿔야 합니다.
./leaderboard querywithtimespan projects/my-project/instances/cloudspanner-leaderboard/databases/leaderboard 168
다음과 같이 1주일 동안 '상위 10대' 플레이어를 포함하는 응답이 표시됩니다.
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 기술을 통해 게임 리더보드가 계속 정확하게 확장될 수 있습니다.
Spanner에 대한 모든 학습이 완료되었으면 귀중한 리소스와 비용을 절약하기 위해 사용된 리소스를 삭제해야 합니다. 이 단계는 매우 간단합니다. Cloud Console의 Cloud Spanner 섹션으로 이동하고 'Cloud Spanner 인스턴스 설정' Codelab 단계에서 만든 인스턴스만 삭제하면 됩니다.
학습한 내용:
- 리더보드에 대한 Google Cloud Spanner 인스턴스, 데이터베이스, 테이블 스키마
- Go 콘솔 애플리케이션을 만드는 방법
- Go 클라이언트 라이브러리를 사용하여 Spanner 데이터베이스 및 테이블을 만드는 방법
- Go 클라이언트 라이브러리를 사용하여 Spanner 데이터베이스에 데이터를 로드하는 방법
- Spanner 커밋 타임스탬프 및 Go 클라이언트 라이브러리를 사용하여 데이터에서 '상위 10대' 결과를 쿼리하는 방법
다음 단계:
- Spanner CAP 백서 읽어보기
- 스키마 설계 및 쿼리 권장사항 알아보기
- Cloud Spanner 커밋 타임스탬프 자세히 알아보기
Google에 의견 보내기
- 잠시 시간을 내어 간단한 설문조사에 응해주시기 바랍니다.