C#에서 Cloud Spanner 시작하기

목표

이 가이드에서는 C#용 Cloud Spanner 클라이언트 라이브러리를 사용하여 다음 단계를 안내합니다.

  • Cloud Spanner 인스턴스 및 데이터베이스 만들기
  • 데이터베이스에서 데이터 읽기, 쓰기 및 데이터에서 SQL 쿼리 실행
  • 데이터베이스 스키마 업데이트
  • 읽기-쓰기 트랜잭션을 사용하여 데이터 업데이트
  • 데이터베이스에 보조 색인 추가
  • 색인을 사용하여 데이터 읽기 및 데이터에서 SQL 쿼리 실행
  • 읽기 전용 트랜잭션을 사용하여 데이터 검색

비용

이 가이드에서는 Google Cloud의 비용 청구 가능한 구성요소인 Cloud Spanner를 사용합니다. Cloud Spanner 사용 비용에 대한 자세한 내용은 가격 책정을 참조하세요.

시작하기 전에

설정에 설명된 단계를 완료하세요. 기본 Google Cloud 프로젝트 생성, 결제 사용 설정, Cloud Spanner API 사용 설정을 수행하고 Cloud Spanner API 사용에 필요한 사용자 인증 정보를 가져오기 위한 OAuth 2.0 설정을 완료해야 합니다.

특히 gcloud auth application-default login을 실행하여 사용자 인증 정보로 로컬 개발 환경을 설정해야 합니다.

로컬 C# 환경 준비

  1. GOOGLE_PROJECT_ID 환경 변수를 Google Cloud 프로젝트 ID로 설정합니다.

    1. 우선, 다음과 같이 현재의 PowerShell 세션에 GOOGLE_PROJECT_ID를 설정합니다.

      $env:GOOGLE_PROJECT_ID = "MY_PROJECT_ID"
    2. 그런 다음 이 명령어 뒤에 생성되는 모든 프로세스에 GOOGLE_PROJECT_ID를 설정합니다.

      [Environment]::SetEnvironmentVariable("GOOGLE_PROJECT_ID", "MY_PROJECT_ID", "User")
  2. 사용자 인증 정보를 다운로드합니다.

    1. Google Cloud Console에서 사용자 인증 정보 페이지로 이동합니다.

      사용자 인증 정보 페이지로 이동

    2. 사용자 인증 정보 생성을 클릭하고 서비스 계정 키를 선택합니다.

    3. '서비스 계정'에서 Compute Engine 기본 서비스 계정을 선택하고, '키 유형'에서 선택된 JSON을 그대로 둡니다. 만들기를 클릭합니다. 컴퓨터가 JSON 파일을 다운로드합니다.

  3. 사용자 인증 정보를 설정합니다. C 드라이브에 위치한 CURRENT_USER의 Downloads 디렉터리에 있는 FILENAME.json이라는 파일의 경우, 다음 명령어를 실행하여 GOOGLE_APPLICATION_CREDENTIALS가 JSON 키를 가리키도록 설정합니다.

    1. 먼저 이 PowerShell 세션에 GOOGLE_APPLICATION_CREDENTIALS를 설정하려면 다음을 사용합니다.

      $env:GOOGLE_APPLICATION_CREDENTIALS = "C:\Users\CURRENT_USER\Downloads\FILENAME.json"
    2. 그런 다음 이 명령어 뒤에 생성되는 모든 프로세스에 GOOGLE_APPLICATION_CREDENTIALS를 설정합니다.

      [Environment]::SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", "C:\Users\CURRENT_USER\Downloads\FILENAME.json", "User")
  4. 샘플 앱 저장소를 로컬 머신에 클론합니다.

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

    또는 ZIP 파일로 샘플을 다운로드하고 압축을 풉니다.

  5. Visual Studio 2017 이상을 사용하여 다운로드된 저장소의 dotnet-docs-samples\spanner\api 디렉터리에 위치한 Spanner.sln을 연 다음 빌드합니다.

  6. 다운로드된 저장소 안의 컴파일된 애플리케이션이 있는 디렉터리로 변경합니다. 예:

    cd dotnet-docs-samples\spanner\api\Spanner
    

인스턴스 만들기

Cloud Spanner를 처음 사용하는 경우 인스턴스를 만들어야 합니다. 이 인스턴스는 Cloud Spanner 데이터베이스에서 사용되는 리소스를 할당한 것입니다. 인스턴스를 만들 때는 인스턴스 구성을 선택합니다. 이 구성에 따라 데이터 저장 위치와 사용할 노드 수가 결정되고, 또한 노드 수에 따라 인스턴스의 제공 리소스 및 스토리지 리소스 양이 결정됩니다.

us-central1 리전에 1개의 노드로 Cloud Spanner 인스턴스를 만들려면 다음 명령어를 실행합니다.

gcloud spanner instances create test-instance --config=regional-us-central1 `
    --description="Test Instance" --nodes=1

그러면 다음과 같은 특성을 가진 인스턴스가 생성됩니다.

  • 인스턴스 ID: test-instance
  • 표시 이름: Test Instance
  • 인스턴스 구성: regional-us-central1. 리전별 구성은 한 리전에 데이터를 저장하는 반면 멀티 리전 구성은 여러 리전에 데이터를 분산시킵니다. 인스턴스에서 자세히 알아보세요.
  • 노드 수: 1개. node_count에 따라 인스턴스의 데이터베이스에서 사용할 수 있는 제공 리소스 및 스토리지 리소스의 양이 달라집니다. 노드 수에서 자세히 알아보세요.

다음과 같이 표시됩니다.

Creating instance...done.

샘플 파일 살펴보기

샘플 저장소에는 C#으로 Cloud Spanner를 사용하는 방법을 보여주는 샘플이 있습니다.

데이터베이스를 만들고 데이터베이스 스키마를 수정하는 방법을 보여주는 spanner/api/Spanner/Program.cs 파일을 살펴봅니다. 데이터는 스키마 및 데이터 모델 페이지에 나와 있는 스키마 예시를 사용합니다.

데이터베이스 만들기

명령줄에서 다음을 실행하여 test-instance라는 인스턴스에 example-db라는 데이터베이스를 만듭니다.

dotnet run createSampleDatabase $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같이 표시됩니다.

Created sample database example-db on instance test-instance

Cloud Spanner 데이터베이스가 생성되었습니다. 다음은 데이터베이스를 만든 코드입니다.

// Initialize request connection string for database creation.
string connectionString =
    $"Data Source=projects/{projectId}/instances/{instanceId}";
// Make the request.
using (var connection = new SpannerConnection(connectionString))
{
    string createStatement = $"CREATE DATABASE `{databaseId}`";
    var cmd = connection.CreateDdlCommand(createStatement);
    await cmd.ExecuteNonQueryAsync();
}
// Update connection string with Database ID for table creation.
connectionString = connectionString + $"/databases/{databaseId}";
using (var connection = new SpannerConnection(connectionString))
{
    // Define create table statement for table #1.
    string createTableStatement =
   @"CREATE TABLE Singers (
         SingerId INT64 NOT NULL,
         FirstName    STRING(1024),
         LastName STRING(1024),
         ComposerInfo   BYTES(MAX)
     ) PRIMARY KEY (SingerId)";
    // Make the request.
    var cmd = connection.CreateDdlCommand(createTableStatement);
    await cmd.ExecuteNonQueryAsync();
    // Define create table statement for table #2.
    createTableStatement =
    @"CREATE TABLE Albums (
         SingerId     INT64 NOT NULL,
         AlbumId      INT64 NOT NULL,
         AlbumTitle   STRING(MAX)
     ) PRIMARY KEY (SingerId, AlbumId),
     INTERLEAVE IN PARENT Singers ON DELETE CASCADE";
    // Make the request.
    cmd = connection.CreateDdlCommand(createTableStatement);
    await cmd.ExecuteNonQueryAsync();
}

또한 이 코드는 기본 음악 애플리케이션용 테이블 두 개(SingersAlbums)를 정의합니다. 이러한 테이블은 이 페이지 전체에서 사용됩니다. 스키마 예시를 아직 보지 않았다면 살펴보세요.

다음 단계는 데이터베이스에 데이터 쓰기입니다.

데이터베이스 클라이언트 만들기

읽기 또는 쓰기를 수행하려면 먼저 Spanner​Connection을 만들어야 합니다.


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

namespace GoogleCloudSamples.Spanner
{
    public class QuickStart
    {
        static async Task MainAsync()
        {
            string projectId = "YOUR-PROJECT-ID";
            string instanceId = "my-instance";
            string databaseId = "my-database";
            string connectionString =
                $"Data Source=projects/{projectId}/instances/{instanceId}/"
                + $"databases/{databaseId}";
            // Create connection to Cloud Spanner.
            using (var connection = new SpannerConnection(connectionString))
            {
                // Execute a simple SQL statement.
                var cmd = connection.CreateSelectCommand(
                    @"SELECT ""Hello World"" as test");
                using (var reader = await cmd.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        Console.WriteLine(
                            reader.GetFieldValue<string>("test"));
                    }
                }
            }
        }
        public static void Main(string[] args)
        {
            MainAsync().Wait();
        }
    }
}

Spanner​Connection를 데이터베이스 연결이라고 생각하면 됩니다. Cloud Spanner와의 모든 상호작용은 Spanner​Connection를 거쳐야 합니다.

자세한 내용은 Spanner​Connection 참조에서 확인하세요.

DML을 사용하여 데이터 쓰기

읽기-쓰기 트랜잭션에서 DML(Data Manipulation Language)을 사용하여 데이터를 삽입할 수 있습니다.

ExecuteNonQueryAsync() 메서드를 사용하여 DML 문을 실행합니다.

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

    // Create connection to Cloud Spanner.
    using (var connection =
        new SpannerConnection(connectionString))
    {
        await connection.OpenAsync();

        SpannerCommand cmd = connection.CreateDmlCommand(
            "INSERT Singers (SingerId, FirstName, LastName) VALUES "
               + "(12, 'Melissa', 'Garcia'), "
               + "(13, 'Russell', 'Morales'), "
               + "(14, 'Jacqueline', 'Long'), "
               + "(15, 'Dylan', 'Shaw')");
        int rowCount = await cmd.ExecuteNonQueryAsync();
        Console.WriteLine($"{rowCount} row(s) inserted...");
    }
}

writeUsingDml 인수를 사용하여 샘플을 실행합니다.

dotnet run writeUsingDml $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같이 표시됩니다.

4 row(s) inserted...

변형을 사용하여 데이터 쓰기

변형을 사용하여 데이터를 삽입할 수도 있습니다.

connection.CreateInsertCommand() 메서드를 사용하여 데이터를 삽입할 수 있습니다. 이 메서드는 테이블에 행을 삽입하는 새 SpannerCommand를 만듭니다. SpannerCommand.ExecuteNonQueryAsync() 메서드는 테이블에 새 행을 추가합니다.

이 코드는 변형을 사용하여 데이터를 삽입하는 방법을 보여줍니다.

public class Singer
{
    public int SingerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Album
{
    public int SingerId { get; set; }
    public int AlbumId { get; set; }
    public string AlbumTitle { get; set; }
}
public static async Task InsertSampleDataAsync(
    string projectId, string instanceId, string databaseId)
{
    const int firstSingerId = 1;
    const int secondSingerId = 2;
    string connectionString =
    $"Data Source=projects/{projectId}/instances/{instanceId}"
    + $"/databases/{databaseId}";
    List<Singer> singers = new List<Singer>
    {
        new Singer { SingerId = firstSingerId, FirstName = "Marc",
            LastName = "Richards" },
        new Singer { SingerId = secondSingerId, FirstName = "Catalina",
            LastName = "Smith" },
        new Singer { SingerId = 3, FirstName = "Alice",
            LastName = "Trentor" },
        new Singer { SingerId = 4, FirstName = "Lea",
            LastName = "Martin" },
        new Singer { SingerId = 5, FirstName = "David",
            LastName = "Lomond" },
    };
    List<Album> albums = new List<Album>
    {
        new Album { SingerId = firstSingerId, AlbumId = 1,
            AlbumTitle = "Total Junk" },
        new Album { SingerId = firstSingerId, AlbumId = 2,
            AlbumTitle = "Go, Go, Go" },
        new Album { SingerId = secondSingerId, AlbumId = 1,
            AlbumTitle = "Green" },
        new Album { SingerId = secondSingerId, AlbumId = 2,
            AlbumTitle = "Forever Hold your Peace" },
        new Album { SingerId = secondSingerId, AlbumId = 3,
            AlbumTitle = "Terrified" },
    };
    // Create connection to Cloud Spanner.
    using (var connection = new SpannerConnection(connectionString))
    {
        await connection.OpenAsync();

        // Insert rows into the Singers table.
        var cmd = connection.CreateInsertCommand("Singers",
            new SpannerParameterCollection
            {
                { "SingerId", SpannerDbType.Int64 },
                { "FirstName", SpannerDbType.String },
                { "LastName", SpannerDbType.String }
            });
        await Task.WhenAll(singers.Select(singer =>
        {
            cmd.Parameters["SingerId"].Value = singer.SingerId;
            cmd.Parameters["FirstName"].Value = singer.FirstName;
            cmd.Parameters["LastName"].Value = singer.LastName;
            return cmd.ExecuteNonQueryAsync();
        }));

        // Insert rows into the Albums table.
        cmd = connection.CreateInsertCommand("Albums",
            new SpannerParameterCollection
            {
                { "SingerId", SpannerDbType.Int64 },
                { "AlbumId", SpannerDbType.Int64 },
                { "AlbumTitle", SpannerDbType.String }
            });
        await Task.WhenAll(albums.Select(album =>
        {
            cmd.Parameters["SingerId"].Value = album.SingerId;
            cmd.Parameters["AlbumId"].Value = album.AlbumId;
            cmd.Parameters["AlbumTitle"].Value = album.AlbumTitle;
            return cmd.ExecuteNonQueryAsync();
        }));
        Console.WriteLine("Inserted data.");
    }
}

insertSampleData 인수를 사용하여 샘플을 실행합니다.

dotnet run insertSampleData $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같이 표시됩니다.

Inserted data.

SQL을 사용하여 데이터 쿼리

Cloud Spanner는 데이터 읽기용 네이티브 SQL 인터페이스를 지원하며, 이 인터페이스에 액세스하려면 명령줄에서 gcloud 명령줄 도구를 사용하거나 C#용 Cloud Spanner 클라이언트 라이브러리를 프로그래매틱 방식으로 사용하면 됩니다.

명령줄에서

다음 SQL 문을 실행하여 Albums 테이블에서 모든 열의 값을 읽습니다.

gcloud spanner databases execute-sql example-db --instance=test-instance `
    --sql='SELECT SingerId, AlbumId, AlbumTitle FROM Albums'

결과가 다음과 같이 표시됩니다.

SingerId AlbumId AlbumTitle
1        1       Total Junk
1        2       Go, Go, Go
2        1       Green
2        2       Forever Hold Your Peace
2        3       Terrified

C#용 Cloud Spanner 클라이언트 라이브러리 사용

명령줄에서 SQL 문을 실행하는 방법 외에 C#용 Cloud Spanner 클라이언트 라이브러리를 사용하여 프로그래매틱 방식으로 같은 SQL 문을 실행할 수 있습니다.

ExecuteReaderAsync()를 사용하여 SQL 쿼리를 실행합니다.

string connectionString =
$"Data Source=projects/{projectId}/instances/"
+ $"{instanceId}/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateSelectCommand(
        "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine("SingerId : "
                + reader.GetFieldValue<string>("SingerId")
                + " AlbumId : "
                + reader.GetFieldValue<string>("AlbumId")
                + " AlbumTitle : "
                + reader.GetFieldValue<string>("AlbumTitle"));
        }
    }
}

다음은 쿼리를 실행하고 데이터에 액세스하는 방법입니다.

dotnet run querySampleData $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같은 결과가 표시됩니다.

SingerId: 1 AlbumId: 1 AlbumTitle: Total Junk
SingerId: 1 AlbumId: 2 AlbumTitle: Go, Go, Go
SingerId: 2 AlbumId: 1 AlbumTitle: Green
SingerId: 2 AlbumId: 2 AlbumTitle: Forever Hold your Peace
SingerId: 2 AlbumId: 3 AlbumTitle: Terrified

SQL 매개변수를 사용하여 쿼리

매개변수를 사용하여 SQL 문에 커스텀 값을 포함시킬 수 있습니다. 다음 예시에서는 LastName의 특정 값을 포함하는 레코드를 쿼리하기 위해 WHERE 절에서 매개변수로 @lastName을 사용합니다.

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateSelectCommand(
        "SELECT SingerId, FirstName, LastName FROM Singers "
        + $"WHERE LastName = @lastName",
        new SpannerParameterCollection {
            {"lastName", SpannerDbType.String}});
    cmd.Parameters["lastName"].Value = "Garcia";
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine("SingerId : "
            + reader.GetFieldValue<string>("SingerId")
            + " FirstName : "
            + reader.GetFieldValue<string>("FirstName")
            + " LastName : "
            + reader.GetFieldValue<string>("LastName"));
        }
    }
}

다음은 매개변수와 함께 쿼리를 실행하고 데이터에 액세스하는 방법입니다.

dotnet run queryWithParameter $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같은 결과가 표시됩니다.

SingerId : 12 FirstName : Melissa LastName : Garcia

데이터베이스 스키마 업데이트

Albums 테이블에 MarketingBudget이라는 새 열을 추가해야 한다고 가정합니다. 기존 테이블에 새 열을 추가하려면 데이터베이스 스키마를 업데이트해야 합니다. Cloud Spanner는 데이터베이스에서 트래픽이 계속 처리되는 동안 데이터베이스의 스키마 업데이트를 지원합니다. 스키마 업데이트 시 데이터베이스를 오프라인으로 전환할 필요가 없고 전체 테이블 또는 열이 잠기지 않습니다. 따라서 스키마 업데이트 중에도 데이터베이스에 데이터를 계속 쓸 수 있습니다. 스키마 업데이트에서 지원되는 스키마 업데이트와 스키마 변경 성능에 대해 자세히 알아보세요.

열 추가

명령줄에서 gcloud 명령줄 도구를 사용하거나 C#용 Cloud Spanner 클라이언트 라이브러리를 프로그래매틱 방식으로 사용하여 열을 추가할 수 있습니다.

명령줄에서

다음과 같은 ALTER TABLE 명령어를 사용하여 테이블에 새 열을 추가합니다.

gcloud spanner databases ddl update example-db --instance=test-instance `
    --ddl='ALTER TABLE Albums ADD COLUMN MarketingBudget INT64'

다음과 같이 표시됩니다.

Schema updating...done.

C#용 Cloud Spanner 클라이언트 라이브러리 사용

CreateDdlCommand()를 사용하여 스키마를 수정합니다.

// Initialize request argument(s).
string connectionString =
    $"Data Source=projects/{projectId}/instances/"
    + $"{instanceId}/databases/{databaseId}";
string alterStatement =
    "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64";
// Make the request.
using (var connection = new SpannerConnection(connectionString))
{
    var updateCmd = connection.CreateDdlCommand(alterStatement);
    await updateCmd.ExecuteNonQueryAsync();
}
Console.WriteLine("Added the MarketingBudget column.");

addColumn 명령어를 사용하여 샘플을 실행합니다.

dotnet run addColumn $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같이 표시됩니다.

Added the MarketingBudget column.

새 열에 데이터 쓰기

다음 코드는 새 열에 데이터를 씁니다. 이 코드는 MarketingBudgetAlbums(1, 1)로 키가 지정된 행에서는 100000으로, Albums(2, 2)로 키가 지정된 행에서는 500000으로 설정합니다.

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateUpdateCommand("Albums",
        new SpannerParameterCollection {
            {"SingerId", SpannerDbType.Int64},
            {"AlbumId", SpannerDbType.Int64},
            {"MarketingBudget", SpannerDbType.Int64},
        });
    var cmdLookup =
        connection.CreateSelectCommand("SELECT * FROM Albums");
    using (var reader = await cmdLookup.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            if (reader.GetFieldValue<int>("SingerId") == 1
                && reader.GetFieldValue<int>("AlbumId") == 1)
            {
                cmd.Parameters["SingerId"].Value =
                    reader.GetFieldValue<int>("SingerId");
                cmd.Parameters["AlbumId"].Value =
                    reader.GetFieldValue<int>("AlbumId");
                cmd.Parameters["MarketingBudget"].Value = 100000;
                await cmd.ExecuteNonQueryAsync();
            }
            if (reader.GetInt64(0) == 2 && reader.GetInt64(1) == 2)
            {
                cmd.Parameters["SingerId"].Value =
                    reader.GetFieldValue<int>("SingerId");
                cmd.Parameters["AlbumId"].Value =
                    reader.GetFieldValue<int>("AlbumId");
                cmd.Parameters["MarketingBudget"].Value = 500000;
                await cmd.ExecuteNonQueryAsync();
            }
        }
    }
}
Console.WriteLine("Updated data.");

writeDataToNewColumn 명령어를 사용하여 샘플을 실행합니다.

dotnet run writeDataToNewColumn $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같이 표시됩니다.

Updated data.

방금 쓴 값을 가져오기 위해 SQL 쿼리를 실행할 수도 있습니다.

다음은 쿼리를 실행하는 코드입니다.

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd =
        connection.CreateSelectCommand("SELECT * FROM Albums");
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            string budget = string.Empty;
            if (reader["MarketingBudget"] != DBNull.Value)
            {
                budget = reader.GetFieldValue<string>("MarketingBudget");
            }
            Console.WriteLine("SingerId : "
            + reader.GetFieldValue<string>("SingerId")
            + " AlbumId : "
            + reader.GetFieldValue<string>("AlbumId")
            + $" MarketingBudget : {budget}");
        }
    }
}

이 쿼리를 실행하려면 queryNewColumn 인수를 사용하여 샘플을 실행합니다.

dotnet run queryNewColumn $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같이 표시됩니다.

SingerId : 1 AlbumId : 1 MarketingBudget : 100000
SingerId : 1 AlbumId : 2 MarketingBudget :
SingerId : 2 AlbumId : 1 MarketingBudget :
SingerId : 2 AlbumId : 2 MarketingBudget : 500000
SingerId : 2 AlbumId : 3 MarketingBudget :

데이터 업데이트

읽기-쓰기 트랜잭션에서 DML을 사용하여 데이터를 업데이트할 수 있습니다.

ExecuteNonQueryAsync() 메서드를 사용하여 DML 문을 실행합니다.

public static async Task WriteWithTransactionUsingDmlCoreAsync(
    string projectId,
    string instanceId,
    string databaseId)
{
    // This sample transfers 200,000 from the MarketingBudget
    // field of the second Album to the first Album. Make sure to run
    // the addColumn and writeDataToNewColumn samples first,
    // in that order.
    string connectionString =
        $"Data Source=projects/{projectId}/instances/{instanceId}"
        + $"/databases/{databaseId}";

    decimal transferAmount = 200000;
    decimal secondBudget = 0;
    decimal firstBudget = 0;

    // Create connection to Cloud Spanner.
    using (var connection =
        new SpannerConnection(connectionString))
    {
        await connection.OpenAsync();

        // Create a readwrite transaction that we'll assign
        // to each SpannerCommand.
        using (var transaction =
                await connection.BeginTransactionAsync())
        {
            // Create statement to select the second album's data.
            var cmdLookup = connection.CreateSelectCommand(
             "SELECT * FROM Albums WHERE SingerId = 2 AND AlbumId = 2");
            cmdLookup.Transaction = transaction;
            // Excecute the select query.
            using (var reader = await cmdLookup.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    // Read the second album's budget.
                    secondBudget =
                       reader.GetFieldValue<decimal>("MarketingBudget");
                    // Confirm second Album's budget is sufficient and
                    // if not raise an exception. Raising an exception
                    // will automatically roll back the transaction.
                    if (secondBudget < transferAmount)
                    {
                        throw new Exception("The first album's "
                                + $"budget {secondBudget} "
                                + "is less than the "
                                + "amount to transfer.");
                    }
                }
            }
            // Read the first album's budget.
            cmdLookup = connection.CreateSelectCommand(
             "SELECT * FROM Albums WHERE SingerId = 1 and AlbumId = 1");
            cmdLookup.Transaction = transaction;
            using (var reader = await cmdLookup.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    firstBudget =
                      reader.GetFieldValue<decimal>("MarketingBudget");
                }
            }

            // Update second album to remove the transfer amount.
            secondBudget -= transferAmount;
            SpannerCommand cmd = connection.CreateDmlCommand(
                "UPDATE Albums SET MarketingBudget = @MarketingBudget "
                + "WHERE SingerId = 2 and AlbumId = 2");
            cmd.Parameters.Add("MarketingBudget", SpannerDbType.Int64, secondBudget);
            cmd.Transaction = transaction;
            await cmd.ExecuteNonQueryAsync();
            // Update first album to add the transfer amount.
            firstBudget += transferAmount;
            cmd = connection.CreateDmlCommand(
                "UPDATE Albums SET MarketingBudget = @MarketingBudget "
                + "WHERE SingerId = 1 and AlbumId = 1");
            cmd.Parameters.Add("MarketingBudget", SpannerDbType.Int64, firstBudget);
            cmd.Transaction = transaction;
            await cmd.ExecuteNonQueryAsync();

            await transaction.CommitAsync();
        }
        Console.WriteLine("Transaction complete.");
    }
}

writeWithTransactionUsingDml 인수를 사용하여 샘플을 실행합니다.

dotnet run writeWithTransactionUsingDml $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같이 표시됩니다.

Transaction complete.

읽기-쓰기 트랜잭션 재시도 수행

Cloud Spanner는 각 네트워크 호출 재시도를 수행할 수 있고, 네트워크 장애에 대한 복원력이 뛰어납니다. 그러나 부하가 높을 때 교착 상태가 발생할 수 있으며 이 경우 Cloud Spanner 트랜잭션이 '취소된' SpannerException을 발생시킵니다. 이 같은 예외를 처리하려면 아래와 같이 '재시도' 방식을 사용하여 전체 트랜잭션을 재시도해야 합니다.

먼저 트랜잭션을 재시도해야 할 때 호출할 메서드를 정의합니다. 다음 예시는 이름이 RetryRobot인 메서드를 정의합니다.

public class RetryRobot
{
    public TimeSpan FirstRetryDelay { get; set; } = TimeSpan.FromSeconds(1000);
    public float DelayMultiplier { get; set; } = 2;
    public int MaxTryCount { get; set; } = 7;
    public Func<Exception, bool> ShouldRetry { get; set; }

    /// <summary>
    /// Retry action when assertion fails.
    /// </summary>
    /// <param name="func"></param>
    public T Eventually<T>(Func<T> func)
    {
        TimeSpan delay = FirstRetryDelay;
        for (int i = 0; ; ++i)
        {
            try
            {
                return func();
            }
            catch (Exception e)
            when (ShouldCatch(e) && i < MaxTryCount)
            {
                Thread.Sleep(delay);
                delay *= (int)DelayMultiplier;
            }
        }
    }

    private bool ShouldCatch(Exception e)
    {
        return ShouldRetry != null && ShouldRetry(e);
    }
}

그 다음 RetryRobot 메서드의 retryRobot이라는 인스턴스를 만들고 재시도해야 하는 오류 조건으로 IsTransientSpannerFault()를 지정합니다. 그런 다음 retryRobot.Eventually을 사용하여 전체 트랜잭션을 실행합니다.

다음은 재시도를 수행하는 코드입니다.

var retryRobot = new RetryRobot
{
    MaxTryCount = 3,
    DelayMultiplier = 2,
    ShouldRetry = (e) => e.IsTransientSpannerFault()
};

await retryRobot.Eventually(() =>
    ReadWriteWithTransactionAsync(
        projectId, instanceId, databaseId));

보조 색인 사용

Albums에서 특정 범위의 AlbumTitle 값이 있는 모든 행을 가져오려고 한다고 가정합니다. SQL 문 또는 읽기 호출을 사용하여 AlbumTitle 열에서 모든 값을 읽은 다음 기준을 충족하지 않는 행을 삭제할 수 있지만 이렇게 전체 테이블 스캔을 수행하는 것은 비용이 많이 들며, 특히 많은 행이 있는 테이블의 경우에는 더욱 그렇습니다. 대신 테이블에 보조 색인을 만들어 기본 키가 아닌 열로 검색하면 행을 빠르게 검색할 수 있습니다.

기존 테이블에 보조 색인을 추가하려면 스키마를 업데이트해야 합니다. 다른 스키마 업데이트와 같이 Cloud Spanner는 데이터베이스에서 트래픽이 계속 처리되는 동안 색인을 추가할 수 있습니다. Cloud Spanner는 기존 데이터로 색인을 자동 백필합니다. 백필을 완료하는 데 몇 분 정도 걸릴 수 있지만 이 프로세스가 진행되는 동안 데이터베이스를 오프라인으로 전환하거나 색인이 생성되는 테이블에 대한 쓰기를 금지할 필요는 없습니다. 자세한 내용은 색인 백필을 참조하세요.

보조 색인을 추가하고 나면 Cloud Spanner는 보조 색인 사용 시 더 빨리 실행될 가능성이 높은 SQL 쿼리에 자동으로 보조 색인을 사용합니다. 읽기 인터페이스를 사용하는 경우에는 사용할 색인을 지정해야 합니다.

보조 색인 추가

명령줄에서 gcloud 명령줄 도구를 사용하거나 C#용 Cloud Spanner 클라이언트 라이브러리를 프로그래매틱 방식으로 사용하여 색인을 추가할 수 있습니다.

명령줄에서

다음과 같은 CREATE INDEX 명령어를 사용하여 데이터베이스에 색인을 추가합니다.

gcloud spanner databases ddl update example-db --instance=test-instance `
    --ddl='CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)'

다음과 같이 표시됩니다.

Schema updating...done.

C#용 Cloud Spanner 클라이언트 라이브러리 사용

CreateDdlCommand()를 사용하여 색인을 추가합니다.

// Initialize request argument(s).
string connectionString =
    $"Data Source=projects/{projectId}/instances/"
    + $"{instanceId}/databases/{databaseId}";
string createStatement =
    "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)";
// Make the request.
using (var connection = new SpannerConnection(connectionString))
{
    var createCmd = connection.CreateDdlCommand(createStatement);
    await createCmd.ExecuteNonQueryAsync();
}
Console.WriteLine("Added the AlbumsByAlbumTitle index.");

addIndex 명령어를 사용하여 샘플을 실행합니다.

  dotnet run addIndex $env:GOOGLE_PROJECT_ID test-instance example-db

색인 추가는 몇 분 정도 걸릴 수 있습니다. 색인이 추가되면 다음과 같이 표시됩니다.

  Added the AlbumsByAlbumTitle index.

STORING 절을 사용하여 색인 추가

위의 읽기 예시에서는 MarketingBudget 열 읽기가 포함되지 않았습니다. 이는 Cloud Spanner의 읽기 인터페이스가 색인에 저장되지 않은 값을 찾기 위해 색인을 데이터 테이블에 조인하는 기능을 지원하지 않기 때문입니다.

색인에 MarketingBudget 사본을 저장하는 AlbumsByAlbumTitle 대체 정의를 만듭니다.

명령줄에서

gcloud spanner databases ddl update example-db --instance=test-instance `
    --ddl='CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)'

색인 추가는 몇 분 정도 걸릴 수 있습니다. 색인이 추가되면 다음과 같이 표시됩니다.

Schema updating...done.

C#용 Cloud Spanner 클라이언트 라이브러리 사용

CreateDdlCommand()를 사용하여 STORING 절을 포함하는 색인을 추가합니다.

// Initialize request argument(s).
string connectionString =
    $"Data Source=projects/{projectId}/instances/"
    + $"{instanceId}/databases/{databaseId}";
string createStatement =
    "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) "
    + "STORING (MarketingBudget)";
// Make the request.
using (var connection = new SpannerConnection(connectionString))
{
    var createCmd = connection.CreateDdlCommand(createStatement);
    await createCmd.ExecuteNonQueryAsync();
}
Console.WriteLine("Added the AlbumsByAlbumTitle2 index.");

addStoringIndex 명령어를 사용하여 샘플을 실행합니다.

dotnet run addStoringIndex $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 같이 표시됩니다.

Added the AlbumsByAlbumTitle2 index.

이제 AlbumsByAlbumTitle2 색인의 AlbumId, AlbumTitle, MarketingBudget 열을 모두 가져오는 읽기를 실행할 수 있습니다.

색인을 명시적으로 지정하는 쿼리를 실행하여 만든 저장 색인을 사용해 데이터를 읽습니다.

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateSelectCommand(
        "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
        + "{FORCE_INDEX=AlbumsByAlbumTitle2} "
        + $"WHERE AlbumTitle >= @startTitle "
        + $"AND AlbumTitle < @endTitle",
        new SpannerParameterCollection {
            {"startTitle", SpannerDbType.String},
            {"endTitle", SpannerDbType.String} });
    cmd.Parameters["startTitle"].Value = startTitle;
    cmd.Parameters["endTitle"].Value = endTitle;
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine("AlbumId : "
            + reader.GetFieldValue<string>("AlbumId")
            + " AlbumTitle : "
            + reader.GetFieldValue<string>("AlbumTitle")
            + " MarketingBudget : "
            + reader.GetFieldValue<string>("MarketingBudget"));
        }
    }
}

queryDataWithStoringIndex 명령어를 사용하여 샘플을 실행합니다.

dotnet run queryDataWithStoringIndex $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 비슷한 출력이 표시됩니다.

AlbumId : 2 AlbumTitle : Forever Hold your Peace MarketingBudget : 300000
AlbumId : 2 AlbumTitle : Go, Go, Go MarketingBudget : 300000

읽기 전용 트랜잭션을 사용하여 데이터 검색

같은 타임스탬프에서 읽기를 하나 이상 실행한다고 가정해 봅시다. 읽기 전용 트랜잭션은 트랜잭션 커밋 기록의 일관된 프리픽스를 관찰하므로 애플리케이션이 항상 일관된 데이터를 가져옵니다. 읽기 전용 트랜잭션을 실행하려면 .NET Framework의 TransactionScope()OpenAsReadOnlyAsync()를 함께 사용합니다.

다음은 같은 읽기 전용 트랜잭션에서 쿼리를 실행하고 읽기를 수행하는 방법을 보여줍니다.

.NET Standard 2.0

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Gets a transaction object that captures the database state
// at a specific point in time.
using (TransactionScope scope = new TransactionScope(
    TransactionScopeAsyncFlowOption.Enabled))
{
    // Create connection to Cloud Spanner.
    using (var connection = new SpannerConnection(connectionString))
    {
        // Open the connection, making the implicitly created
        // transaction read only when it connects to the outer
        // transaction scope.
        await connection.OpenAsReadOnlyAsync()
            .ConfigureAwait(false);
        var cmd = connection.CreateSelectCommand(
            "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
        // Read #1.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : "
                    + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : "
                    + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : "
                    + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }
        // Read #2. Even if changes occur in-between the reads,
        // the transaction ensures that Read #1 and Read #2
        // return the same data.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : "
                    + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : "
                    + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : "
                    + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }
    }
    scope.Complete();
    Console.WriteLine("Transaction complete.");
}

.NET Standard 1.5

string connectionString =
    $"Data Source=projects/{projectId}/instances/{instanceId}"
    + $"/databases/{databaseId}";

// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    await connection.OpenAsync();

    // Open a new read only transaction.
    using (var transaction =
        await connection.BeginReadOnlyTransactionAsync())
    {
        var cmd = connection.CreateSelectCommand(
            "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
        cmd.Transaction = transaction;

        // Read #1.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : "
                    + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : "
                    + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : "
                    + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }
        // Read #2. Even if changes occur in-between the reads,
        // the transaction ensures that Read #1 and Read #2
        // return the same data.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : "
                    + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : "
                    + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : "
                    + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }
    }
}
Console.WriteLine("Transaction complete.");

queryDataWithTransaction 명령어를 사용하여 샘플을 실행합니다.

dotnet run queryDataWithTransaction $env:GOOGLE_PROJECT_ID test-instance example-db

다음과 비슷한 출력이 표시됩니다.

SingerId : 2 AlbumId : 2 AlbumTitle : Forever Hold your Peace
SingerId : 1 AlbumId : 2 AlbumTitle : Go, Go, Go
SingerId : 2 AlbumId : 1 AlbumTitle : Green
SingerId : 2 AlbumId : 3 AlbumTitle : Terrified
SingerId : 1 AlbumId : 1 AlbumTitle : Total Junk
SingerId : 2 AlbumId : 2 AlbumTitle : Forever Hold your Peace
SingerId : 1 AlbumId : 2 AlbumTitle : Go, Go, Go
SingerId : 2 AlbumId : 1 AlbumTitle : Green
SingerId : 2 AlbumId : 3 AlbumTitle : Terrified
SingerId : 1 AlbumId : 1 AlbumTitle : Total Junk

정리

이 가이드에서 사용한 리소스에 대한 추가 비용이 Google Cloud 계정에 청구되지 않도록 하려면 데이터베이스와 새로 만든 인스턴스를 삭제합니다.

데이터베이스 삭제

인스턴스를 삭제하면 인스턴스 내의 모든 데이터베이스가 자동으로 삭제됩니다. 다음 단계는 인스턴스를 삭제하지 않고 데이터베이스를 삭제하는 방법을 보여줍니다. 인스턴스에 대한 비용은 여전히 발생합니다.

명령줄에서

gcloud spanner databases delete example-db --instance=test-instance

Cloud Console 사용

  1. Google Cloud Console에서 Cloud Spanner 인스턴스 페이지로 이동합니다.

    인스턴스 페이지로 이동

  2. 인스턴스를 클릭합니다.

  3. 삭제할 데이터베이스를 클릭합니다.

  4. 데이터베이스 세부정보 페이지에서 삭제를 클릭합니다.

  5. 데이터베이스 삭제 여부를 확인하고 삭제를 클릭합니다.

인스턴스 삭제

인스턴스를 삭제하면 해당 인스턴스에서 만든 모든 데이터베이스가 자동으로 삭제됩니다.

명령줄에서

gcloud spanner instances delete test-instance

Cloud Console 사용

  1. Google Cloud Console에서 Cloud Spanner 인스턴스 페이지로 이동합니다.

    인스턴스 페이지로 이동

  2. 인스턴스를 클릭합니다.

  3. 삭제를 클릭합니다.

  4. 인스턴스 삭제 여부를 확인하고 삭제를 클릭합니다.

다음 단계

  • 가상 머신 인스턴스에서 Cloud Spanner 액세스: Cloud Spanner 데이터베이스에 액세스할 수 있는 가상 머신 인스턴스를 만듭니다.
  • 인증 시작하기에서 승인 및 사용자 인증 정보를 알아봅니다.
  • Cloud Spanner 개념을 자세히 알아봅니다.