Spanner 使用入门 (ADO.NET)

目标

本教程将介绍如何使用 Spanner ADO.NET 驱动程序完成以下步骤:

  • 创建 Spanner 实例和数据库。
  • 写入、读取数据库中的数据和对数据执行 SQL 查询。
  • 更新数据库架构。
  • 使用读写事务更新数据。
  • 向数据库添加二级索引。
  • 使用索引来读取数据和对数据执行 SQL 查询。
  • 使用只读事务检索数据。

费用

本教程使用 Spanner,它是Google Cloud的收费组件。如需了解使用 Spanner 的费用,请参阅价格

准备工作

完成设置中介绍的步骤,包括创建和设置默认 Google Cloud 项目、启用结算功能、启用 Cloud Spanner API 以及设置 OAuth 2.0 来获取身份验证凭据以使用 Cloud Spanner API。

尤其要确保运行 gcloud auth application-default login,以便使用身份验证凭据设置本地开发环境。

准备本地 ADO.NET 环境

  1. 在开发机器上下载并安装 .NET(如果尚未安装)。

  2. 将示例代码库克隆到您的本地机器:

    git clone https://github.com/googleapis/dotnet-spanner-entity-framework.git
    
  3. 切换到包含 Spanner ADO.NET 驱动程序示例代码的目录:

    cd dotnet-spanner-entity-framework/spanner-ado-net/spanner-ado-net-getting-started-guide
    

创建实例

在首次使用 Spanner 时,必须创建一个实例,实例是 Spanner 数据库使用的资源分配单位。创建实例时,请选择一个实例配置(决定数据的存储位置),同时选择要使用的节点数(决定实例中服务资源和存储资源的数量)。

如需了解如何使用以下任一方法创建 Spanner 实例,请参阅创建实例。您可以将实例命名为 test-instance,以便将其用于本文档中引用名为 test-instance 的实例的其他主题。

  • Google Cloud CLI
  • Google Cloud 控制台
  • 客户端库(C++、C#、Go、Java、Node.js、PHP、Python 或 Ruby)

浏览示例文件

示例代码库包含一个示例,展示了如何将 Spanner 与 ADO.NET 搭配使用。

请浏览 SampleRunner.cs 文件,其中说明了如何使用 Spanner。代码展示了如何创建和使用新数据库。数据使用架构和数据模型页面中显示的示例架构。

创建数据库

GoogleSQL

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

PostgreSQL

gcloud spanner databases create example-db --instance=test-instance \
  --database-dialect=POSTGRESQL

您应该会看到:

Creating database...done.

创建表

以下代码会在数据库中创建两个表。

GoogleSQL

public static async Task CreateTables(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Create two tables in one batch on Spanner.
    var batch = connection.CreateBatch();
    batch.BatchCommands.Add("CREATE TABLE Singers (" +
                            "  SingerId   INT64 NOT NULL, " +
                            "  FirstName  STRING(1024), " +
                            "  LastName   STRING(1024), " +
                            "  SingerInfo BYTES(MAX) " +
                            ") PRIMARY KEY (SingerId)");
    batch.BatchCommands.Add("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");
    await batch.ExecuteNonQueryAsync();
    Console.WriteLine("Created Singers & Albums tables");
}

PostgreSQL

public static async Task CreateTables(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Create two tables in one batch on Spanner.
    var batch = connection.CreateBatch();
    batch.BatchCommands.Add("create table singers (" +
                            "  singer_id   bigint not null primary key, " +
                            "  first_name  varchar(1024), " +
                            "  last_name   varchar(1024), " +
                            "  singer_info bytea" +
                            ")");
    batch.BatchCommands.Add("create table albums (" +
                            "  singer_id     bigint not null, " +
                            "  album_id      bigint not null, " +
                            "  album_title   varchar, " +
                            "  primary key (singer_id, album_id)" +
                            ") interleave in parent singers on delete cascade");
    await batch.ExecuteNonQueryAsync();
    Console.WriteLine("Created Singers & Albums tables");
}

使用以下命令运行示例:

GoogleSQL

dotnet run createtables projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run createtablespg projects/PROJECT_ID/instances/test-instance/databases/example-db

下一步是将数据写入数据库。

创建连接

您必须先创建一个连接,然后才能与 Spanner 进行交互。数据库名称和其他连接属性在 ADO.NET 连接字符串中指定。

GoogleSQL

/// <summary>
/// Create an ADO.NET connection to a Spanner database.
/// </summary>
/// <param name="connectionString">
/// A connection string in the format
/// 'Data Source=projects/my-project/instances/my-instance/databases/my-database'.
/// </param>
public static async Task CreateConnection(string connectionString)
{
    // Use a SpannerConnectionStringBuilder to construct a connection string.
    // The SpannerConnectionStringBuilder contains properties for the most
    // used connection string variables.
    var builder = new SpannerConnectionStringBuilder(connectionString)
    {
        // Sets the default isolation level that should be used for all
        // read/write transactions on this connection.
        DefaultIsolationLevel = IsolationLevel.RepeatableRead,

        // The Options property can be used to set any connection property
        // as a key-value pair.
        Options = "statement_cache_size=2000"
    };

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

    await using var command = connection.CreateCommand();
    command.CommandText = "SELECT 'Hello World' as Message";
    await using var reader = await command.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"Greeting from Spanner: {reader.GetString(0)}");
    }
}

PostgreSQL

/// <summary>
/// Create an ADO.NET connection to a Spanner PostgreSQL database.
/// </summary>
/// <param name="connectionString">
/// A connection string in the format
/// 'Data Source=projects/my-project/instances/my-instance/databases/my-database'.
/// </param>
public static async Task CreateConnection(string connectionString)
{
    // Use a SpannerConnectionStringBuilder to construct a connection string.
    // The SpannerConnectionStringBuilder contains properties for the most
    // used connection string variables.
    var builder = new SpannerConnectionStringBuilder(connectionString)
    {
        // Sets the default isolation level that should be used for all
        // read/write transactions on this connection.
        DefaultIsolationLevel = IsolationLevel.RepeatableRead,

        // The Options property can be used to set any connection property
        // as a key-value pair.
        Options = "statement_cache_size=2000"
    };

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

    await using var command = connection.CreateCommand();
    command.CommandText = "SELECT 'Hello World' as Message";
    await using var reader = await command.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"Greeting from Spanner: {reader.GetString(0)}");
    }
}

使用 DML 写入数据

您可以在读写事务中使用数据操纵语言 (DML) 插入数据。

使用 DbCommand#ExecuteNonQuery 方法来执行 DML 语句。

GoogleSQL

public static async Task WriteDataWithDml(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Add 4 rows in one statement.
    // The ADO.NET driver supports positional query parameters.
    await using var command = connection.CreateCommand();
    command.CommandText = "INSERT INTO Singers (SingerId, FirstName, LastName) " +
                          "VALUES (?, ?, ?), (?, ?, ?), " +
                          "       (?, ?, ?), (?, ?, ?)";
    command.Parameters.Add(12);
    command.Parameters.Add("Melissa");
    command.Parameters.Add("Garcia");

    command.Parameters.Add(13);
    command.Parameters.Add("Russel");
    command.Parameters.Add("Morales");

    command.Parameters.Add(14);
    command.Parameters.Add("Jacqueline");
    command.Parameters.Add("Long");

    command.Parameters.Add(15);
    command.Parameters.Add("Dylan");
    command.Parameters.Add("Shaw");

    var affected = await command.ExecuteNonQueryAsync();
    Console.WriteLine($"{affected} record(s) inserted.");
}

PostgreSQL

public static async Task WriteDataWithDml(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Add 4 rows in one statement.
    // The ADO.NET driver supports positional query parameters.
    await using var command = connection.CreateCommand();
    command.CommandText = "insert into singers (singer_id, first_name, last_name) " +
                          "VALUES (?, ?, ?), (?, ?, ?), " +
                          "       (?, ?, ?), (?, ?, ?)";
    command.Parameters.Add(12);
    command.Parameters.Add("Melissa");
    command.Parameters.Add("Garcia");

    command.Parameters.Add(13);
    command.Parameters.Add("Russel");
    command.Parameters.Add("Morales");

    command.Parameters.Add(14);
    command.Parameters.Add("Jacqueline");
    command.Parameters.Add("Long");

    command.Parameters.Add(15);
    command.Parameters.Add("Dylan");
    command.Parameters.Add("Shaw");

    var affected = await command.ExecuteNonQueryAsync();
    Console.WriteLine($"{affected} record(s) inserted.");
}

使用以下命令运行示例:

GoogleSQL

dotnet run dmlwrite projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run dmlwritepg projects/PROJECT_ID/instances/test-instance/databases/example-db

结果应显示:

4 records inserted.

使用变更写入数据

您还可以使用变更插入数据。

您可以使用 batch.CreateInsertCommand() 方法插入数据,此方法会创建一个新的 SpannerBatchCommand 以将行插入表中。SpannerBatchCommand.ExecuteNonQueryAsync() 方法将向表中添加新行。

以下代码展示了如何使用变更写入数据:

GoogleSQL

struct Singer
{
    internal long SingerId;
    internal string FirstName;
    internal string LastName;
}

struct Album
{
    internal long SingerId;
    internal long AlbumId;
    internal string Title;
}

public static async Task WriteDataWithMutations(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    Singer[] singers =
    [
        new() {SingerId=1, FirstName = "Marc", LastName = "Richards"},
        new() {SingerId=2, FirstName = "Catalina", LastName = "Smith"},
        new() {SingerId=3, FirstName = "Alice", LastName = "Trentor"},
        new() {SingerId=4, FirstName = "Lea", LastName = "Martin"},
        new() {SingerId=5, FirstName = "David", LastName = "Lomond"},
    ];
    Album[] albums =
    [
        new() {SingerId = 1, AlbumId = 1, Title = "Total Junk"},
        new() {SingerId = 1, AlbumId = 2, Title = "Go, Go, Go"},
        new() {SingerId = 2, AlbumId = 1, Title = "Green"},
        new() {SingerId = 2, AlbumId = 2, Title = "Forever Hold Your Peace"},
        new() {SingerId = 2, AlbumId = 3, Title = "Terrified"},
    ];
    var batch = connection.CreateBatch();
    foreach (var singer in singers)
    {
        // The name of a parameter must correspond with a column name.
        var command = batch.CreateInsertCommand("Singers");
        command.AddParameter("SingerId", singer.SingerId);
        command.AddParameter("FirstName", singer.FirstName);
        command.AddParameter("LastName", singer.LastName);
        batch.BatchCommands.Add(command);
    }
    foreach (var album in albums)
    {
        // The name of a parameter must correspond with a column name.
        var command = batch.CreateInsertCommand("Albums");
        command.AddParameter("SingerId", album.SingerId);
        command.AddParameter("AlbumId", album.AlbumId);
        command.AddParameter("AlbumTitle", album.Title);
        batch.BatchCommands.Add(command);
    }
    var affected = await batch.ExecuteNonQueryAsync();
    Console.WriteLine($"Inserted {affected} rows.");
}

PostgreSQL

struct Singer
{
    internal long SingerId;
    internal string FirstName;
    internal string LastName;
}

struct Album
{
    internal long SingerId;
    internal long AlbumId;
    internal string Title;
}

public static async Task WriteDataWithMutations(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    Singer[] singers =
    [
        new() {SingerId=1, FirstName = "Marc", LastName = "Richards"},
        new() {SingerId=2, FirstName = "Catalina", LastName = "Smith"},
        new() {SingerId=3, FirstName = "Alice", LastName = "Trentor"},
        new() {SingerId=4, FirstName = "Lea", LastName = "Martin"},
        new() {SingerId=5, FirstName = "David", LastName = "Lomond"},
    ];
    Album[] albums =
    [
        new() {SingerId = 1, AlbumId = 1, Title = "Total Junk"},
        new() {SingerId = 1, AlbumId = 2, Title = "Go, Go, Go"},
        new() {SingerId = 2, AlbumId = 1, Title = "Green"},
        new() {SingerId = 2, AlbumId = 2, Title = "Forever Hold Your Peace"},
        new() {SingerId = 2, AlbumId = 3, Title = "Terrified"},
    ];
    var batch = connection.CreateBatch();
    foreach (var singer in singers)
    {
        // The name of a parameter must correspond with a column name.
        var command = batch.CreateInsertCommand("singers");
        command.AddParameter("singer_id", singer.SingerId);
        command.AddParameter("first_name", singer.FirstName);
        command.AddParameter("last_name", singer.LastName);
        batch.BatchCommands.Add(command);
    }
    foreach (var album in albums)
    {
        // The name of a parameter must correspond with a column name.
        var command = batch.CreateInsertCommand("albums");
        command.AddParameter("singer_id", album.SingerId);
        command.AddParameter("album_id", album.AlbumId);
        command.AddParameter("album_title", album.Title);
        batch.BatchCommands.Add(command);
    }
    var affected = await batch.ExecuteNonQueryAsync();
    Console.WriteLine($"Inserted {affected} rows.");
}

使用 write 参数运行以下示例:

GoogleSQL

dotnet run write projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run writepg projects/PROJECT_ID/instances/test-instance/databases/example-db

使用 SQL 查询数据

Spanner 支持使用 SQL 接口读取数据,您可以使用 Google Cloud CLI 在命令行中访问该接口,也可以使用 Spanner ADO.NET 驱动程序以编程方式访问该接口。

在命令行中

执行以下 SQL 语句,读取 Albums 表中所有列的值:

GoogleSQL

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

PostgreSQL

gcloud spanner databases execute-sql example-db --instance=test-instance \
    --sql='SELECT singer_id, album_id, album_title 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

使用 Spanner ADO.NET 驱动程序

除了在命令行中执行 SQL 语句外,还可以使用 Spanner ADO.NET 驱动程序以编程方式发出相同的 SQL 语句。

以下方法用于执行 SQL 查询:

  • DbCommand 类中的 ExecuteReader 方法:使用此方法可执行会返回行的 SQL 语句,例如包含 THEN RETURN 子句的查询或 DML 语句。
  • DbDataReader 类:使用此类访问由 SQL 语句返回的数据。

以下示例使用了 ExecuteReaderAsync 方法:

GoogleSQL

public static async Task QueryData(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    await using var command = connection.CreateCommand();
    command.CommandText = "SELECT SingerId, AlbumId, AlbumTitle " +
                          "FROM Albums " +
                          "ORDER BY SingerId, AlbumId";
    await using var reader = await command.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"{reader["SingerId"]} {reader["AlbumId"]} {reader["AlbumTitle"]}");
    }
}

PostgreSQL

public static async Task QueryData(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    await using var command = connection.CreateCommand();
    command.CommandText = "SELECT singer_id, album_id, album_title " +
                          "FROM albums " +
                          "ORDER BY singer_id, album_id";
    await using var reader = await command.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"{reader["singer_id"]} {reader["album_id"]} {reader["album_title"]}");
    }
}

使用以下命令运行示例:

GoogleSQL

dotnet run query projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run querypg projects/PROJECT_ID/instances/test-instance/databases/example-db

结果应显示:

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

使用 SQL 参数进行查询

如果您的应用具有频繁执行的查询,您可以通过将其参数化来提高其性能。生成的参数化查询可以缓存下来并重复使用,这样做可以降低编译开销。如需了解详情,请参阅使用查询参数来加快频繁执行的查询的运行速度

以下示例演示了如何在 WHERE 子句中使用参数来查询包含特定 LastName 值的记录。

Spanner ADO.NET 驱动程序支持位置查询参数和命名查询参数。SQL 语句中的 ? 表示位置查询参数。向 DbCommandParameters 添加查询参数值。例如:

GoogleSQL

public static async Task QueryDataWithParameter(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    await using var command = connection.CreateCommand();
    command.CommandText = "SELECT SingerId, FirstName, LastName " +
                          "FROM Singers " +
                          "WHERE LastName = ?";
    command.Parameters.Add("Garcia");
    await using var reader = await command.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"{reader["SingerId"]} {reader["FirstName"]} {reader["LastName"]}");
    }
}

PostgreSQL

public static async Task QueryDataWithParameter(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    await using var command = connection.CreateCommand();
    command.CommandText = "SELECT singer_id, first_name, last_name " +
                          "FROM singers " +
                          "WHERE last_name = ?";
    command.Parameters.Add("Garcia");
    await using var reader = await command.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"{reader["singer_id"]} {reader["first_name"]} {reader["last_name"]}");
    }
}

使用以下命令运行示例:

GoogleSQL

dotnet run querywithparameter projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run querywithparameterpg projects/PROJECT_ID/instances/test-instance/databases/example-db

结果会显示以下内容:

12 Melissa Garcia

更新数据库架构

假设您需要将名为 MarketingBudget 的新列添加到 Albums 表。向现有表添加新列需要更新数据库架构。Spanner 支持在数据库继续处理流量的同时,对数据库进行架构更新。架构更新不需要使数据库离线,并且不会锁定整个表或列;在架构更新期间,您可以继续将数据写入数据库。如需详细了解支持的架构更新和架构更改性能,请参阅更新架构

添加列

您可以使用 Google Cloud CLI 在命令行中添加列,也可以使用 Spanner ADO.NET 驱动程序以编程方式添加列。

在命令行中

使用以下 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 marketing_budget bigint'

您应该会看到:

Schema updating...done.

使用 Spanner ADO.NET 驱动程序

使用 ExecuteNonQueryAsync 方法修改架构:

GoogleSQL

public static async Task AddColumn(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    await using var command = connection.CreateCommand();
    command.CommandText = "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64";
    await command.ExecuteNonQueryAsync();

    Console.WriteLine("Added MarketingBudget column");
}

PostgreSQL

public static async Task AddColumn(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    await using var command = connection.CreateCommand();
    command.CommandText = "alter table albums add column marketing_budget bigint";
    await command.ExecuteNonQueryAsync();

    Console.WriteLine("Added marketing_budget column");
}

使用以下命令运行示例:

GoogleSQL

dotnet run addcolumn projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run addcolumnpg projects/PROJECT_ID/instances/test-instance/databases/example-db

结果会显示以下内容:

Added MarketingBudget column.

执行 DDL 批处理

我们建议您在一个批处理操作中执行多个架构修改。使用 ADO.NET CreateBatch 方法创建批处理。以下示例会在一个批处理操作中创建两个表:

GoogleSQL

public static async Task DdlBatch(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Executing multiple DDL statements as one batch is
    // more efficient than executing each statement individually.
    var batch = connection.CreateBatch();
    batch.BatchCommands.Add(
        "CREATE TABLE Venues (" +
        "  VenueId     INT64 NOT NULL, " +
        "  Name        STRING(1024), " +
        "  Description JSON, " +
        ") PRIMARY KEY (VenueId)");
    batch.BatchCommands.Add(
        "CREATE TABLE Concerts (" +
        "  ConcertId INT64 NOT NULL, " +
        "  VenueId   INT64 NOT NULL, " +
        "  SingerId  INT64 NOT NULL, " +
        "  StartTime TIMESTAMP, " +
        "  EndTime   TIMESTAMP, " +
        "  CONSTRAINT Fk_Concerts_Venues " +
        "    FOREIGN KEY (VenueId) REFERENCES Venues (VenueId), " +
        "  CONSTRAINT Fk_Concerts_Singers " +
        "    FOREIGN KEY (SingerId) REFERENCES Singers (SingerId), " +
        ") PRIMARY KEY (ConcertId)");
    await batch.ExecuteNonQueryAsync();

    Console.WriteLine("Added Venues and Concerts tables");
}

PostgreSQL

public static async Task DdlBatch(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Executing multiple DDL statements as one batch is
    // more efficient than executing each statement individually.
    var batch = connection.CreateBatch();
    batch.BatchCommands.Add(
        "create table venues (" +
        "  venue_id    bigint not null primary key, " +
        "  name        varchar(1024), " +
        "  description jsonb" +
        ")");
    batch.BatchCommands.Add(
        "create table concerts (" +
        "  concert_id bigint not null primary key, " +
        "  venue_id   bigint not null, " +
        "  singer_id  bigint not null, " +
        "  start_time timestamptz, " +
        "  end_time   timestamptz, " +
        "  constraint fk_concerts_venues foreign key " +
        "    (venue_id) references venues (venue_id), " +
        "  constraint fk_concerts_singers foreign key " +
        "    (singer_id) references singers (singer_id)" +
        ")");
    await batch.ExecuteNonQueryAsync();

    Console.WriteLine("Added Venues and Concerts tables");
}

使用以下命令运行示例:

GoogleSQL

dotnet run ddlbatch projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run ddlbatchpg projects/PROJECT_ID/instances/test-instance/databases/example-db

结果会显示以下内容:

Added Venues and Concerts tables.

将数据写入新列

以下代码可将数据写入新列。对于 Albums(1, 1) 键控的行,该代码会将 MarketingBudget 设置为 100000;而对于 Albums(2, 2) 键控的行,该代码会将其设置为 500000

GoogleSQL

public static async Task UpdateDataWithMutations(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    (long SingerId, long AlbumId, long MarketingBudget)[] albums = [
        (1L, 1L, 100000L),
        (2L, 2L, 500000L),
    ];
    // Use a batch to update two rows in one round-trip.
    var batch = connection.CreateBatch();
    foreach (var album in albums)
    {
        // This creates a command that will use a mutation to update the row.
        var command = batch.CreateUpdateCommand("Albums");
        command.AddParameter("SingerId", album.SingerId);
        command.AddParameter("AlbumId", album.AlbumId);
        command.AddParameter("MarketingBudget", album.MarketingBudget);
        batch.BatchCommands.Add(command);
    }
    var affected = await batch.ExecuteNonQueryAsync();
    Console.WriteLine($"Updated {affected} albums.");
}

PostgreSQL

public static async Task UpdateDataWithMutations(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    (long SingerId, long AlbumId, long MarketingBudget)[] albums = [
        (1L, 1L, 100000L),
        (2L, 2L, 500000L),
    ];
    // Use a batch to update two rows in one round-trip.
    var batch = connection.CreateBatch();
    foreach (var album in albums)
    {
        // This creates a command that will use a mutation to update the row.
        var command = batch.CreateUpdateCommand("albums");
        command.AddParameter("singer_id", album.SingerId);
        command.AddParameter("album_id", album.AlbumId);
        command.AddParameter("marketing_budget", album.MarketingBudget);
        batch.BatchCommands.Add(command);
    }
    var affected = await batch.ExecuteNonQueryAsync();
    Console.WriteLine($"Updated {affected} albums.");
}

使用以下命令运行示例:

GoogleSQL

dotnet run update projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run updatepg projects/PROJECT_ID/instances/test-instance/databases/example-db

结果会显示以下内容:

Updated 2 albums

您也可以执行 SQL 查询来获取刚刚写入的值。

以下示例使用 ExecuteReaderAsync 方法执行查询:

GoogleSQL

public static async Task QueryNewColumn(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    await using var command = connection.CreateCommand();
    command.CommandText = "SELECT SingerId, AlbumId, MarketingBudget " +
                          "FROM Albums " +
                          "ORDER BY SingerId, AlbumId";
    await using var reader = await command.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"{reader["SingerId"]} {reader["AlbumId"]} {reader["MarketingBudget"]}");
    }
}

PostgreSQL

public static async Task QueryNewColumn(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    await using var command = connection.CreateCommand();
    command.CommandText = "select singer_id, album_id, marketing_budget " +
                          "from albums " +
                          "order by singer_id, album_id";
    await using var reader = await command.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"{reader["singer_id"]} {reader["album_id"]} {reader["marketing_budget"]}");
    }
}

如需执行此查询,请运行以下命令:

GoogleSQL

dotnet run querymarketingbudget projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run querymarketingbudgetpg projects/PROJECT_ID/instances/test-instance/databases/example-db

您应该会看到:

1 1 100000
1 2 null
2 1 null
2 2 500000
2 3 null

更新数据

您可以在读写事务中使用 DML 来更新数据。

调用 connection.BeginTransactionAsync() 可在 ADO.NET 中执行读写事务。

GoogleSQL

public static async Task WriteDataWithTransaction(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Transfer marketing budget from one album to another. We do it in a
    // transaction to ensure that the transfer is atomic.
    await using var transaction = await connection.BeginTransactionAsync();

    // The Spanner ADO.NET driver supports both positional and named
    // query parameters. This query uses named query parameters.
    const string selectSql =
        "SELECT MarketingBudget " +
        "FROM Albums " +
        "WHERE SingerId = @singerId and AlbumId = @albumId";
    // Get the marketing_budget of singer 2 / album 2.
    await using var command = connection.CreateCommand();
    command.CommandText = selectSql;
    command.Transaction = transaction;
    command.Parameters.AddWithValue("singerId", 2);
    command.Parameters.AddWithValue("albumId", 2);
    var budget2 = (long) (await command.ExecuteScalarAsync() ?? 0L);

    const long transfer = 20000L;
    // The transaction will only be committed if this condition still holds
    // at the time of commit. Otherwise, the transaction will be aborted.
    if (budget2 >= transfer)
    {
        // Get the marketing_budget of singer 1 / album 1.
        command.Parameters["singerId"].Value = 1;
        command.Parameters["albumId"].Value = 1;
        var budget1 = (long) (await command.ExecuteScalarAsync() ?? 0L);

        // Transfer part of the marketing budget of Album 2 to Album 1.
        budget1 += transfer;
        budget2 -= transfer;
        const string updateSql =
            "UPDATE Albums " +
            "SET MarketingBudget = @budget " +
            "WHERE SingerId = @singerId and AlbumId = @albumId";
        // Create a DML batch and execute it as part of the current transaction.
        var batch = connection.CreateBatch();
        batch.Transaction = transaction;

        // Update the marketing budgets of both Album 1 and Album 2 in a batch.
        (long SingerId, long AlbumId, long MarketingBudget)[] budgets = [
            new (1L, 1L, budget1),
            new (2L, 2L, budget2),
        ];
        foreach (var budget in budgets)
        {
            var batchCommand = batch.CreateBatchCommand();
            batchCommand.CommandText = updateSql;
            var singerIdParameter = batchCommand.CreateParameter();
            singerIdParameter.ParameterName = "singerId";
            singerIdParameter.Value = budget.SingerId;
            batchCommand.Parameters.Add(singerIdParameter);
            var albumIdParameter = batchCommand.CreateParameter();
            albumIdParameter.ParameterName = "albumId";
            albumIdParameter.Value = budget.AlbumId;
            batchCommand.Parameters.Add(albumIdParameter);
            var marketingBudgetParameter = batchCommand.CreateParameter();
            marketingBudgetParameter.ParameterName = "budget";
            marketingBudgetParameter.Value = budget.MarketingBudget;
            batchCommand.Parameters.Add(marketingBudgetParameter);
            batch.BatchCommands.Add(batchCommand);
        }
        var affected = await batch.ExecuteNonQueryAsync();
        // The batch should update 2 rows.
        if (affected != 2)
        {
            await transaction.RollbackAsync();
            throw new InvalidOperationException($"Unexpected num affected: {affected}");
        }
    }
    // Commit the transaction.
    await transaction.CommitAsync();
    Console.WriteLine("Transferred marketing budget from Album 2 to Album 1");
}

PostgreSQL

public static async Task WriteDataWithTransaction(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Transfer marketing budget from one album to another. We do it in a
    // transaction to ensure that the transfer is atomic.
    await using var transaction = await connection.BeginTransactionAsync();

    // The Spanner ADO.NET driver supports both positional and named
    // query parameters. This query uses named query parameters.
    const string selectSql =
        "SELECT marketing_budget " +
        "FROM albums " +
        "WHERE singer_id = $1 and album_id = $2";
    // Get the marketing_budget of singer 2 / album 2.
    await using var command = connection.CreateCommand();
    command.CommandText = selectSql;
    command.Transaction = transaction;
    command.Parameters.AddWithValue("p1", 2);
    command.Parameters.AddWithValue("p2", 2);
    var budget2 = (long) (await command.ExecuteScalarAsync() ?? 0L);

    const long transfer = 20000L;
    // The transaction will only be committed if this condition still holds
    // at the time of commit. Otherwise, the transaction will be aborted.
    if (budget2 >= transfer)
    {
        // Get the marketing_budget of singer 1 / album 1.
        command.Parameters["p1"].Value = 1;
        command.Parameters["p2"].Value = 1;
        var budget1 = (long) (await command.ExecuteScalarAsync() ?? 0L);

        // Transfer part of the marketing budget of Album 2 to Album 1.
        budget1 += transfer;
        budget2 -= transfer;
        const string updateSql =
            "UPDATE albums " +
            "SET marketing_budget = $1 " +
            "WHERE singer_id = $2 and album_id = $3";
        // Create a DML batch and execute it as part of the current transaction.
        var batch = connection.CreateBatch();
        batch.Transaction = transaction;

        // Update the marketing budgets of both Album 1 and Album 2 in a batch.
        (long SingerId, long AlbumId, long MarketingBudget)[] budgets = [
            new (1L, 1L, budget1),
            new (2L, 2L, budget2),
        ];
        foreach (var budget in budgets)
        {
            var batchCommand = batch.CreateBatchCommand();
            batchCommand.CommandText = updateSql;
            var marketingBudgetParameter = batchCommand.CreateParameter();
            marketingBudgetParameter.ParameterName = "p1";
            marketingBudgetParameter.Value = budget.MarketingBudget;
            batchCommand.Parameters.Add(marketingBudgetParameter);
            var singerIdParameter = batchCommand.CreateParameter();
            singerIdParameter.ParameterName = "p2";
            singerIdParameter.Value = budget.SingerId;
            batchCommand.Parameters.Add(singerIdParameter);
            var albumIdParameter = batchCommand.CreateParameter();
            albumIdParameter.ParameterName = "p3";
            albumIdParameter.Value = budget.AlbumId;
            batchCommand.Parameters.Add(albumIdParameter);
            batch.BatchCommands.Add(batchCommand);
        }
        var affected = await batch.ExecuteNonQueryAsync();
        // The batch should update 2 rows.
        if (affected != 2)
        {
            await transaction.RollbackAsync();
            throw new InvalidOperationException($"Unexpected num affected: {affected}");
        }
    }
    // Commit the transaction.
    await transaction.CommitAsync();
    Console.WriteLine("Transferred marketing budget from Album 2 to Album 1");
}

使用以下命令运行示例:

GoogleSQL

dotnet run writewithtransactionusingdml projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run writewithtransactionusingdmlpg projects/PROJECT_ID/instances/test-instance/databases/example-db

事务标记和请求标记

使用事务标记和请求标记可排查 Spanner 中的事务和查询问题。您可以在 Transaction 对象上设置标记以向 Spanner 发送事务标记,也可以在 DbCommand 对象上设置标记以向 Spanner 发送请求标记。例如:

GoogleSQL

public static async Task Tags(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    const long singerId = 1L;
    const long albumId = 1L;

    await using var transaction = await connection.BeginTransactionAsync();
    // Set a tag on the transaction before executing any statements.
    transaction.Tag = "example-tx-tag";

    await using var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.Tag = "query-marketing-budget";
    command.CommandText =
        "SELECT MarketingBudget " +
        "FROM Albums " +
        "WHERE SingerId=? and AlbumId=?";
    command.Parameters.Add(singerId);
    command.Parameters.Add(albumId);
    var budget = (long)(await command.ExecuteScalarAsync() ?? 0L);

    // Reduce the marketing budget by 10% if it is more than 1,000.
    if (budget > 1000)
    {
        budget -= budget / 10;
        await using var updateCommand = connection.CreateCommand();
        updateCommand.Transaction = transaction;
        updateCommand.Tag = "reduce-marketing-budget";
        updateCommand.CommandText =
            "UPDATE Albums SET MarketingBudget=@budget WHERE SingerId=@singerId AND AlbumId=@albumId";
        updateCommand.Parameters.AddWithValue("budget", budget);
        updateCommand.Parameters.AddWithValue("singerId", singerId);
        updateCommand.Parameters.AddWithValue("albumId", albumId);
        await updateCommand.ExecuteNonQueryAsync();
    }
    // Commit the transaction.
    await transaction.CommitAsync();
    Console.WriteLine("Reduced marketing budget");
}

PostgreSQL

public static async Task Tags(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    const long singerId = 1L;
    const long albumId = 1L;

    await using var transaction = await connection.BeginTransactionAsync();
    // Set a tag on the transaction before executing any statements.
    transaction.Tag = "example-tx-tag";

    await using var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.Tag = "query-marketing-budget";
    command.CommandText =
        "select marketing_budget " +
        "from albums " +
        "where singer_id=? and album_id=?";
    command.Parameters.Add(singerId);
    command.Parameters.Add(albumId);
    var budget = (long)(await command.ExecuteScalarAsync() ?? 0L);

    // Reduce the marketing budget by 10% if it is more than 1,000.
    if (budget > 1000)
    {
        budget -= budget / 10;
        await using var updateCommand = connection.CreateCommand();
        updateCommand.Transaction = transaction;
        updateCommand.Tag = "reduce-marketing-budget";
        updateCommand.CommandText =
            "update albums set marketing_budget=$1 where singer_id=$2 and album_id=$3";
        updateCommand.Parameters.Add(budget);
        updateCommand.Parameters.Add(singerId);
        updateCommand.Parameters.Add(albumId);
        await updateCommand.ExecuteNonQueryAsync();
    }
    // Commit the transaction.
    await transaction.CommitAsync();
    Console.WriteLine("Reduced marketing budget");
}

使用以下命令运行示例:

GoogleSQL

dotnet run tags projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run tagspg projects/PROJECT_ID/instances/test-instance/databases/example-db

使用只读事务检索数据

假设您要在同一时间戳执行多个读取操作。只读事务会观察事务提交记录的一致前缀,以便应用始终获得一致的数据。 调用 connection.BeginReadOnlyTransactionAsync() 可执行只读事务。

下面演示了如何运行查询并在同一只读事务中执行读取操作:

GoogleSQL

public static async Task ReadOnlyTransaction(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Start a read-only transaction on this connection.
    await using var transaction = await connection.BeginReadOnlyTransactionAsync();

    await using var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "SELECT SingerId, AlbumId, AlbumTitle " +
                          "FROM Albums " +
                          "ORDER BY SingerId, AlbumId";
    await using (var reader = await command.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine(
                $"{reader["SingerId"]} {reader["AlbumId"]} {reader["AlbumTitle"]}");
        }
    }

    // Execute another query using the same read-only transaction.
    command.CommandText = "SELECT SingerId, AlbumId, AlbumTitle " +
                          "FROM Albums " +
                          "ORDER BY AlbumTitle";
    await using (var reader = await command.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine(
                $"{reader["SingerId"]} {reader["AlbumId"]} {reader["AlbumTitle"]}");
        }
    }

    // End the read-only transaction by calling Commit.
    await transaction.CommitAsync();
}

PostgreSQL

public static async Task ReadOnlyTransaction(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Start a read-only transaction on this connection.
    await using var transaction = await connection.BeginReadOnlyTransactionAsync();

    await using var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "SELECT SingerId, AlbumId, AlbumTitle " +
                          "FROM Albums " +
                          "ORDER BY SingerId, AlbumId";
    await using (var reader = await command.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine(
                $"{reader["SingerId"]} {reader["AlbumId"]} {reader["AlbumTitle"]}");
        }
    }

    // Execute another query using the same read-only transaction.
    command.CommandText = "SELECT SingerId, AlbumId, AlbumTitle " +
                          "FROM Albums " +
                          "ORDER BY AlbumTitle";
    await using (var reader = await command.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine(
                $"{reader["SingerId"]} {reader["AlbumId"]} {reader["AlbumTitle"]}");
        }
    }

    // End the read-only transaction by calling Commit.
    await transaction.CommitAsync();
}

使用以下命令运行示例:

GoogleSQL

dotnet run readonlytransaction projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run readonlytransactionpg projects/PROJECT_ID/instances/test-instance/databases/example-db

结果会显示以下内容:

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

分区 DML

分区数据操纵语言 (DML) 旨在用于以下类型的批量更新和删除:

  • 定期清理和垃圾回收。
  • 使用默认值回填新列。

GoogleSQL

public static async Task PartitionedDml(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Enable Partitioned DML on this connection.
    await using var command = connection.CreateCommand();
    command.CommandText = "SET AUTOCOMMIT_DML_MODE='PARTITIONED_NON_ATOMIC'";
    await command.ExecuteNonQueryAsync();

    // Back-fill a default value for the MarketingBudget column.
    command.CommandText = "UPDATE Albums SET MarketingBudget=0 WHERE MarketingBudget IS NULL";
    var affected = await command.ExecuteNonQueryAsync();

    // Partitioned DML returns the minimum number of records that were affected.
    Console.WriteLine($"Updated at least {affected} albums");

    // Reset the value for AUTOCOMMIT_DML_MODE to its default.
    command.CommandText = "RESET AUTOCOMMIT_DML_MODE";
    await command.ExecuteNonQueryAsync();
}

PostgreSQL

public static async Task PartitionedDml(string connectionString)
{
    await using var connection = new SpannerConnection(connectionString);
    await connection.OpenAsync();

    // Enable Partitioned DML on this connection.
    await using var command = connection.CreateCommand();
    command.CommandText = "set autocommit_dml_mode='partitioned_non_atomic'";
    await command.ExecuteNonQueryAsync();

    // Back-fill a default value for the MarketingBudget column.
    command.CommandText = "update albums set marketing_budget=0 where marketing_budget is null";
    var affected = await command.ExecuteNonQueryAsync();

    // Partitioned DML returns the minimum number of records that were affected.
    Console.WriteLine($"Updated at least {affected} albums");

    // Reset the value for autocommit_dml_mode to its default.
    command.CommandText = "reset autocommit_dml_mode";
    await command.ExecuteNonQueryAsync();
}

使用以下命令运行示例:

GoogleSQL

dotnet run pdml projects/PROJECT_ID/instances/test-instance/databases/example-db

PostgreSQL

dotnet run pdmlpg projects/PROJECT_ID/instances/test-instance/databases/example-db

清理

为避免因本教程中使用的资源导致您的 Google 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. 确认您要删除实例并点击删除

后续步骤