C# で Cloud Spanner を使ってみる

目標

このチュートリアルでは、C# 用の Cloud Spanner クライアント ライブラリを使用する以下のステップについて説明します。

  • Cloud Spanner のインスタンスとデータベースを作成します。
  • データベースのデータに対し、書き込み、読み取り、SQL クエリの実行を行います。
  • データベース スキーマを更新します。
  • 読み取り / 書き込みトランザクションを使用してデータを更新します。
  • セカンダリ インデックスをデータベースに追加します。
  • インデックスを使用して、データの読み込みと SQL クエリの実行を行います。
  • 読み取り専用トランザクションを使用してデータを取得します。

料金

このチュートリアルで使用する Cloud Spanner は、Google Cloud Platform の有料コンポーネントです。Cloud Spanner を使用する料金については、料金をご覧ください。

始める前に

  1. 設定に示されている手順を完了します。その手順では、Google Cloud Platform のデフォルト プロジェクトの作成と設定、課金の有効化、Cloud Spanner API の有効化、Cloud Spanner API を使用するのに必要な認証情報を取得するために OAuth 2.0 を設定する方法について説明しています。
    特に、ローカル開発環境で認証情報を設定するために、必ず gcloud auth application-default login を実行してください。

  2. GOOGLE_PROJECT_ID 環境変数を Google Cloud Platform プロジェクト 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")
  3. 認証情報をダウンロードします。

    1. Google Cloud Platform Console で [API とサービス] ページにアクセスし、[認証情報] をクリックします。
      [API とサービス] ページに移動
    2. [認証情報を作成] をクリックし、[サービス アカウント キー] を選択します。
    3. [サービス アカウント] で [Compute Engine default service account] を選択し、[キーのタイプ] では [JSON] を選択したままにします。[作成] をクリックします。JSON ファイルがダウンロードされます。
  4. 認証情報を設定します。C ドライブ上の CURRENT_USER のダウンロード ディレクトリにある 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")
  5. ローカルマシンにサンプルアプリのリポジトリのクローンを作成します。

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

    あるいは、zip 形式のサンプルをダウンロードして、ファイルを抽出することもできます。

  6. ダウンロードしたリポジトリの dotnet-docs-samples\spanner\api ディレクトリ内にある Spanner.sln を Visual Studio 2017 以降で開き、ビルドします。

  7. コンパイル済みアプリケーションが含まれている、ダウンロードされたリポジトリ内のディレクトリに移動します。次に例を示します。

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

インスタンスの作成

Cloud Spanner を最初に使用するときは、インスタンスを作成する必要があります。インスタンスとは、Cloud Spanner データベースによって使用されるリソースの割り当てのことです。インスタンスを作成するときは、インスタンス構成を選択してデータの格納場所を指定し、さらに使用するノード数も選択して、インスタンスの配信リソースおよびストレージ リソースの量を決定します。

次のコマンドを実行して、1 ノードの us-central1 リージョンに 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 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);
    try
    {
        await cmd.ExecuteNonQueryAsync();
    }
    catch (SpannerException e) when (e.ErrorCode == ErrorCode.AlreadyExists)
    {
        // OK.
    }
}
// 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();
}

コードでは、基本的な音楽アプリケーション用の 2 つのテーブル 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)を使用してデータを挿入できます。

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 クライアント ライブラリを使用して行います。

コマンドラインから

Albums テーブルのすべての列から値を読み取るには、次の SQL ステートメントを実行します。

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 ステートメントを発行することもできます。

SQL クエリを実行するには、ExecuteReaderAsync() を使用します。

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 ステートメントにカスタム値を追加できます。ここでは、WHERE 句でパラメータとして @lastName を使用し、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

データベース スキーマの更新

MarketingBudget という列を新たに Albums テーブルに追加する必要があるとします。既存のテーブルに新しい列を追加するには、データベース スキーマの更新が必要です。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.

新しい列へのデータの書き込み

次のコードは、新しい列にデータを書き込みます。MarketingBudget の値を、キーが Albums(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 をスローします。この例外を処理するには、次のように retry を使用して、トランザクション全体を再試行する必要があります。

まず、トランザクションの再試行が必要なときに呼び出されるメソッドを定義します。次の例では、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 は、内部でインデックスにデータを書き込みます(「バックフィル」)。バックフィルには数分かかることがありますが、このプロセスの間に、データベースをオフラインにしたり、特定のテーブルや列への書き込みを避けたりする必要はありません。詳細については、インデックスのバックフィリングをご覧ください。

セカンダリ インデックスの追加

インデックスを追加するには、コマンドラインで 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.

インデックスを使用したクエリ

新しいインデックスを使用してクエリを実行するには、コマンドラインまたはクライアント ライブラリを使用します。

コマンドラインから

gcloud コマンドライン ツールで SQL ステートメントを実行し、["Aardvark", "Goo")AlbumsTitle の範囲に対して AlbumsByAlbumTitle インデックスを使用して Albums から AlbumIdAlbumTitleMarketingBudget を取得します。

gcloud spanner databases execute-sql example-db --instance=test-instance --sql='SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} WHERE AlbumTitle >= "Aardvark" AND AlbumTitle < "Goo"'

結果は次のようになります。

AlbumId  AlbumTitle               MarketingBudget
2        Go, Go, Go
2        Forever Hold Your Peace  300000

C# 用の Cloud Spanner クライアント ライブラリの使用

プログラムでインデックスを使用するコードは、前に使用したクエリのコードと似ています。

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=AlbumsByAlbumTitle} "
        + $"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"));
        }
    }
}

queryDataWithIndex コマンドを使用してサンプルを実行します。

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

次のような出力が表示されます。

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

詳細については、以下のリファレンスをご覧ください。

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 から AlbumIdAlbumTitleMarketingBudget 列をすべて取得する読み取りを実行できました。

作成した Storing インデックスを使用してデータを読み取るには、インデックスを明示的に指定するクエリを実行します。

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 : 1 AlbumTitle : Go, Go, Go MarketingBudget : 300000

読み取り専用トランザクションを使用したデータの取得

同じタイムスタンプで複数の読み取りを実行する場合について考えます。読み取り専用トランザクションは、トランザクションの commit 履歴で整合性のあるプレフィックスを監視しているため、アプリケーションは常に整合性のあるデータを取得できます。 読み取り専用トランザクションを実行するには、.NET フレームワークの 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 : 1 AlbumTitle : Go, Go, Go
SingerId : 2 AlbumId : 1 AlbumTitle : Green
SingerId : 2 AlbumId : 3 AlbumTitle : Terrified
SingerId : 1 AlbumId : 2 AlbumTitle : Total Junk
SingerId : 2 AlbumId : 2 AlbumTitle : Forever Hold your Peace
SingerId : 1 AlbumId : 1 AlbumTitle : Go, Go, Go
SingerId : 2 AlbumId : 1 AlbumTitle : Green
SingerId : 2 AlbumId : 3 AlbumTitle : Terrified
SingerId : 1 AlbumId : 2 AlbumTitle : Total Junk

クリーンアップ

このチュートリアルで使用したリソースの Google Cloud Platform アカウントが課金されないようにするため、作成したデータベースとインスタンスを削除します。

データベースの削除

インスタンスを削除すると、それに含まれるすべてのデータベースが自動的に削除されます。このステップでは、インスタンスを削除しないでデータベースを削除する方法を示します(インスタンスの料金は引き続き発生します)。

コマンドラインから

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

GCP Console の使用

  1. Google Cloud Platform Console の Cloud Spanner インスタンス ページに移動します。
    Cloud Spanner インスタンス ページに移動
  2. インスタンスをクリックします。
  3. 削除するデータベースをクリックします。
  4. [データベースの詳細] ページで [削除] をクリックします。
  5. データベースを削除することを確認し、[削除] をクリックします。

インスタンスの削除

インスタンスを削除すると、そのインスタンスで作成されたすべてのデータベースが自動的に削除されます。

コマンドラインから

gcloud spanner instances delete test-instance

GCP Console の使用

  1. Google Cloud Platform Console の Cloud Spanner インスタンス ページに移動します。
    Cloud Spanner インスタンス ページに移動
  2. インスタンスをクリックします。
  3. [削除] をクリックします。
  4. インスタンスを削除することを確認し、[削除] をクリックします。

次のステップ

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Cloud Spanner のドキュメント