Google.Cloud.Spanner.Data
Google.Cloud.Spanner.Data
is the ADO.NET provider for Cloud Spanner. It is the recommended
package for regular Cloud Spanner database access from .NET.
Note:
This documentation is for version 5.0.0-beta04
of the library.
Some samples may not work with other versions.
The Google.Cloud.Spanner.Admin.Instance.V1 package should be used for Cloud Spanner instance administration, such as creating or deleting instances.
Installation
Install the Google.Cloud.Spanner.Data
package from NuGet. Add it to
your project in the normal way (for example by right-clicking on the
project in Visual Studio and choosing "Manage NuGet Packages...").
Please ensure you enable pre-release packages (for example, in the
Visual Studio NuGet user interface, check the "Include prerelease"
box). Some of the following samples might only work with the latest
pre-release version (5.0.0-beta04
) of Google.Cloud.Spanner.Data
.
Authentication
When running on Google Cloud, no action needs to be taken to authenticate.
Otherwise, the simplest way of authenticating your API calls is to set up Application Default Credentials. The credentials will automatically be used to authenticate. See Set up Application Default Credentials for more details.
Getting started
Operations generally follow ADO.NET conventions. However Cloud Spanner does not support DML (eg. "INSERT INTO ..."). Therefore you should use the corresponding API on SpannerConnection to create ADO command objects instead of setting CommandText manually.
Credentials
Without any other configuration, Google.Cloud.Spanner.Data
uses
Application Default Credentials.
If you need to provide other credentials, there are three alternatives:
- Specify
CredentialFile
in the connection string - Pass a
GoogleCredential
into the constructor ofSpannerConnection
orSpannerConnectionStringBuilder
. Scopes will be applied automatically, and self-signed JWTs will be used where possible. - Pass a
ChannelCredentials
into the constructor ofSpannerConnection
orSpannerConnectionStringBuilder
.
SpannerConnection and Dependency Injection
A SpannerConnection
can be provided via dependency injection based
on the ConnectionStrings section of the application configuration,
via the AddSpannerConnection
extension method. For example,
consider an application configured with appsettings.json
file like this:
{
"ConnectionStrings": {
"SpannerDb": "Data Source=projects/my-project/instances/my-instance"
}
}
The SpannerConnection
can be configured on the service collection
as shown below:
services.AddSpannerConnection("SpannerDb");
The dependency is registered as a transient dependency (as a new
SpannerConnection
should be created for each request), and is
automatically disposed by the dependency injection infrastructure.
If the dependency injection container has been configured with
ChannelCredentials
or GoogleCredential
values, those are used
automatically by AddSpannerConnection
. (If both are specified,
ChannelCredentials
takes priority.) Note that this means any
CredentialsFile
specified in the connection string is ignored when
the connection is obtained via dependency injection and a
ChannelCredentials
or GoogleCredential
is available from the
same container.
Sample code
Once you have created your Google Cloud Project and Spanner Instance using the web console, you can start using the ADO.NET provider to create and modify a Cloud Spanner database.
Creating a Database and Table
Note
For convenienceGoogle.Cloud.Spanner.Data
allows you to use standardSpannerCommand
s for DDL operations. But do take into account that Spanner transactions do not support DDL operations, so even if you setSpannerCommand.Transaction
on a DDL command, the command will execute outside the transaction.
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
SpannerCommand createDbCmd = connection.CreateDdlCommand($"CREATE DATABASE {databaseName}");
await createDbCmd.ExecuteNonQueryAsync();
SpannerCommand createTableCmd = connection.CreateDdlCommand(
@"CREATE TABLE TestTable (
Key STRING(MAX) NOT NULL,
StringValue STRING(MAX),
Int64Value INT64,
) PRIMARY KEY (Key)");
await createTableCmd.ExecuteNonQueryAsync();
}
DataAdapter support (.NET 4.5+ only)
Cloud Spanner supports a limited DataAdapter that provides basic CRUD operations on any table. Create a SpannerDataAdapter with the target table and its primary keys. You may replace any commands provided by SpannerDataAdapter with your own custom commands.
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
DataSet untypedDataSet = new DataSet();
// Provide the name of the Cloud Spanner table and primary key column names.
SpannerDataAdapter adapter = new SpannerDataAdapter(connection, "TestTable", "Key");
adapter.Fill(untypedDataSet);
// Insert a row
DataRow row = untypedDataSet.Tables[0].NewRow();
row["Key"] = Guid.NewGuid().ToString("N");
row["StringValue"] = "New String Value";
row["Int64Value"] = 0L;
untypedDataSet.Tables[0].Rows.Add(row);
adapter.Update(untypedDataSet.Tables[0]);
}
Modifying data
Cloud Spanner supports two approaches to modifying data: DML, and direct row modifications.
DML
DML is capable of affecting multiple rows with a single command. For example, you could delete all rows matching a query, or update rows to set the value of one column equal to another one.
DML can be executed in standard mode using ExecuteNonQuery
or ExecuteNonQueryAsync
:
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.OpenAsync();
// If the transaction is aborted, RunWithRetriableTransactionAsync will
// retry the whole unit of work with a fresh transaction each time.
// Please be aware that the whole unit of work needs to be prepared
// to be called more than once.
await connection.RunWithRetriableTransactionAsync(async transaction =>
{
SpannerCommand cmd = connection.CreateDmlCommand(
"UPDATE TestTable SET StringValue='Updated' WHERE Int64Value=@value");
cmd.Parameters.Add("value", SpannerDbType.Int64, 10);
cmd.Transaction = transaction;
int rowsAffected = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"{rowsAffected} rows updated...");
});
}
It is recommended that you execute DML statements within a retriable transaction. Queries within the same transaction are able to observe the changes already made by DML statements, and later DML statements can use the values created or updated by earlier ones. Also, if the transaction is aborted at any point, the whole unit of work will be retried automatically. Note that the code executing within a retriable transaction needs to be prepared to be called more than once. Please read the Cloud Spanner user documentation for details on when and why transactions can be aborted.
Some DML statements can be executed in a partitioned manner, enabling an efficient
update of large data sets. ExecutePartitionedUpdate
or ExecutePartitionedUpdateAsync
:
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.OpenAsync();
SpannerCommand cmd = connection.CreateDmlCommand(
"UPDATE TestTable SET TestTable.StringValue='Updated in partitions' WHERE TestTable.Int64Value=@value");
cmd.Parameters.Add("value", SpannerDbType.Int64, 9);
long rowsAffected = await cmd.ExecutePartitionedUpdateAsync();
Console.WriteLine($"{rowsAffected} rows updated...");
}
Partitioned DML updates cannot be performed within another transaction, and have "at least once" semantics: the update can be applied more than once to a row in some cases, and so is best used with idempotent updates.
Not all DML statements can be partitioned. Please read the Cloud Spanner user documentation for details on the restrictions.
Batch DML
Batch DML allows you to execute multiple DML commands in one batched operation.
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.OpenAsync();
// If the transaction is aborted, RunWithRetriableTransactionAsync will
// retry the whole unit of work with a fresh transaction each time.
// Please be aware that the whole unit of work needs to be prepared
// to be called more than once.
await connection.RunWithRetriableTransactionAsync(async (transaction) =>
{
SpannerBatchCommand cmd = transaction.CreateBatchDmlCommand();
cmd.Add(
"UPDATE TestTable SET StringValue='Updated' WHERE Int64Value=@value",
new SpannerParameterCollection { { "value", SpannerDbType.Int64, 5 } });
cmd.Add(
"DELETE FROM TestTable WHERE Int64Value=@value",
new SpannerParameterCollection { { "value", SpannerDbType.Int64, 250 } });
IEnumerable<long> rowsAffected = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"{rowsAffected.ElementAt(0)} rows updated...");
Console.WriteLine($"{rowsAffected.ElementAt(1)} rows deleted...");
});
}
Statements are executed sequentally in the same order they are provided. Changes made by one statement are visible to all subsequent statements in the batch.
When one of the statemens in the batch fails, execution is halted and all subsequent statements are not
executed. A SpannerBatchNonQueryException
will be thrown that contains both information about the error
and the number of rows affected by each of the statements that executed before the failed one. If you are executing
the batch DML command inside a non-retriable transaction you can simply commit the transaction when SpannerBatchNonQueryException
is thrown if partial success is acceptable in your application.
It is recommended to run Batch DML within a retriable transaction. If the transaction
aborts the whole unit of work will be retried automatically.
Note that the code executing within a retriable transaction needs to be prepared to be called more
than once.
Please read the Cloud Spanner user documentation
for details on when and why transactions can be aborted.
Direct row modifications
The following operations are supported for direct row modification:
- Insert
- Update
- Delete
- Insert or update (also known as "upsert")
Create a command from the SpannerConnection
, providing the table name to the appropriate method
(CreateInsertCommand
and the like), then use the command parameters to specify values for columns.
The command can be reused to perform the same kind of operation for multiple rows.
This sample inserts two rows, then reads them again:
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.OpenAsync();
// If the transaction is aborted, RunWithRetriableTransactionAsync will
// retry the whole unit of work with a fresh transaction each time.
await connection.RunWithRetriableTransactionAsync(async transaction =>
{
SpannerCommand cmd = connection.CreateInsertCommand("TestTable");
SpannerParameter keyParameter = cmd.Parameters.Add("Key", SpannerDbType.String);
SpannerParameter stringValueParameter = cmd.Parameters.Add("StringValue", SpannerDbType.String);
SpannerParameter int64ValueParameter = cmd.Parameters.Add("Int64Value", SpannerDbType.Int64);
cmd.Transaction = transaction;
// This executes 5 distinct insert commands using the retriable transaction.
// The mutations will be effective once the transaction has committed successfully.
for (int i = 0; i < 5; i++)
{
keyParameter.Value = Guid.NewGuid().ToString("N");
stringValueParameter.Value = $"StringValue{i}";
int64ValueParameter.Value = i;
int rowsAffected = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"{rowsAffected} rows written...");
}
});
}
This sample reads three keys using a SELECT
command, updates a row using an UPDATE
command, then
deletes a row using a DELETE
command:
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.OpenAsync();
// If the transaction is aborted, RunWithRetriableTransactionAsync will
// retry the whole unit of work with a fresh transaction each time.
// Please be aware that the whole unit of work needs to be prepared
// to be called more than once.
await connection.RunWithRetriableTransactionAsync(async transaction =>
{
// Read the first two keys in the database.
List<string> keys = new List<string>();
SpannerCommand selectCmd = connection.CreateSelectCommand("SELECT * FROM TestTable");
selectCmd.Transaction = transaction;
using (SpannerDataReader reader = await selectCmd.ExecuteReaderAsync())
{
while (keys.Count < 3 && await reader.ReadAsync())
{
keys.Add(reader.GetFieldValue<string>("Key"));
}
}
// Update the Int64Value of keys[0]
// Include the primary key and update columns.
SpannerCommand updateCmd = connection.CreateUpdateCommand("TestTable");
updateCmd.Parameters.Add("Key", SpannerDbType.String, keys[0]);
updateCmd.Parameters.Add("Int64Value", SpannerDbType.Int64, 0L);
updateCmd.Transaction = transaction;
await updateCmd.ExecuteNonQueryAsync();
// Delete row for keys[1]
SpannerCommand deleteCmd = connection.CreateDeleteCommand("TestTable");
deleteCmd.Parameters.Add("Key", SpannerDbType.String, keys[1]);
deleteCmd.Transaction = transaction;
await deleteCmd.ExecuteNonQueryAsync();
});
When direct row modifications are performed in a transaction, they are only applied when the transaction is committed. Queries within the transaction will not observe any changes. It is recommended to run direct row modifications within a retriable transaction. If the transaction aborts the whole unit of work will be retried automatically. Note that the code executing within a retriable transaction needs to be prepared to be called more than once. Please read the Cloud Spanner user documentation for details on when and why transactions can be aborted.
Automatic retry of single commands
When executing a single write command without specifying a transaction, an exclusive ephemeral transaction will be used internally. If the transaction is aborted the command will be retried automatically. This means that, for executing single commands that don't need to share a transaction, you don't need to use a retriable transaction.
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.OpenAsync();
// The following commands will be executed within an exclusive ephemeral transaction.
// If the transaction is aborted, execution of the command will be automatically retried.
SpannerCommand cmd = connection.CreateInsertCommand("TestTable");
cmd.Parameters.Add("Key", SpannerDbType.String, Guid.NewGuid().ToString("N"));
cmd.Parameters.Add("StringValue", SpannerDbType.String, "hello");
cmd.Parameters.Add("Int64Value", SpannerDbType.Int64, 8);
int rowsAffected = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"{rowsAffected} rows written...");
}
This applies to DML, Batch DML and direct row modifications. It does not apply to partioned DML commands as it is the Cloud Spanner service the one that handles the transaction lifecycle for partioned DML commands, which are already automatically retried on aborted.
Using a commit timestamp
Spanner allows you to write a commit timestamp for insert and update operations.
When using direct row modifications, the commit timestamp can be set
using SpannerParameter.CommitTimestamp
as the value for a
parameter, as shown below.
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.OpenAsync();
string createTableStatement =
@"CREATE TABLE UsersHistory (
Id INT64 NOT NULL,
CommitTs TIMESTAMP NOT NULL OPTIONS
(allow_commit_timestamp=true),
Name STRING(MAX) NOT NULL,
Email STRING(MAX),
Deleted BOOL NOT NULL,
) PRIMARY KEY(Id, CommitTs DESC)";
await connection.CreateDdlCommand(createTableStatement).ExecuteNonQueryAsync();
// Create a command for inserting rows.
SpannerCommand cmd = connection.CreateInsertCommand("UsersHistory",
new SpannerParameterCollection
{
{ "Id", SpannerDbType.Int64 },
{ "CommitTs", SpannerDbType.Timestamp, SpannerParameter.CommitTimestamp },
{ "Name", SpannerDbType.String },
{ "Deleted", SpannerDbType.Bool , false}
});
int rowsAffected = 0;
// If the transaction is aborted, RunWithRetriableTransactionAsync will
// retry the whole unit of work with a fresh transaction each time.
// Please be aware that the whole unit of work needs to be prepared
// to be called more than once.
await connection.RunWithRetriableTransactionAsync(async transaction =>
{
// Insert a first row
cmd.Parameters["Id"].Value = 10L;
cmd.Parameters["Name"].Value = "Demo 1";
cmd.Transaction = transaction;
rowsAffected += await cmd.ExecuteNonQueryAsync();
});
await connection.RunWithRetriableTransactionAsync(async transaction =>
{
// Insert a second row
cmd.Parameters["Id"].Value = 11L;
cmd.Parameters["Name"].Value = "Demo 2";
cmd.Transaction = transaction;
rowsAffected += await cmd.ExecuteNonQueryAsync();
});
Console.WriteLine($"{rowsAffected} rows written...");
// Display the inserted values
SpannerCommand selectCmd = connection.CreateSelectCommand("SELECT * FROM UsersHistory");
using (SpannerDataReader reader = await selectCmd.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
long id = reader.GetFieldValue<long>("Id");
string name = reader.GetFieldValue<string>("Name");
DateTime timestamp = reader.GetFieldValue<DateTime>("CommitTs");
Console.WriteLine($"{id}: {name} - {timestamp:HH:mm:ss.ffffff}");
}
}
}
This parameter value cannot be used in DML. Instead, use the
PENDING_COMMIT_TIMESTAMP()
function in your DML statement.
See the Cloud Spanner documentation for more information about commit timestamps.
Transactions and Fault Handling
Cloud Spanner is fully ACID compliant.
Retries should be implemented at the transaction level (as opposed to individual call level)
because there is a chance in high stress situations that a Cloud Spanner session (transaction)
can be aborted due to causing a deadlock. In this case, the entire transaction should be
retried. The methods SpannerConnection.RunWithRetriableTransactionAsync(...)
will take care of
automatically retrying the entire callback passed as a paremeter using a fresh transaction
if previous attempts aborted.
- The callback doesn't need to handle the transaction lifecycle.
- The callback needs to set the transaction received on all the commands it creates.
- The callback might be called more than once, so the code should be written bearing this in mind.
Please read the Cloud Spanner user documentation for details on when and why transactions can be aborted.
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.OpenAsync();
// If the transaction is aborted, RunWithRetriableTransactionAsync will
// retry the whole unit of work with a fresh transaction each time.
await connection.RunWithRetriableTransactionAsync(async transaction =>
{
SpannerCommand cmd = connection.CreateInsertCommand("TestTable");
SpannerParameter keyParameter = cmd.Parameters.Add("Key", SpannerDbType.String);
SpannerParameter stringValueParameter = cmd.Parameters.Add("StringValue", SpannerDbType.String);
SpannerParameter int64ValueParameter = cmd.Parameters.Add("Int64Value", SpannerDbType.Int64);
cmd.Transaction = transaction;
// This executes 5 distinct insert commands using the retriable transaction.
// The mutations will be effective once the transaction has committed successfully.
for (int i = 0; i < 5; i++)
{
keyParameter.Value = Guid.NewGuid().ToString("N");
stringValueParameter.Value = $"StringValue{i}";
int64ValueParameter.Value = i;
int rowsAffected = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"{rowsAffected} rows written...");
}
});
}
Session pool management
Google.Cloud.Spanner.Data
manages Spanner sessions for you via a
session pool. The pool has various configuration
options, but additionally you may wish to take
advantage of additional SpannerConnection
methods at the start and
end of your application.
To ensure that the session pool is populated with the configured
minimum number of sessions before serving requests from your
application, you may wish to wait for the task returned by
SpannerConnection.WhenSessionPoolReady()
to complete before
marking your application as "ready to serve".
// This would usually be executed during application start-up, before any Spanner
// operations are performed. It can be used at any time, however. It is purely passive:
// it doesn't modify the session pool or trigger any other actions.
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.WhenSessionPoolReady();
}
When your application is shutting down, you may wish to delete the
sessions it has been using from the server. Most applications will
not need to do this: sessions automatically expire after an idle
time of an hour anyway, and the server-enforced session limit is
high. Applications using a large number of sessions may wish to
clean up sessions more actively, however. Again, SpannerConnection
provides a simple method to accomplish this:
// When your application is shutting down. Note that any pending or future requests
// for sessions will fail.
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
await connection.ShutdownSessionPoolAsync();
}
If you want to diagnose session management issues,
SpannerConnection
provides a method to fetch a snapshot of
the statistics for the session pool associated with the connection:
using (SpannerConnection connection = new SpannerConnection(connectionString))
{
SessionPool.SessionPoolSegmentStatistics stats = connection.GetSessionPoolSegmentStatistics();
if (stats is null)
{
Console.WriteLine("No session pool for this connection string yet");
}
else
{
// Access individual properties...
Console.WriteLine($"Database name: {stats.DatabaseName}");
Console.WriteLine($"Active sessions: {stats.ActiveSessionCount}");
Console.WriteLine($"Pooled sessions: {stats.PoolCount}");
// ... or just use the overridden ToString method to log all the statistics in one go:
Console.WriteLine(stats);
}
}
Retrieving an RpcException from a SpannerException
The RpcException
property of SpannerException
returns the underlying
RpcException
which caused the SpannerException
. The returned
value may be null if the SpannerException
was not caused by an RpcException
.
This can in turn be used to obtain an ErrorInfo which can provide actionable error details.
Sample code:
using Google.Api.Gax.Grpc;
try
{
// Code using Spanner.
}
catch (SpannerException ex)
{
if (ex.RpcException is RpcException rpcException)
{
if (rpcException.GetErrorInfo() is ErrorInfo errorInfo)
{
var domain = errorInfo.Domain;
var reason = errorInfo.Reason;
var metadata = errorInfo.Metadata;
// Use the domain, reason and metadata of ErrorInfo, e.g. for logging or taking remedial action.
}
else
{
// Use RpcException's other properties (e.g. Status and Message) to handle the exception appropriately.
}
}
else
{
// Use SpannerException's other properties (e.g. ErrorCode and Message) to handle the exception appropriately.
}
}