Visão geral de transações

Esta página explica as transações no Spanner e inclui exemplos de código para execução delas.

Introdução

Uma transação no Spanner é um conjunto de leituras e gravações executadas atomicamente em um único ponto lógico no tempo em colunas, linhas e tabelas em um banco de dados.

O Spanner é compatível com estes modos de transação:

  • Leitura e gravação com bloqueio. Essas transações dependem de bloqueio pessimista e, se necessário, confirmação em duas fases. O bloqueio de transações de leitura e gravação pode ser cancelado, exigindo uma nova tentativa do aplicativo.

  • Somente leitura. Esse tipo de transação oferece consistência garantida em várias leituras, mas não permite gravações. Por padrão, as transações somente leitura são executadas em um carimbo de data/hora escolhido pelo sistema que garante consistência externa, mas elas também podem ser configuradas para serem lidas em um carimbo de data/hora no passado. As transações somente leitura não precisam ser confirmadas e não têm bloqueios. Além disso, as transações somente leitura podem aguardar a conclusão das gravações em andamento antes de executá-las.

  • DML particionada. Esse tipo de transação executa uma instrução de linguagem de manipulação de dados (DML, na sigla em inglês) como DML particionada. A DML particionada foi projetada para atualizações e exclusões em massa, principalmente limpeza e preenchimento periódicos. Se você precisar confirmar um grande número de gravações cegas, mas não exigir uma transação atômica, modifique em massa as tabelas do Spanner usando gravação em lote. Para mais informações, consulte Modificar dados usando gravações em lote.

Nesta página, descrevemos as propriedades gerais e a semântica das transações no Spanner. Além disso, apresentamos as interfaces de transações de leitura e gravação, somente leitura e DML particionada no Spanner.

Transações de leitura e gravação

Estes são os cenários em que você precisa usar uma transação de leitura e gravação com bloqueio:

  • Se você fizer uma gravação que dependa do resultado de uma ou mais leituras, faça a gravação e as leituras na mesma transação de leitura e gravação.
    • Exemplo: dobrar o saldo da conta bancária A. A leitura do saldo de A precisa estar na mesma transação que a gravação para substituir o saldo pelo valor duplicado.

  • Se você fizer uma ou mais gravações que precisam ser confirmadas atomicamente, faça-as na mesma transação de leitura e gravação.
    • Exemplo: transferir US $200 da conta A para a conta B. As duas gravações (uma para diminuir A em US $200 e outra para aumentar B em US $200) e as leituras dos saldos iniciais da conta precisam estar na mesma transação.

  • Se você pode fazer uma ou mais gravações, dependendo dos resultados de uma ou mais leituras, faça as gravações e as leituras na mesma transação de leitura e gravação, mesmo que as gravações acabem não sendo executadas.
    • Exemplo: transferir US$ 200 da conta bancária A para a conta bancária B se o saldo atual de A for superior a US$ 500. A transação precisa ter uma leitura do saldo de A e uma instrução condicional que contenha as gravações.

Neste cenário, não use uma transação de leitura e gravação com bloqueio:

  • Se você está apenas fazendo leituras e pode expressá-las usando um método de leitura única, use esse método ou uma transação somente leitura. As leituras únicas não são bloqueadas, ao contrário das transações de leitura e gravação.

Propriedades

Uma transação de leitura e gravação no Spanner executa um conjunto de leituras e gravações atomicamente em um único ponto lógico no tempo. Além disso, o carimbo de data/hora em que as transações de leitura e gravação são executadas corresponde à hora de um relógio normal, e a ordem de serialização corresponde à ordem do carimbo de data/hora.

Por que usar uma transação de leitura e gravação? Elas oferecem as propriedades ACID dos bancos de dados relacionais. Na verdade, as transações de leitura e gravação do Spanner oferecem garantias ainda mais fortes do que o ACID tradicional. Consulte a seção Semântica abaixo.

Isolamento

Confira a seguir propriedades de isolamento para transações de leitura/gravação e somente leitura.

Transações que leem e gravam

Estas são as propriedades de isolamento que você recebe após confirmar com sucesso uma transação que contém uma série de leituras (ou consultas) e gravações:

  • Todas as leituras na transação retornaram valores que refletem um snapshot consistente capturado no carimbo de data/hora de confirmação da transação.
  • Linhas ou intervalos vazios permaneceram assim no momento da confirmação.
  • Todas as gravações dentro da transação foram confirmadas no carimbo de data/hora de confirmação da transação.
  • As gravações não eram visíveis para nenhuma transação até a confirmação da transação.

Certos drivers de cliente do Spanner contêm lógica de nova tentativa de transação para mascarar erros temporários, o que eles fazem executando novamente a transação e validando os dados observados pelo cliente.

O efeito é que todas as leituras e gravações parecem ter ocorrido em um único momento, tanto da perspectiva da transação em si quanto da perspectiva de outros leitores e gravadores no banco de dados do Spanner. Em outras palavras, as leituras e gravações acabam ocorrendo no mesmo carimbo de data/hora. Veja uma ilustração disso na seção Consistência externa e capacidade de serialização abaixo.

Transações que só leem

As garantias para uma transação de leitura e gravação que somente lê são similares: todas as leituras dentro dessa transação retornam dados do mesmo carimbo de data/hora, mesmo para a inexistência de linhas. Uma diferença é que, se você lê dados e depois confirma a transação de leitura e gravação sem nenhuma gravação, não há garantia de que os dados não mudaram no banco de dados após a leitura e antes da confirmação. Se você quer saber se os dados foram alterados desde a última leitura, a melhor abordagem é lê-los novamente (em uma transação de leitura/gravação ou usando uma leitura forte). Por questões de eficiência, e se você já sabe que vai apenas ler e não gravar, use uma transação somente leitura em vez de uma transação de leitura/gravação.

Atomicidade, consistência, durabilidade

Além da propriedade de isolamento, o Spanner fornece atomicidade (se qualquer uma das gravações na transação for confirmada, todas elas serão confirmadas), consistência (o banco de dados permanece em um estado consistente após a transação) e durabilidade (os dados confirmados permanecem confirmados).

Benefícios dessas propriedades

Devido a essas propriedades, como desenvolvedor de aplicativos, você se concentra na exatidão de cada transação por si só, sem se preocupar em como proteger a execução dela contra outras transações que podem ser executadas ao mesmo tempo.

Interface

As bibliotecas de cliente do Spanner fornecem uma interface para executar um conjunto de trabalho no contexto de uma transação de leitura e gravação, com novas tentativas para cancelamentos de transações. Aqui está um pouco do contexto para explicar esse ponto: uma transação do Spanner pode precisar ser testada várias vezes antes de ser confirmada. Por exemplo, se duas transações tentam trabalhar nos dados ao mesmo tempo de maneira que pode causar impasse, o Spanner cancela uma delas para que a outra possa progredir. Mais raramente, eventos temporários no Spanner podem resultar no cancelamento de algumas transações. Como as transações são atômicas, uma transação cancelada não tem efeito visível no banco de dados. Portanto, execute as transações com novas tentativas até que tenham sucesso.

Ao usar uma transação em uma biblioteca de cliente do Spanner, você define o corpo de uma transação (ou seja, as leituras e gravações a serem executadas em uma ou mais tabelas de um banco de dados) na forma de um objeto de função. Em segundo plano, a biblioteca de cliente do Spanner executa a função repetidamente até que a transação seja confirmada ou um erro que não permita uma nova tentativa seja encontrado.

Exemplo

Imagine que você adicionou uma coluna MarketingBudget à tabela Albums mostrada na página "Modelo de dados e esquema":

CREATE TABLE Albums (
  SingerId        INT64 NOT NULL,
  AlbumId         INT64 NOT NULL,
  AlbumTitle      STRING(MAX),
  MarketingBudget INT64
) PRIMARY KEY (SingerId, AlbumId);

O departamento de marketing decidiu promover o álbum codificado por Albums (1, 1) e pediu para você transferir US$ 200.000 do orçamento de Albums (2, 2), mas apenas se o dinheiro estiver disponível no orçamento desse álbum. Use uma transação de leitura e gravação com bloqueio para essa operação porque, dependendo do resultado de uma leitura, é possível que a transação faça alguma gravação.

Veja a seguir como executar uma transação de leitura e gravação:

C++

void ReadWriteTransaction(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  using ::google::cloud::StatusOr;

  // A helper to read a single album MarketingBudget.
  auto get_current_budget =
      [](spanner::Client client, spanner::Transaction txn,
         std::int64_t singer_id,
         std::int64_t album_id) -> StatusOr<std::int64_t> {
    auto key = spanner::KeySet().AddKey(spanner::MakeKey(singer_id, album_id));
    auto rows = client.Read(std::move(txn), "Albums", std::move(key),
                            {"MarketingBudget"});
    using RowType = std::tuple<std::int64_t>;
    auto row = spanner::GetSingularRow(spanner::StreamOf<RowType>(rows));
    if (!row) return std::move(row).status();
    return std::get<0>(*std::move(row));
  };

  auto commit = client.Commit(
      [&client, &get_current_budget](
          spanner::Transaction const& txn) -> StatusOr<spanner::Mutations> {
        auto b1 = get_current_budget(client, txn, 1, 1);
        if (!b1) return std::move(b1).status();
        auto b2 = get_current_budget(client, txn, 2, 2);
        if (!b2) return std::move(b2).status();
        std::int64_t transfer_amount = 200000;

        return spanner::Mutations{
            spanner::UpdateMutationBuilder(
                "Albums", {"SingerId", "AlbumId", "MarketingBudget"})
                .EmplaceRow(1, 1, *b1 + transfer_amount)
                .EmplaceRow(2, 2, *b2 - transfer_amount)
                .Build()};
      });

  if (!commit) throw std::move(commit).status();
  std::cout << "Transfer was successful [spanner_read_write_transaction]\n";
}

C#


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

public class ReadWriteWithTransactionAsyncSample
{
    public async Task<int> ReadWriteWithTransactionAsync(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 Add Column and Write Data To New Column samples first,
        // in that order.

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

        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        decimal transferAmount = 200000;
        decimal secondBudget = 0;
        decimal firstBudget = 0;

        using var connection = new SpannerConnection(connectionString);
        using var cmdLookup1 = connection.CreateSelectCommand("SELECT * FROM Albums WHERE SingerId = 2 AND AlbumId = 2");

        using (var reader = await cmdLookup1.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                // Read the second album's budget.
                secondBudget = reader.GetFieldValue<decimal>("MarketingBudget");
                // Confirm second Album's budget is sufficient and
                // if not raise an exception. Raising an exception
                // will automatically roll back the transaction.
                if (secondBudget < transferAmount)
                {
                    throw new Exception($"The second album's budget {secondBudget} is less than the amount to transfer.");
                }
            }
        }

        // Read the first album's budget.
        using var cmdLookup2 = connection.CreateSelectCommand("SELECT * FROM Albums WHERE SingerId = 1 and AlbumId = 1");
        using (var reader = await cmdLookup2.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                firstBudget = reader.GetFieldValue<decimal>("MarketingBudget");
            }
        }

        // Specify update command parameters.
        using var cmdUpdate = connection.CreateUpdateCommand("Albums", new SpannerParameterCollection
        {
            { "SingerId", SpannerDbType.Int64 },
            { "AlbumId", SpannerDbType.Int64 },
            { "MarketingBudget", SpannerDbType.Int64 },
        });

        // Update second album to remove the transfer amount.
        secondBudget -= transferAmount;
        cmdUpdate.Parameters["SingerId"].Value = 2;
        cmdUpdate.Parameters["AlbumId"].Value = 2;
        cmdUpdate.Parameters["MarketingBudget"].Value = secondBudget;
        var rowCount = await cmdUpdate.ExecuteNonQueryAsync();

        // Update first album to add the transfer amount.
        firstBudget += transferAmount;
        cmdUpdate.Parameters["SingerId"].Value = 1;
        cmdUpdate.Parameters["AlbumId"].Value = 1;
        cmdUpdate.Parameters["MarketingBudget"].Value = firstBudget;
        rowCount += await cmdUpdate.ExecuteNonQueryAsync();
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func writeWithTransaction(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		getBudget := func(key spanner.Key) (int64, error) {
			row, err := txn.ReadRow(ctx, "Albums", key, []string{"MarketingBudget"})
			if err != nil {
				return 0, err
			}
			var budget int64
			if err := row.Column(0, &budget); err != nil {
				return 0, err
			}
			return budget, nil
		}
		album2Budget, err := getBudget(spanner.Key{2, 2})
		if err != nil {
			return err
		}
		const transferAmt = 200000
		if album2Budget >= transferAmt {
			album1Budget, err := getBudget(spanner.Key{1, 1})
			if err != nil {
				return err
			}
			album1Budget += transferAmt
			album2Budget -= transferAmt
			cols := []string{"SingerId", "AlbumId", "MarketingBudget"}
			txn.BufferWrite([]*spanner.Mutation{
				spanner.Update("Albums", cols, []interface{}{1, 1, album1Budget}),
				spanner.Update("Albums", cols, []interface{}{2, 2, album2Budget}),
			})
			fmt.Fprintf(w, "Moved %d from Album2's MarketingBudget to Album1's.", transferAmt)
		}
		return nil
	})
	return err
}

Java

static void writeWithTransaction(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        // Transfer marketing budget from one album to another. We do it in a transaction to
        // ensure that the transfer is atomic.
        Struct row =
            transaction.readRow("Albums", Key.of(2, 2), Arrays.asList("MarketingBudget"));
        long album2Budget = row.getLong(0);
        // Transaction will only be committed if this condition still holds at the time of
        // commit. Otherwise it will be aborted and the callable will be rerun by the
        // client library.
        long transfer = 200000;
        if (album2Budget >= transfer) {
          long album1Budget =
              transaction
                  .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget"))
                  .getLong(0);
          album1Budget += transfer;
          album2Budget -= transfer;
          transaction.buffer(
              Mutation.newUpdateBuilder("Albums")
                  .set("SingerId")
                  .to(1)
                  .set("AlbumId")
                  .to(1)
                  .set("MarketingBudget")
                  .to(album1Budget)
                  .build());
          transaction.buffer(
              Mutation.newUpdateBuilder("Albums")
                  .set("SingerId")
                  .to(2)
                  .set("AlbumId")
                  .to(2)
                  .set("MarketingBudget")
                  .to(album2Budget)
                  .build());
        }
        return null;
      });
}

Node.js

// This sample transfers 200,000 from the MarketingBudget field
// of the second Album to the first Album, as long as the second
// Album has enough money in its budget. Make sure to run the
// addColumn and updateData samples first (in that order).

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

const transferAmount = 200000;

database.runTransaction(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  let firstBudget, secondBudget;
  const queryOne = {
    columns: ['MarketingBudget'],
    keys: [[2, 2]], // SingerId: 2, AlbumId: 2
  };

  const queryTwo = {
    columns: ['MarketingBudget'],
    keys: [[1, 1]], // SingerId: 1, AlbumId: 1
  };

  Promise.all([
    // Reads the second album's budget
    transaction.read('Albums', queryOne).then(results => {
      // Gets second album's budget
      const rows = results[0].map(row => row.toJSON());
      secondBudget = rows[0].MarketingBudget;
      console.log(`The second album's marketing budget: ${secondBudget}`);

      // Makes sure the second album's budget is large enough
      if (secondBudget < transferAmount) {
        throw new Error(
          `The second album's budget (${secondBudget}) is less than the transfer amount (${transferAmount}).`
        );
      }
    }),

    // Reads the first album's budget
    transaction.read('Albums', queryTwo).then(results => {
      // Gets first album's budget
      const rows = results[0].map(row => row.toJSON());
      firstBudget = rows[0].MarketingBudget;
      console.log(`The first album's marketing budget: ${firstBudget}`);
    }),
  ])
    .then(() => {
      console.log(firstBudget, secondBudget);
      // Transfers the budgets between the albums
      firstBudget += transferAmount;
      secondBudget -= transferAmount;

      console.log(firstBudget, secondBudget);

      // Updates the database
      // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
      // must be converted (back) to strings before being inserted as INT64s.
      transaction.update('Albums', [
        {
          SingerId: '1',
          AlbumId: '1',
          MarketingBudget: firstBudget.toString(),
        },
        {
          SingerId: '2',
          AlbumId: '2',
          MarketingBudget: secondBudget.toString(),
        },
      ]);
    })
    .then(() => {
      // Commits the transaction and send the changes to the database
      return transaction.commit();
    })
    .then(() => {
      console.log(
        `Successfully executed read-write transaction to transfer ${transferAmount} from Album 2 to Album 1.`
      );
    })
    .catch(err => {
      console.error('ERROR:', err);
    })
    .then(() => {
      transaction.end();
      // Closes the database when finished
      return database.close();
    });
});

PHP

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;
use UnexpectedValueException;

/**
 * Performs a read-write transaction to update two sample records in the
 * database.
 *
 * This will transfer 200,000 from the `MarketingBudget` field for the second
 * Album to the first Album. If the `MarketingBudget` for the second Album is
 * too low, it will raise an exception.
 *
 * Before running this sample, you will need to run the `update_data` sample
 * to populate the fields.
 * Example:
 * ```
 * read_write_transaction($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_write_transaction(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $database->runTransaction(function (Transaction $t) use ($spanner) {
        $transferAmount = 200000;

        // Read the second album's budget.
        $secondAlbumKey = [2, 2];
        $secondAlbumKeySet = $spanner->keySet(['keys' => [$secondAlbumKey]]);
        $secondAlbumResult = $t->read(
            'Albums',
            $secondAlbumKeySet,
            ['MarketingBudget'],
            ['limit' => 1]
        );

        $firstRow = $secondAlbumResult->rows()->current();
        $secondAlbumBudget = $firstRow['MarketingBudget'];
        if ($secondAlbumBudget < $transferAmount) {
            // Throwing an exception will automatically roll back the transaction.
            throw new UnexpectedValueException(
                'The second album\'s budget is lower than the transfer amount: ' . $transferAmount
            );
        }

        $firstAlbumKey = [1, 1];
        $firstAlbumKeySet = $spanner->keySet(['keys' => [$firstAlbumKey]]);
        $firstAlbumResult = $t->read(
            'Albums',
            $firstAlbumKeySet,
            ['MarketingBudget'],
            ['limit' => 1]
        );

        // Read the first album's budget.
        $firstRow = $firstAlbumResult->rows()->current();
        $firstAlbumBudget = $firstRow['MarketingBudget'];

        // Update the budgets.
        $secondAlbumBudget -= $transferAmount;
        $firstAlbumBudget += $transferAmount;
        printf('Setting first album\'s budget to %s and the second album\'s ' .
            'budget to %s.' . PHP_EOL, $firstAlbumBudget, $secondAlbumBudget);

        // Update the rows.
        $t->updateBatch('Albums', [
            ['SingerId' => 1, 'AlbumId' => 1, 'MarketingBudget' => $firstAlbumBudget],
            ['SingerId' => 2, 'AlbumId' => 2, 'MarketingBudget' => $secondAlbumBudget],
        ]);

        // Commit the transaction!
        $t->commit();

        print('Transaction complete.' . PHP_EOL);
    });
}

Python

def read_write_transaction(instance_id, database_id):
    """Performs a read-write transaction to update two sample records in the
    database.

    This will transfer 200,000 from the `MarketingBudget` field for the second
    Album to the first Album. If the `MarketingBudget` is too low, it will
    raise an exception.

    Before running this sample, you will need to run the `update_data` sample
    to populate the fields.
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    def update_albums(transaction):
        # Read the second album budget.
        second_album_keyset = spanner.KeySet(keys=[(2, 2)])
        second_album_result = transaction.read(
            table="Albums",
            columns=("MarketingBudget",),
            keyset=second_album_keyset,
            limit=1,
        )
        second_album_row = list(second_album_result)[0]
        second_album_budget = second_album_row[0]

        transfer_amount = 200000

        if second_album_budget < transfer_amount:
            # Raising an exception will automatically roll back the
            # transaction.
            raise ValueError("The second album doesn't have enough funds to transfer")

        # Read the first album's budget.
        first_album_keyset = spanner.KeySet(keys=[(1, 1)])
        first_album_result = transaction.read(
            table="Albums",
            columns=("MarketingBudget",),
            keyset=first_album_keyset,
            limit=1,
        )
        first_album_row = list(first_album_result)[0]
        first_album_budget = first_album_row[0]

        # Update the budgets.
        second_album_budget -= transfer_amount
        first_album_budget += transfer_amount
        print(
            "Setting first album's budget to {} and the second album's "
            "budget to {}.".format(first_album_budget, second_album_budget)
        )

        # Update the rows.
        transaction.update(
            table="Albums",
            columns=("SingerId", "AlbumId", "MarketingBudget"),
            values=[(1, 1, first_album_budget), (2, 2, second_album_budget)],
        )

    database.run_in_transaction(update_albums)

    print("Transaction complete.")

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner         = Google::Cloud::Spanner.new project: project_id
client          = spanner.client instance_id, database_id
transfer_amount = 200_000

client.transaction do |transaction|
  first_album  = transaction.read("Albums", [:MarketingBudget], keys: [[1, 1]]).rows.first
  second_album = transaction.read("Albums", [:MarketingBudget], keys: [[2, 2]]).rows.first

  raise "The second album does not have enough funds to transfer" if second_album[:MarketingBudget] < transfer_amount

  new_first_album_budget  = first_album[:MarketingBudget] + transfer_amount
  new_second_album_budget = second_album[:MarketingBudget] - transfer_amount

  transaction.update "Albums", [
    { SingerId: 1, AlbumId: 1, MarketingBudget: new_first_album_budget  },
    { SingerId: 2, AlbumId: 2, MarketingBudget: new_second_album_budget }
  ]
end

puts "Transaction complete"

Semântica

Capacidade de serialização e consistência externa

O Spanner oferece "capacidade de serialização", o que significa que todas as transações aparecem como se tivessem sido executadas em uma ordem serial, mesmo que algumas das leituras, gravações e outras operações de transações distintas realmente tenham ocorrido em paralelo. O Spanner atribui carimbos de data/hora de confirmação que refletem a ordem das transações confirmadas para implementar essa propriedade. Na verdade, o Spanner oferece uma garantia mais forte do que a capacidade de serialização chamada consistência externa: as transações são confirmadas em uma ordem que é refletida nos carimbos de data/hora de confirmação, e esses carimbos de data/hora de confirmação refletem o tempo real para que você possa compará-los ao relógio. As leituras em uma transação veem tudo que foi confirmado antes disso, e as gravações são vistas por tudo que começar depois da confirmação da transação.

Por exemplo, veja a execução de duas transações ilustrada no diagrama abaixo:

linha do tempo com a execução de duas transações que leem os mesmos dados

A transação Txn1 em azul lê alguns dados A, armazena em buffer uma gravação em A e, em seguida, faz a confirmação. A transação Txn2 em verde inicia após Txn1, lê alguns dados B e, em seguida, lê os dados A. Como Txn2 lê o valor de A depois que Txn1 confirmou a gravação para A, Txn2 verá o efeito da gravação de Txn1 em A, mesmo que Txn2 tenha começado antes de Txn1 terminar.

Mesmo que haja alguma sobreposição na hora em que Txn1 e Txn2 estão sendo executadas, os carimbos de data/hora de confirmação c1 e c2 respeitam uma ordem de transação linear. Isso significa que todos os efeitos das leituras e gravações de Txn1 parecem ter ocorrido em um único ponto de tempo (c1) e todos os efeitos das leituras e gravações Txn2 também parecem ter ocorrido em um único ponto de tempo (c2). Além disso, c1 < c2 (que é garantido porque as gravações confirmadas de Txn1 e Txn2 confirmaram a gravação, o que é verdade mesmo se as gravações aconteceram em máquinas diferentes), o que respeita a ordem de Txn1 acontecer antes de Txn2. No entanto, se Txn2 apenas fez leituras na transação, então c1 <= c2.

Nas leituras, um prefixo do histórico de confirmação é considerado: se uma leitura vê o efeito de Txn2, ela também percebe o efeito de Txn1. Todas as transações confirmadas com sucesso têm essa propriedade.

Garantias de leitura e gravação

Se uma chamada para executar uma transação falhar, as garantias de leitura e gravação dependerão do erro com que a chamada da confirmação subjacente falhou.

Por exemplo, um erro como "Linha não encontrada" ou "Linha já existe" indica que a gravação das mutações armazenadas em buffer encontrou algum erro, por exemplo: uma linha que o cliente está tentando atualizar não existe. Nesse caso, as leituras são garantidas de maneira consistente, as gravações não são aplicadas e a inexistência da linha é garantida para também ser consistente com as leituras.

Como cancelar operações da transação

As operações de leitura assíncronas podem ser canceladas a qualquer momento pelo usuário, por exemplo, quando uma operação de nível superior é cancelada ou você decide parar uma leitura com base nos resultados iniciais recebidos da leitura. Isso não afeta outras operações dentro da transação.

No entanto, mesmo que você tenha tentado cancelar a leitura, o Spanner não garante que ela seja realmente cancelada. Depois de solicitar o cancelamento de uma leitura, ela ainda pode ser concluída ou falhar por algum outro motivo (por exemplo, anulação). Além disso, essa leitura cancelada pode realmente retornar alguns resultados para você. Esses resultados, possivelmente incompletos, serão validados como parte da confirmação da transação.

Observe que, ao contrário das leituras, o cancelamento de uma operação de confirmação da transação resultará na anulação da transação, a menos que ela já tenha sido confirmada ou falhado por outro motivo.

Desempenho

Bloqueio

O Spanner permite que vários clientes interajam simultaneamente com o mesmo banco de dados. Para garantir a consistência de várias transações simultâneas, o Spanner usa uma combinação de bloqueios compartilhados e exclusivos para controlar o acesso aos dados. Quando você executa uma leitura como parte de uma transação, o Spanner adquire bloqueios de leitura compartilhados, o que permite que outras leituras ainda acessem os dados até que a transação esteja pronta para confirmação. No momento da confirmação da transação e da aplicação das gravações, a transação tenta fazer upgrade para um bloqueio exclusivo. Ele impede novos bloqueios compartilhados de leitura nos dados e espera que os atuais sejam limpos. Em seguida, coloca um bloqueio único para acesso exclusivo aos dados.

Observações sobre bloqueios:

  • Os bloqueios são considerados na granularidade de linha e coluna. Se a transação T1 bloqueou a coluna "A" da linha "foo" e a transação T2 quer gravar na coluna "B" da linha "foo", não há conflito.
  • Gravações em um item de dados que também não leem os dados que estão sendo gravados (também conhecidas como "gravações cegas") não entram em conflito com outras gravações cegas do mesmo item. O carimbo de data/hora de confirmação de cada gravação determina a ordem em que ele é aplicado ao banco de dados. Uma consequência disso é que o Spanner só precisa fazer upgrade para um bloqueio exclusivo se você leu os dados que está gravando. Caso contrário, o Spanner usa um bloqueio compartilhado de gravação.
  • Ao realizar pesquisas de linhas em uma transação de leitura e gravação, use os índices secundários para limitar as linhas verificadas a um intervalo menor. Isso faz com que o Spanner bloqueie um número menor de linhas na tabela, permitindo a modificação simultânea em linhas fora do intervalo.
  • Os bloqueios não podem ser usados para garantir acesso exclusivo a um recurso fora do Spanner. As transações podem ser canceladas por vários motivos pelo Spanner, como, por exemplo, ao permitir que os dados se movam pelos recursos de computação da instância. Se uma transação for repetida, seja explicitamente pelo código do aplicativo ou implicitamente pelo código do cliente, como o driver JDBC do Spanner, só será garantido que os bloqueios foram mantidos durante a tentativa realmente confirmada.

  • É possível usar a ferramenta de introdução Estatísticas de bloqueio para investigar conflitos de bloqueio no seu banco de dados.

Detecção de impasses

O Spanner detecta quando várias transações podem estar em impasse e força todas as transações, exceto uma, a serem canceladas. Por exemplo, pense no seguinte cenário: a transação Txn1 mantém um bloqueio no registro A e está aguardando um bloqueio no registro B, e Txn2 mantém um bloqueio no registro B e está aguardando um bloqueio no registro A. A única maneira de progredir nessa situação é cancelar uma das transações para liberar o bloqueio, permitindo que a outra transação continue.

O Spanner usa o algoritmo padrão "wound-wait" para lidar com a detecção de impasse. Em segundo plano, o Spanner monitora a idade de cada transação que solicita bloqueios conflitantes. Ele também permite que transações mais antigas anulem transações mais recentes. “Mais antigo” significa que a primeira leitura, consulta ou confirmação da transação aconteceu antes.

Ao dar prioridade a transações mais antigas, o Spanner garante que cada transação tenha a chance de adquirir bloqueios em algum momento, depois que ela envelhecer o suficiente para ter prioridade mais alta do que outras transações. Por exemplo, uma transação que adquire um bloqueio compartilhado do leitor pode ser interrompida por uma transação mais antiga que precisa de um bloqueio compartilhado do gravador.

Execução distribuída

O Spanner pode executar transações em dados que abrangem vários servidores. Esse avanço acaba reduzindo o desempenho em comparação com transações de servidor único.

Quais tipos de transações podem ser distribuídos? Em segundo plano, o Spanner pode dividir a responsabilidade por linhas no banco de dados entre vários servidores. Uma linha e as linhas correspondentes em tabelas intercaladas geralmente são disponibilizadas pelo mesmo servidor, assim como duas linhas na mesma tabela com as chaves próximas. O Spanner pode executar transações em linhas em servidores diferentes. No entanto, como regra geral, as transações que afetam muitas linhas colocalizadas são mais rápidas e baratas do que as transações que afetam muitas linhas espalhadas por todo o banco de dados ou em uma tabela grande.

As transações mais eficientes no Spanner incluem apenas as leituras e gravações que precisam ser aplicadas atomicamente. As transações são mais rápidas quando todas as leituras e gravações acessam dados na mesma parte do espaço de chave.

Transações somente leitura

Além de bloquear transações de leitura e gravação, o Spanner oferece transações somente leitura.

Use uma transação somente leitura quando precisar executar mais de uma leitura no mesmo carimbo de data/hora. Se você puder expressar sua leitura usando um dos métodos de leitura única do Spanner, use esse método. O desempenho da utilização de uma chamada de leitura única é comparável ao de uma única leitura feita em uma transação somente leitura.

Se você estiver lendo uma grande quantidade de dados, use partições para ler os dados em paralelo.

Como as transações somente leitura não gravam, elas não mantêm bloqueios e não bloqueiam outras transações. As transações somente leitura estão de acordo com um prefixo consistente do histórico de confirmações da transação. Portanto, o aplicativo sempre recebe dados consistentes.

Propriedades

Uma transação somente leitura do Spanner executa um conjunto de leituras em um único ponto lógico no tempo, tanto da perspectiva da transação somente leitura quanto da perspectiva de outros leitores e gravadores no banco de dados do Spanner. Isso significa que as transações somente leitura sempre estão de acordo com um estado consistente do banco de dados em um ponto escolhido no histórico das transações.

Interface

O Spanner oferece uma interface para executar um conjunto de trabalho no contexto de uma transação somente leitura, com novas tentativas para cancelamentos de transações.

Exemplo

A seguir, mostramos como usar uma transação somente leitura para receber dados consistentes para duas leituras no mesmo carimbo de data/hora:

C++

void ReadOnlyTransaction(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto read_only = spanner::MakeReadOnlyTransaction();

  spanner::SqlStatement select(
      "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
  using RowType = std::tuple<std::int64_t, std::int64_t, std::string>;

  // Read#1.
  auto rows1 = client.ExecuteQuery(read_only, select);
  std::cout << "Read 1 results\n";
  for (auto& row : spanner::StreamOf<RowType>(rows1)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
  // Read#2. Even if changes occur in-between the reads the transaction ensures
  // that Read #1 and Read #2 return the same data.
  auto rows2 = client.ExecuteQuery(read_only, select);
  std::cout << "Read 2 results\n";
  for (auto& row : spanner::StreamOf<RowType>(rows2)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
}

C#


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(AmbientTransactionOptions.ForTimestampBoundReadOnly(), 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;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func readOnlyTransaction(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	ro := client.ReadOnlyTransaction()
	defer ro.Close()
	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := ro.Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		var singerID int64
		var albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}

	iter = ro.Read(ctx, "Albums", spanner.AllKeys(), []string{"SingerId", "AlbumId", "AlbumTitle"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID int64
		var albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}
}

Java

static void readOnlyTransaction(DatabaseClient dbClient) {
  // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it.
  // We use a try-with-resource block to automatically do so.
  try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) {
    ResultSet queryResultSet =
        transaction.executeQuery(
            Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"));
    while (queryResultSet.next()) {
      System.out.printf(
          "%d %d %s\n",
          queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2));
    }
    try (ResultSet readResultSet =
        transaction.read(
            "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) {
      while (readResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2));
      }
    }
  }
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

// Gets a transaction object that captures the database state
// at a specific point in time
database.getSnapshot(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  const queryOne = 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums';

  try {
    // Read #1, using SQL
    const [qOneRows] = await transaction.run(queryOne);

    qOneRows.forEach(row => {
      const json = row.toJSON();
      console.log(
        `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`
      );
    });

    const queryTwo = {
      columns: ['SingerId', 'AlbumId', 'AlbumTitle'],
    };

    // Read #2, using the `read` method. Even if changes occur
    // in-between the reads, the transaction ensures that both
    // return the same data.
    const [qTwoRows] = await transaction.read('Albums', queryTwo);

    qTwoRows.forEach(row => {
      const json = row.toJSON();
      console.log(
        `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`
      );
    });

    console.log('Successfully executed read-only transaction.');
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    transaction.end();
    // Close the database when finished.
    await database.close();
  }
});

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Reads data inside of a read-only transaction.
 *
 * Within the read-only transaction, or "snapshot", the application sees
 * consistent view of the database at a particular timestamp.
 * Example:
 * ```
 * read_only_transaction($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_only_transaction(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $snapshot = $database->snapshot();
    $results = $snapshot->execute(
        'SELECT SingerId, AlbumId, AlbumTitle FROM Albums'
    );
    print('Results from the first read:' . PHP_EOL);
    foreach ($results as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }

    // Perform another read using the `read` method. Even if the data
    // is updated in-between the reads, the snapshot ensures that both
    // return the same data.
    $keySet = $spanner->keySet(['all' => true]);
    $results = $database->read(
        'Albums',
        $keySet,
        ['SingerId', 'AlbumId', 'AlbumTitle']
    );

    print('Results from the second read:' . PHP_EOL);
    foreach ($results->rows() as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }
}

Python

def read_only_transaction(instance_id, database_id):
    """Reads data inside of a read-only transaction.

    Within the read-only transaction, or "snapshot", the application sees
    consistent view of the database at a particular timestamp.
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    with database.snapshot(multi_use=True) as snapshot:
        # Read using SQL.
        results = snapshot.execute_sql(
            "SELECT SingerId, AlbumId, AlbumTitle FROM Albums"
        )

        print("Results from first read:")
        for row in results:
            print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

        # Perform another read using the `read` method. Even if the data
        # is updated in-between the reads, the snapshot ensures that both
        # return the same data.
        keyset = spanner.KeySet(all_=True)
        results = snapshot.read(
            table="Albums", columns=("SingerId", "AlbumId", "AlbumTitle"), keyset=keyset
        )

        print("Results from second read:")
        for row in results:
            print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

client.snapshot do |snapshot|
  snapshot.execute("SELECT SingerId, AlbumId, AlbumTitle FROM Albums").rows.each do |row|
    puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
  end

  # Even if changes occur in-between the reads, the transaction ensures that
  # both return the same data.
  snapshot.read("Albums", [:AlbumId, :AlbumTitle, :SingerId]).rows.each do |row|
    puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
  end
end

Transações de DML particionada

Com a linguagem de manipulação de dados particionada (DML particionada, na sigla em inglês), é possível executar instruções UPDATE e DELETE em larga escala sem ultrapassar os limites de transação nem bloquear uma tabela inteira. O Spanner particiona o espaço de chave e executa as instruções DML em cada partição em uma transação separada de leitura/gravação.

Execute as instruções DML em transações de leitura e gravação que você mesmo cria explicitamente no código. Para mais informações, consulte Como usar a DML.

Propriedades

É possível executar apenas uma instrução DML particionada por vez, esteja você usando um método de biblioteca de cliente ou a Google Cloud CLI.

As transações particionadas não são compatíveis com confirmação ou reversão. O Spanner executa e aplica a instrução DML imediatamente. Se você cancelar a operação ou ela falhar, o Spanner cancelará todas as partições em execução e não iniciará as partições restantes. O Spanner não reverte partições que já foram executadas.

Interface

O Spanner oferece uma interface para executar uma única instrução DML particionada.

Examples

O exemplo de código a seguir atualiza a coluna MarketingBudget da tabela Albums.

C++

Use a função ExecutePartitionedDml() para executar uma instrução DML particionada.

void DmlPartitionedUpdate(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("UPDATE Albums SET MarketingBudget = 100000"
                            "  WHERE SingerId > 1"));
  if (!result) throw std::move(result).status();
  std::cout << "Updated at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_update]\n";
}

C#

Use o método ExecutePartitionedUpdateAsync() para executar uma instrução DML particionada.


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

public class UpdateUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> UpdateUsingPartitionedDmlCoreAsync(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();

        using var cmd = connection.CreateDmlCommand("UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) updated...");
        return rowCount;
    }
}

Go

Use o método PartitionedUpdate() para executar uma instrução DML particionada.


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func updateUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "%d record(s) updated.\n", rowCount)
	return nil
}

Java

Use o método executePartitionedUpdate() para executar uma instrução DML particionada.

static void updateUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records updated.\n", rowCount);
}

Node.js

Use o método runPartitionedUpdate() para executar uma instrução DML particionada.

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1',
  });
  console.log(`Successfully updated ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

Use o método executePartitionedUpdate() para executar uma instrução DML particionada.

use Google\Cloud\Spanner\SpannerClient;

/**
 * Updates sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1'
    );

    printf('Updated %d row(s).' . PHP_EOL, $rowCount);
}

Python

Use o método execute_partitioned_dml() para executar uma instrução DML particionada.

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml(
    "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

print("{} records updated.".format(row_ct))

Ruby

Use o método execute_partitioned_update() para executar uma instrução DML particionada.

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

puts "#{row_count} records updated."

O exemplo de código a seguir exclui linhas da tabela Singers com base na coluna SingerId.

C++

void DmlPartitionedDelete(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("DELETE FROM Singers WHERE SingerId > 10"));
  if (!result) throw std::move(result).status();
  std::cout << "Deleted at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_delete]\n";
}

C#


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

public class DeleteUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> DeleteUsingPartitionedDmlCoreAsync(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();

        using var cmd = connection.CreateDmlCommand("DELETE FROM Singers WHERE SingerId > 10");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) deleted...");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func deleteUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "DELETE FROM Singers WHERE SingerId > 10"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err

	}
	fmt.Fprintf(w, "%d record(s) deleted.", rowCount)
	return nil
}

Java

static void deleteUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "DELETE FROM Singers WHERE SingerId > 10";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records deleted.\n", rowCount);
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'DELETE FROM Singers WHERE SingerId > 10',
  });
  console.log(`Successfully deleted ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Delete sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function delete_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'DELETE FROM Singers WHERE SingerId > 10'
    );

    printf('Deleted %d row(s).' . PHP_EOL, $rowCount);
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml("DELETE FROM Singers WHERE SingerId > 10")

print("{} record(s) deleted.".format(row_ct))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "DELETE FROM Singers WHERE SingerId > 10"
)

puts "#{row_count} records deleted."