Transações

Introdução

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

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

  • Bloqueio de leitura e gravação. Esse tipo de transação é o único compatível com a gravação de dados no Cloud Spanner. Essas transações dependem de bloqueio pessimista e, se necessário, efetivação em duas fases. O bloqueio das 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. É possível configurar as transações de somente leitura para que sejam lidas com carimbo de data/hora no passado. Elas não precisam ser confirmadas e não são bloqueadas.

  • DML particionada. Esse tipo de transação executa uma declaraçã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 limpezas e preenchimentos periódicos.

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

Transações de leitura e gravação

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

  • 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 subtrair US$ 200 de A e outra para adicionar US$ 200 a B) e as leituras dos saldos das contas iniciais 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.

Este é um cenário no qual você não deve usar uma transação de bloqueio de leitura e gravação:

  • Se você está apenas fazendo leituras e pode expressar a leitura 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 Cloud 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? Essas transações oferecem as propriedades ACID dos bancos de dados relacionais. Na realidade, as transações de leitura e gravação do Cloud Spanner oferecem garantias ainda mais fortes do que o ACID tradicional. Consulte a seção Semântica abaixo.

Isolamento

Transações que leem e gravam

Estas são as propriedades de isolamento que você consegue para uma transação de leitura e gravação que contém uma série de leituras e gravações:

  • Todas as leituras dentro dessa transação retornam dados do mesmo carimbo de data/hora.
  • Se uma transação foi confirmada com êxito, nenhuma outra gravação modificou os dados que foram lidos na transação depois que a leitura foi feita.
  • Essas propriedades são mantidas mesmo para leituras que não retornaram linhas e as lacunas entre as linhas retornadas pelas leituras do intervalo: a inexistência de linhas conta como dados.
  • Todas as gravações dentro dessa transação são confirmadas no mesmo carimbo de data/hora.
  • Todas as gravações dentro dessa transação só são visíveis após a efetivação da transação.

O efeito é que todas as leituras e gravações parecem ter ocorrido em um único ponto no tempo, tanto da perspectiva da própria transação quanto da perspectiva de outros leitores e gravadores no banco de dados do Cloud 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ê ler dados e depois confirmar na 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ê quiser 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 Cloud Spanner fornece atomicidade (se qualquer uma das gravações de 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 (dados confirmados permanecem confirmados).

Benefícios dessas propriedades

Devido a essas propriedades, como desenvolvedor de aplicativos, você pode se concentrar na correção de cada transação por conta própria, sem se preocupar em como proteger a execução dela contra outras transações que podem ser executadas ao mesmo tempo.

Interface

O Cloud Spanner oferece uma interface para executar um conjunto de trabalho no contexto de uma transação de leitura e gravação, com tentativas para o cancelamento da transação. Veja um pouco do contexto para explicar este ponto: pode ser necessário testar uma transação do Cloud Spanner várias vezes antes da confirmação. Por exemplo, se duas transações tentam trabalhar em dados ao mesmo tempo de uma maneira que possa causar impasse, o Cloud Spanner anula uma delas para que a outra possa progredir. Mais raramente, eventos temporários no Cloud 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, as transações devem ser executadas com novas tentativas até que tenham sucesso.

Quando você usa uma transação na Cloud Spanner API, define o conjunto de uma transação (ou seja, lê e grava para executar em uma ou mais tabelas em um banco de dados) na forma de um objeto de função. Em segundo plano, o Cloud Spanner executa a função repetidamente até a confirmação da transação ou até que seja encontrado um erro que não possa ser repetido.

Exemplo

Imagine que você adicionou uma coluna MarketingBudget à tabela Albums, exibida na página "Esquema e modelo de dados":

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) (se o orçamento desse álbum for de pelo menos US$ 300.000) para Albums (1, 1). Use uma transação de bloqueio de leitura e gravação para essa operação, já que a transação pode fazer gravações, dependendo do resultado de uma leitura.

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

C#

public static async Task 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 addColumn and writeDataToNewColumn 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 minimumAmountToTransfer = 300000;
        decimal secondBudget = 0;
        decimal firstBudget = 0;

        // Create connection to Cloud Spanner.
        using (var connection =
            new SpannerConnection(connectionString))
        {
            // Create statement to select the second album's data.
            var cmdLookup = connection.CreateSelectCommand(
            "SELECT * FROM Albums WHERE SingerId = 2 AND AlbumId = 2");
            // Excecute the select query.
            using (var reader = await cmdLookup.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    // Read the second album's budget.
                    secondBudget =
                      reader.GetFieldValue<decimal>("MarketingBudget");
                    // Confirm second Album's budget is sufficient and
                    // if not raise an exception. Raising an exception
                    // will automatically roll back the transaction.
                    if (secondBudget < minimumAmountToTransfer)
                    {
                        throw new Exception("The second album's "
                            + $"budget {secondBudget} "
                            + "is less than the minimum required "
                            + "amount to transfer.");
                    }
                }
            }
            // Read the first album's budget.
            cmdLookup = connection.CreateSelectCommand(
            "SELECT * FROM Albums WHERE SingerId = 1 and AlbumId = 1");
            using (var reader = await cmdLookup.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    firstBudget =
                      reader.GetFieldValue<decimal>("MarketingBudget");
                }
            }

            // Specify update command parameters.
            var cmd = connection.CreateUpdateCommand("Albums",
                new SpannerParameterCollection {
                {"SingerId", SpannerDbType.Int64},
                {"AlbumId", SpannerDbType.Int64},
                {"MarketingBudget", SpannerDbType.Int64},
            });
            // Update second album to remove the transfer amount.
            secondBudget -= transferAmount;
            cmd.Parameters["SingerId"].Value = 2;
            cmd.Parameters["AlbumId"].Value = 2;
            cmd.Parameters["MarketingBudget"].Value = secondBudget;
            await cmd.ExecuteNonQueryAsync();
            // Update first album to add the transfer amount.
            firstBudget += transferAmount;
            cmd.Parameters["SingerId"].Value = 1;
            cmd.Parameters["AlbumId"].Value = 1;
            cmd.Parameters["MarketingBudget"].Value = firstBudget;
            await cmd.ExecuteNonQueryAsync();
            scope.Complete();
            Console.WriteLine("Transaction complete.");
        }
    }
}

Go

func writeWithTransaction(ctx context.Context, w io.Writer, client *spanner.Client) error {
	_, 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
		}
		if album2Budget >= 300000 {
			album1Budget, err := getBudget(spanner.Key{1, 1})
			if err != nil {
				return err
			}
			const transfer = 200000
			album1Budget += transfer
			album2Budget -= transfer
			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}),
			})
		}
		return nil
	})
	return err
}

Java

static void writeWithTransaction(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Override
            public Void run(TransactionContext transaction) throws Exception {
              // 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.
              if (album2Budget >= 300000) {
                long album1Budget =
                    transaction
                        .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget"))
                        .getLong(0);
                long transfer = 200000;
                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. 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;
const minimumAmountToTransfer = 300000;

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 sufficient
      if (secondBudget < minimumAmountToTransfer) {
        throw new Error(
          `The second album's budget (${secondBudget}) is less than the minimum required amount to transfer.`
        );
      }
    }),

    // 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(() => {
      // Transfer the budgets between the albums
      console.log(firstBudget, secondBudget);
      firstBudget += transferAmount;
      secondBudget -= transferAmount;

      console.log(firstBudget, secondBudget);

      // Update 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(() => {
      // Close 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` 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($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $database->runTransaction(function (Transaction $t) use ($spanner) {
        // 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 < 300000) {
            // Throwing an exception will automatically roll back the transaction.
            throw new UnexpectedValueException(
                'The second album doesn\'t have enough funds to transfer'
            );
        }

        $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.
        $transferAmmount = 200000;
        $secondAlbumBudget -= $transferAmmount;
        $firstAlbumBudget += $transferAmmount;
        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 < 300000:
            # 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

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

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

  new_first_album_budget  = first_album[:MarketingBudget]  + 200_000
  new_second_album_budget = second_album[:MarketingBudget] - 200_000

  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 Cloud 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 Cloud Spanner atribui carimbos de data/hora de confirmação que refletem a ordem das transações confirmadas para implementar essa propriedade. Na verdade, o Cloud 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 delas, e eles refletem o tempo real para que você possa compará-los ao horário no 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:

Cronograma 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 depois faz a confirmação. A transação Txn2 em verde começa após Txn1, lê alguns dados B e depois lê os dados A. Como Txn2 lê o valor de A depois que Txn1 confirma a gravação dele em A, Txn2 vê o efeito da gravação de Txn1 em A, por mais que Txn2 tenha começado antes da conclusão de Txn1.

Mesmo que haja algumas sobreposições 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 de Txn2 também parecem ter ocorrido em um único ponto de tempo (c2). Além disso, c1 < c2 (que é garantido devido às gravações confirmadas de Txn1 e Txn2, 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 é levado em consideração: 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.

As leituras da transação não veem as mesmas gravações da transação. É importante observar que as leituras não veem as gravações que são armazenadas em buffer na mesma transação. As gravações são armazenadas em buffer até o fim da transação e não são visíveis para nenhuma leitura até a confirmação da transação. Isso ocorre porque, quando você armazena mutações em buffer, elas são salvas localmente no cliente e não são enviadas ao servidor até o momento da confirmação. As leituras, no entanto, são enviadas diretamente ao servidor.

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 existentes dentro da transação.

No entanto, mesmo se você tentar cancelar a leitura, o Cloud Spanner não garante que isso aconteça. Depois de solicitar o cancelamento de uma leitura, ela ainda pode ser concluída com sucesso 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á no cancelamento da transação, a menos que a transação já tenha sido confirmada ou tenha falhado por outro motivo.

Desempenho

Bloqueio

O Cloud 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 Cloud 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 Cloud Spanner adquire bloqueios compartilhados de leitura. Isso permite que outras leituras continuem acessando os dados até a transação estar pronta para efetivação. No momento da efetivaçã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 efetivação de cada gravação determina a ordem na qual ele é aplicado ao banco de dados. Uma consequência disso é que o Cloud Spanner só precisa fazer o upgrade para um bloqueio exclusivo se você leu os dados que está gravando. Caso contrário, o Cloud Spanner usa um bloqueio compartilhado de gravação.

Detecção de impasses

O Cloud Spanner detecta quando várias transações podem estar bloqueadas e obriga o cancelamento de todas as transações, exceto uma. Por exemplo, pense no seguinte cenário: a transação Txn1 conté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 Cloud Spanner usa o algoritmo padrão "wound-wait" para lidar com a detecção de impasse: em segundo plano, o Cloud Spanner acompanha o tempo de cada transação que solicita bloqueios conflitantes e permite que transações mais antigas cancelem transações mais novas. "Mais antiga" aqui significa que a transação começou mais cedo. Ao dar prioridade às transações mais antigas, o Cloud Spanner garante que cada transação acabe tendo a chance de adquirir bloqueios por se tornar antiga o suficiente para ter maior prioridade do que outras transações. Por exemplo, uma transação que contém um bloqueio compartilhado de leitura pode ser cancelada por uma transação mais antiga que quer um bloqueio compartilhado de gravação.

Transações distribuídas

O Cloud Spanner é avançado o bastante para fazer transações distribuídas, que têm contato com muitas partes do banco de dados, mesmo que estejam em servidores diferentes. Fundamentalmente, esse avanço acaba reduzindo o desempenho em comparação com transações de local único.

Quais são as diretrizes gerais de distribuição de uma transação? Em segundo plano, o Cloud 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 atendidas pelo mesmo servidor, assim como duas linhas na mesma tabela com as chaves próximas. O Cloud Spanner pode executar transações em linhas em diferentes servidores. No entanto, como uma regra geral, as transações que afetam muitas linhas colocalizadas são mais rápidas e mais baratas do que as transações que afetam muitas linhas espalhadas por todo o banco de dados ou em toda uma tabela grande.

Outras otimizações de desempenho a serem levadas em consideração: confira se cada transação contém apenas as leituras e gravações que devem ser aplicadas atomicamente e se não incluem as que podem ser aplicadas com uma única chamada de leitura ou uma gravação, ou em transação separada.

Transações somente leitura

Além de bloquear transações de leitura e gravação, o Cloud 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 Cloud Spanner, use esse método. O desempenho da utilização de uma chamada de leitura única deve ser comparável ao de uma única leitura feita em uma transação somente leitura.

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 observam 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 Cloud Spanner executa um conjunto de leituras em um único ponto lógico no tempo, tanto da perspectiva da transação somente leitura quanto do ponto de vista de outros leitores e gravadores no banco de dados do Cloud Spanner. Isso significa que as transações somente leitura observam sempre um estado consistente do banco de dados em um ponto escolhido no histórico das transações.

Interface

O Cloud 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 de somente leitura para receber dados consistentes para duas leituras no mesmo carimbo de data/hora:

C#

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Gets a transaction object that captures the database state
// at a specific point in time.
using (TransactionScope scope = new TransactionScope(
    TransactionScopeAsyncFlowOption.Enabled))
{
    // Create connection to Cloud Spanner.
    using (var connection = new SpannerConnection(connectionString))
    {
        // Open the connection, making the implicitly created
        // transaction read only when it connects to the outer
        // transaction scope.
        await connection.OpenAsReadOnlyAsync()
            .ConfigureAwait(false);
        var cmd = connection.CreateSelectCommand(
            "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
        // Read #1.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : "
                    + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : "
                    + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : "
                    + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }
        // Read #2. Even if changes occur in-between the reads,
        // the transaction ensures that Read #1 and Read #2
        // return the same data.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : "
                    + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : "
                    + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : "
                    + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }
    }
    scope.Complete();
    Console.WriteLine("Transaction complete.");
}

Go

func readOnlyTransaction(ctx context.Context, w io.Writer, client *spanner.Client) error {
	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));
    }
    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($instanceId, $databaseId)
{
    $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(u'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(u'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), é possível executar instruções UPDATE e DELETE em larga escala sem ultrapassar os limites de transação nem bloquear uma tabela inteira. O Cloud Spanner particiona o espaço de chaves e executa as Declarações DML em cada partição em uma transação de leitura e gravação individual.

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

Propriedades

É possível executar apenas uma Declaração DML particionada por vez, esteja você usando um método de biblioteca de cliente ou a ferramenta de linha de comando gcloud.

As transações particionadas não são compatíveis com confirmação ou reversão. O Cloud Spanner executa e aplica a Declaração DML imediatamente. Se a operação for cancelada ou falhar, o Cloud Spanner cancela todas as partições em execução e não inicia as partições restantes. O Cloud Spanner não reverte quaisquer partições que já tenham sido executadas.

Interface

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

Exemplos

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

C#

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

public static async Task UpdateUsingPartitionedDmlCoreAsync(
    string projectId,
    string instanceId,
    string databaseId)
{
    string connectionString =
        $"Data Source=projects/{projectId}/instances/{instanceId}"
        + $"/databases/{databaseId}";

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

        SpannerCommand cmd = connection.CreateDmlCommand(
            "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
        );
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();
        Console.WriteLine($"{rowCount} row(s) updated...");
    }
}

Go

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

func updateUsingPartitionedDML(ctx context.Context, w io.Writer, client *spanner.Client) error {
	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 declaraçã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 declaraçã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 declaraçã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($instanceId, $databaseId)
{
    $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 declaraçã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 declaraçã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#

public static async Task DeleteUsingPartitionedDmlCoreAsync(
    string projectId,
    string instanceId,
    string databaseId)
{
    string connectionString =
        $"Data Source=projects/{projectId}/instances/{instanceId}"
        + $"/databases/{databaseId}";

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

        SpannerCommand cmd = connection.CreateDmlCommand(
            "DELETE Singers WHERE SingerId > 10"
        );
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();
        Console.WriteLine($"{rowCount} row(s) deleted...");
    }
}

Go

func deleteUsingPartitionedDML(ctx context.Context, w io.Writer, client *spanner.Client) error {
	stmt := spanner.Statement{SQL: "DELETE 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 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($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        "DELETE 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 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."
Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Documentação do Cloud Spanner