C# で Spanner を使ってみる


目標

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

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

費用

このチュートリアルで使用する Spanner は、Google 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 のダウンロード ディレクトリにある 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. ダウンロードしたリポジトリの dotnet-docs-samples\spanner\api ディレクトリ内にある Spanner.sln を Visual Studio 2017 以降で開き、ビルドします。

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

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

インスタンスを作成する

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

次のコマンドを実行して、1 ノードの us-central1 リージョンに 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# で Spanner を使用する方法を示すサンプルが含まれています。

Spanner .NET GitHub リポジトリを確認してください。データベースの作成方法とデータベース スキーマの変更方法が示されています。データで使用されているスキーマ例の詳細は、スキーマとデータモデルのページをご覧ください。

データベースの作成

コマンドラインで次のコマンドを実行して、test-instance というインスタンスに example-db というデータベースを作成します。

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

次のように表示されます。

Created sample database example-db on instance test-instance
次のコードでは、データベースとデータベース内の 2 つのテーブルを作成します。

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

public class CreateDatabaseAsyncSample
{
    public async Task CreateDatabaseAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}";

        using var connection = new SpannerConnection(connectionString);
        var createDatabase = $"CREATE DATABASE `{databaseId}`";
        // Define create table statement for table #1.
        var createSingersTable =
        @"CREATE TABLE Singers (
                     SingerId INT64 NOT NULL,
                     FirstName STRING(1024),
                     LastName STRING(1024),
                     ComposerInfo BYTES(MAX),
                     FullName STRING(2048) AS (ARRAY_TO_STRING([FirstName, LastName], "" "")) STORED
                 ) PRIMARY KEY (SingerId)";
        // Define create table statement for table #2.
        var createAlbumsTable =
        @"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";

        using var createDbCommand = connection.CreateDdlCommand(createDatabase, createSingersTable, createAlbumsTable);
        await createDbCommand.ExecuteNonQueryAsync();
    }
}

次のステップでは、データベースにデータを書き込みます。

データベース クライアントの作成

読み取りまたは書き込みを行うには、その前に 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 はデータベース接続と考えることができます。Spanner とのすべてのやり取りは Spanner​Connection を経由する必要があります。

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

DML でのデータの書き込み

読み取り / 書き込みトランザクションでデータ操作言語(DML)を使用してデータを挿入できます。

ExecuteNonQueryAsync() メソッドを使用して DML ステートメントを実行します。


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

public class WriteUsingDmlCoreAsyncSample
{
    public async Task<int> WriteUsingDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        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...");
        return rowCount;
    }
}

writeUsingDml 引数を使用してサンプルを実行します。

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

次のように表示されます。

4 row(s) inserted...

ミューテーションを使用してデータを書き込む

ミューテーションを使ってデータを挿入することもできます。

データを挿入するには、connection.CreateInsertCommand() メソッドを使用します。このメソッドでは、行をテーブルに挿入するための新しい SpannerCommand を作成します。SpannerCommand.ExecuteNonQueryAsync() メソッドを使用すると、テーブルに新しい行を追加できます。

次のコードは、ミューテーションを使用してデータを挿入する方法を示しています。


using Google.Cloud.Spanner.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class InsertDataAsyncSample
{
    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 async Task InsertDataAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        List<Singer> singers = new List<Singer>
        {
            new Singer { SingerId = 1, FirstName = "Marc", LastName = "Richards" },
            new Singer { SingerId = 2, 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 = 1, AlbumId = 1, AlbumTitle = "Total Junk" },
            new Album { SingerId = 1, AlbumId = 2, AlbumTitle = "Go, Go, Go" },
            new Album { SingerId = 2, AlbumId = 1, AlbumTitle = "Green" },
            new Album { SingerId = 2, AlbumId = 2, AlbumTitle = "Forever Hold your Peace" },
            new Album { SingerId = 2, AlbumId = 3, AlbumTitle = "Terrified" },
        };

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

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

            await Task.WhenAll(albums.Select(album =>
            {
                // Insert rows into the Albums table.
                using var cmd = connection.CreateInsertCommand("Albums", new SpannerParameterCollection
                {
                        { "SingerId", SpannerDbType.Int64, album.SingerId },
                        { "AlbumId", SpannerDbType.Int64, album.AlbumId },
                        { "AlbumTitle", SpannerDbType.String,album.AlbumTitle }
                });
                cmd.Transaction = transaction;
                return cmd.ExecuteNonQueryAsync();
            }));
        });
        Console.WriteLine("Data inserted.");
    }
}

insertSampleData 引数を使用してサンプルを実行します。

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

次のように表示されます。

Inserted data.

SQL を使用したデータのクエリ

Spanner では、データの読み取り用に SQL インターフェースがサポートされています。このインターフェースにアクセスするには、コマンドラインで Google Cloud CLI を使用するか、プログラムで C# 用の 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# 用の Spanner クライアント ライブラリを使用する

コマンドラインで SQL ステートメントを実行するだけでなく、プログラムから C# 用 Spanner クライアント ライブラリを使用して同じ SQL ステートメントを発行することもできます。

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


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

public class QuerySampleDataAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
    }

    public async Task<List<Album>> QuerySampleDataAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        var albums = new List<Album>();
        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand("SELECT SingerId, AlbumId, AlbumTitle FROM Albums");

        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                SingerId = reader.GetFieldValue<int>("SingerId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle")
            });
        }
        return albums;
    }
}

クエリを発行してデータにアクセスする方法を次に示します。

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 パラメータを使用したクエリ

アプリケーションに頻繁に実行されるクエリがある場合は、対象のクエリをパラメータ化してパフォーマンスを改善できます。パラメータ クエリをキャッシュに保存して再利用できます。これにより、コンパイルのコストを削減できます。詳細については、クエリ パラメータを使用して、頻繁に実行するクエリを高速化するをご覧ください。

ここでは、WHERE 句のパラメータを使用して、LastName の特定の値を含むレコードをクエリします。


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

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

    public async Task<List<Singer>> QueryWithParameterAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand(
            $"SELECT SingerId, FirstName, LastName FROM Singers WHERE LastName = @lastName",
            new SpannerParameterCollection { { "lastName", SpannerDbType.String, "Garcia" } });

        var singers = new List<Singer>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            singers.Add(new Singer
            {
                SingerId = reader.GetFieldValue<int>("SingerId"),
                FirstName = reader.GetFieldValue<string>("FirstName"),
                LastName = reader.GetFieldValue<string>("LastName")
            });
        }
        return singers;
    }
}

パラメータ付きのクエリでデータにアクセスするには、次のコマンドを実行します。

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

次のような結果が表示されます。

SingerId : 12 FirstName : Melissa LastName : Garcia

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

MarketingBudget という列を新たに Albums テーブルに追加する必要があるとします。既存のテーブルに新しい列を追加するには、データベース スキーマの更新が必要です。Spanner は、データベースがトラフィックの処理を継続している間にデータベースのスキーマを更新することをサポートしています。スキーマの更新では、データベースをオフラインにする必要がなく、テーブル全体または列全体をロックすることもありません。スキーマの更新中もデータベースへのデータの書き込みを続けることができます。サポートされるスキーマの更新とスキーマ変更のパフォーマンスの詳細については、スキーマの更新をご覧ください。

列の追加

列を追加するには、コマンドラインで Google Cloud CLI を使用するか、プログラムから C# 用 Spanner クライアント ライブラリを使用します。

コマンドラインから

テーブルに新しい列を追加するには、次の ALTER TABLE コマンドを使用します。

GoogleSQL

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

PostgreSQL

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

次のように表示されます。

Schema updating...done.

C# 用の Spanner クライアント ライブラリを使用する

スキーマを変更するには、CreateDdlCommand() を使用します。


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

public class AddColumnAsyncSample
{
    public async Task AddColumnAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        string alterStatement = "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64";

        using var connection = new SpannerConnection(connectionString);
        using 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 に設定します。


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

public class UpdateDataAsyncSample
{
    public async Task<int> UpdateDataAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);

        var rowCount = 0;
        SpannerCommand cmd = connection.CreateDmlCommand(
            "UPDATE Albums SET MarketingBudget = @MarketingBudget "
            + "WHERE SingerId = 1 and AlbumId = 1");
        cmd.Parameters.Add("MarketingBudget", SpannerDbType.Int64, 100000);
        rowCount += await cmd.ExecuteNonQueryAsync();

        cmd = connection.CreateDmlCommand(
            "UPDATE Albums SET MarketingBudget = @MarketingBudget "
            + "WHERE SingerId = 2 and AlbumId = 2");
        cmd.Parameters.Add("MarketingBudget", SpannerDbType.Int64, 500000);
        rowCount += await cmd.ExecuteNonQueryAsync();

        Console.WriteLine("Data Updated.");
        return rowCount;
    }
}

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

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

以下のように表示されます。

Updated data.

さらに、SQL クエリを実行して、書き込んだばかりの値を取得することもできます。

クエリを実行するコードを次に示します。


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

public class QueryNewColumnAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public long MarketingBudget { get; set; }
    }

    public async Task<List<Album>> QueryNewColumnAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        var albums = new List<Album>();
        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand("SELECT * FROM Albums");
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                SingerId = reader.GetFieldValue<int>("SingerId"),
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                MarketingBudget = reader.IsDBNull(reader.GetOrdinal("MarketingBudget")) ? 0 : reader.GetFieldValue<long>("MarketingBudget")
            });
        }
        return albums;
    }
}

このクエリを実行するには、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 ステートメントを実行します。


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

public class WriteWithTransactionUsingDmlCoreAsyncSample
{
    public async Task<int> 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 AddColumnAsyncSample and WriteDataToNewColumnAsyncSample first,
        // in that order.
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        decimal transferAmount = 200000;
        decimal secondBudget = 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;
        // Execute the select query.
        using var reader1 = await cmdLookup.ExecuteReaderAsync();
        while (await reader1.ReadAsync())
        {
            // Read the second album's budget.
            secondBudget = reader1.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 second album's budget {secondBudget} is less than the amount to transfer.");
            }
        }

        // 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;
        var rowCount = await cmd.ExecuteNonQueryAsync();

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

        await transaction.CommitAsync();

        Console.WriteLine("Transaction complete.");
        return rowCount;
    }
}

writeWithTransactionUsingDml 引数を使用してサンプルを実行します。

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

次のように表示されます。

Transaction complete.

セカンダリ インデックスの使用

Albums から AlbumTitle の値が特定の範囲内にある行すべてを取得すると仮定します。SQL ステートメントまたは読み取り呼び出しを使用して AlbumTitle 列からすべての値を読み取り、基準を満たしていない行を破棄することもできますが、このようなテーブル全体のスキャンは割高です(特に、行数が多いテーブルの場合)。代わりに、テーブルにセカンダリ インデックスを作成することにより、主キー以外の列を検索するときの行の取得速度を上げることができます。

既存のテーブルにセカンダリ インデックスを追加するには、スキーマの更新が必要です。他のスキーマの更新と同様に、Spanner ではデータベースがトラフィックの処理を継続している間にインデックスを追加できます。Spanner では、インデックスに既存のデータが自動的にバックフィルされます。バックフィルには数分かかることがありますが、このプロセスの間に、データベースをオフラインにしたり、インデックス化対象のテーブルへの書き込みを控えたりする必要はありません。詳しくは、セカンダリ インデックスを追加するをご覧ください。

セカンダリ インデックスを追加すると、インデックス効果で実行速度が上がりそうな SQL クエリに対して Spanner ではそのセカンダリ インデックスが自動的に使用されるようになります。読み取りインターフェースを使用する場合は、使用するインデックスを指定する必要があります。

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

コマンドラインでインデックスを追加するには、gcloud CLI を使用するか、プログラムから C# 用 Spanner クライアント ライブラリを使用します。

コマンドラインから

データベースにインデックスを追加するには、次の CREATE INDEX コマンドを使用します。

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

以下のように表示されます。

Schema updating...done.

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

インデックスを追加するには、CreateDdlCommand() を使用します。


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

public class AddIndexAsyncSample
{
    public async Task AddIndexAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        string createStatement = "CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)";

        using var connection = new SpannerConnection(connectionString);
        using 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.

インデックス限定読み取り用のインデックスを追加する

上記の読み取り例には、MarketingBudget 列の読み取りが含まれていませんでした。これは、Spanner の読み取りインターフェースが、インデックスとデータテーブルを結合してインデックスに格納されていない値を検索する機能をサポートしていないためです。

MarketingBudget のコピーをインデックスに格納する AlbumsByAlbumTitle の代替定義を作成します。

コマンドラインから

GoogleSQL

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

PostgreSQL

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

インデックスの追加には数分かかる場合があります。インデックスが追加されると、次のように表示されます。

Schema updating...done.

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

CreateDdlCommand() を使用し、STORING 句を指定してインデックスを追加します。


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

public class AddStoringIndexAsyncSample
{
    public async Task AddStoringIndexAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        string createStatement = "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)";

        using var connection = new SpannerConnection(connectionString);
        using 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 インデックスを使用してデータを読み取るには、インデックスを明示的に指定するクエリを実行します。


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

public class QueryDataWithStoringIndexAsyncSample
{
    public class Album
    {
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
        public long? MarketingBudget { get; set; }
    }

    public async Task<List<Album>> QueryDataWithStoringIndexAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        var cmd = connection.CreateSelectCommand(
            "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
            + "{FORCE_INDEX=AlbumsByAlbumTitle2}");

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle"),
                MarketingBudget = reader.IsDBNull(reader.GetOrdinal("MarketingBudget")) ? 0 : reader.GetFieldValue<long>("MarketingBudget")
            });
        }
        return albums;
    }
}

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

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

同じタイムスタンプで複数の読み取りを実行する場合について考えます。読み取り専用トランザクションは、トランザクションの commit 履歴で整合性のあるプレフィックスを監視しているため、アプリケーションは常に整合性のあるデータを取得できます。読み取り専用トランザクションを実行するには、.NET Framework の TransactionScope()OpenAsReadOnlyAsync() を一緒に使用します。

同じ読み取り専用トランザクションでクエリと読み取りを実行する方法を次に示します。

.NET Standard 2.0


using Google.Cloud.Spanner.Data;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Transactions;

public class QueryDataWithTransactionAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
    }

    public async Task<List<Album>> QueryDataWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        var albums = new List<Album>();
        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        using var connection = new SpannerConnection(connectionString);

        // Opens the connection so that the Spanner transaction included in the TransactionScope
        // is read-only TimestampBound.Strong.
        await connection.OpenAsync(SpannerTransactionCreationOptions.ReadOnly, options: null, cancellationToken: default);
        using 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())
            {
                albums.Add(new Album
                {
                    AlbumId = reader.GetFieldValue<int>("AlbumId"),
                    SingerId = reader.GetFieldValue<int>("SingerId"),
                    AlbumTitle = reader.GetFieldValue<string>("AlbumTitle")
                });
            }
        }
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return albums;
    }
}

.NET Standard 1.5


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

public class QueryDataWithTransactionCoreAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
    }

    public async Task<List<Album>> QueryDataWithTransactionCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        var albums = new List<Album>();

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        // Open a new read only transaction.
        using var transaction = await connection.BeginTransactionAsync(SpannerTransactionCreationOptions.ReadOnly, cancellationToken: default);
        using 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())
            {
                albums.Add(new Album
                {
                    AlbumId = reader.GetFieldValue<int>("AlbumId"),
                    SingerId = reader.GetFieldValue<int>("SingerId"),
                    AlbumTitle = reader.GetFieldValue<string>("AlbumTitle")
                });
            }
        }

        Console.WriteLine("Transaction complete.");
        return albums;
    }
}

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

クリーンアップ

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

データベースの削除

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

コマンドラインから

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

Google Cloud コンソールの使用

  1. Google Cloud コンソールで、[Spanner インスタンス] ページに移動します。

    インスタンス ページに移動

  2. インスタンスをクリックします。

  3. 削除するデータベースをクリックします。

  4. [データベースの詳細] ページで [削除] をクリックします。

  5. データベースを削除することを確認し、[削除] をクリックします。

インスタンスを削除する

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

コマンドラインから

gcloud spanner instances delete test-instance

Google Cloud コンソールの使用

  1. Google Cloud コンソールで、[Spanner インスタンス] ページに移動します。

    インスタンス ページに移動

  2. インスタンスをクリックします。

  3. [削除] をクリックします。

  4. インスタンスを削除することを確認し、[削除] をクリックします。

次のステップ