Descripción general de las transacciones

En esta página, se explican las transacciones en Spanner y se incluye un código de muestra para ejecutar transacciones.

Introducción

Una transacción en Spanner es un conjunto de operaciones de lectura y escritura que se ejecutan de forma atómica en un único momento lógico en las columnas, las filas y las tablas de una base de datos.

Spanner admite los siguientes modos de transacción:

  • Bloqueo de lectura y escritura. Estas transacciones dependen del bloqueo pesimista y, si es necesario, la confirmación en dos fases. El bloqueo de transacciones de lectura y escritura puede anular, lo que requiere que la aplicación vuelva a intentarlo.

  • Solo lectura. Este tipo de transacción proporciona coherencia garantizada en varias operaciones de lectura, pero no permite operaciones de escritura. De forma predeterminada, las transacciones de solo lectura se ejecutan en una marca de tiempo elegida por el sistema que garantiza la coherencia externa, pero también se pueden configurar para que se lean en una marca de tiempo del pasado. Las transacciones de solo lectura no necesitan confirmarse ni aceptan bloqueos. Además, es posible que las transacciones de solo lectura esperen a que se completen las operaciones de escritura en curso antes de ejecutarse.

  • DML particionado. Este tipo de transacción ejecuta una declaración de lenguaje de manipulación de datos (DML) como DML particionado. El DML particionado está diseñado para actualizaciones y eliminaciones masivas, en especial, la limpieza y el reabastecimiento periódicos. Si necesitas confirmar una gran cantidad de escrituras ocultas, pero no necesitas una transacción atómica, puedes modificar de forma masiva las tablas de Spanner con la escritura por lotes. Para obtener más información, consulta Modifica datos mediante escrituras por lotes.

En esta página, se describen la semántica y las propiedades generales de las transacciones en Spanner y se presentan las interfaces de lectura y escritura, solo lectura y DML particionado en Spanner.

Transacciones de lectura y escritura

Estas son situaciones en las que deberías usar una transacción de bloqueo de lectura y escritura:

  • Si realizas una escritura que depende del resultado de una o más operaciones de lectura, debes realizar esa escritura y las operaciones de lectura en la misma transacción de lectura y escritura.
    • Ejemplo: duplica el saldo de la cuenta bancaria A. La lectura del saldo de la cuenta A debe estar en la misma transacción que la escritura para reemplazar el saldo con el valor duplicado.

  • Si realizas una o más operaciones de escritura que deben confirmarse de manera atómica, debes realizarlas en la misma transacción de lectura y escritura.
    • Ejemplo: transfiere $200 de la cuenta A a la cuenta B. Las dos operaciones de escritura (una para disminuir A por $200 y otra para aumentar B por $200) y las operaciones de lectura de los saldos de cuenta iniciales deben estar en la misma transacción.

  • Si puedes realizar una o más escrituras, según los resultados de una o más operaciones de lectura, debes realizarlas en la misma transacción de lectura y escritura, incluso si las operaciones de escritura no se ejecutan.
    • Ejemplo: transfiere $200 de la cuenta bancaria A a la cuenta bancaria B si el saldo actual de A es superior a $500. Tu transacción debe contener una lectura del saldo de A y una declaración condicional que contenga las escrituras.

Esta es una situación en la que no deberías usar una transacción de bloqueo de lectura y escritura:

  • Si solo realizas lecturas y puedes expresarlas con un método de lectura única, debes usar ese método o una transacción de solo lectura. Las lecturas únicas no se bloquean, a diferencia de las transacciones de lectura y escritura.

Propiedades

Una transacción de lectura y escritura en Spanner ejecuta un conjunto de operaciones de lectura y escritura de forma atómica en un solo momento lógico. Además, la marca de tiempo en la que se ejecutan las transacciones de lectura y escritura coincide con el tiempo real, y el orden de serialización coincide con el orden de la marca de tiempo.

¿Por qué usar una transacción de lectura y escritura? Las transacciones de lectura y escritura proporcionan las propiedades ACID de las bases de datos relacionales (de hecho, las transacciones de lectura y escritura de Spanner ofrecen garantías aún más sólidas que el ACID tradicional; consulta la sección Semántica a continuación).

Aislamiento

Las siguientes son propiedades de aislamiento para transacciones de lectura y escritura y de solo lectura.

Transacciones que leen y escriben

Estas son las propiedades de aislamiento que obtienes después de confirmar de forma correcta una transacción que contiene una serie de lecturas (o consultas) y escrituras:

  • Todas las lecturas dentro de la transacción mostraron valores que reflejan una instantánea coherente tomada en la marca de tiempo de confirmación de la transacción.
  • Las filas o rangos vacíos permanecían así en el momento de la confirmación.
  • Todas las escrituras dentro de la transacción se confirmaron en la marca de tiempo de confirmación de la transacción.
  • Las escrituras no fueron visibles para ninguna transacción hasta después de que se confirmó la transacción.

Ciertos controladores de cliente de Spanner contienen una lógica de reintento de transacción para enmascarar los errores transitorios, que se llevan a cabo cuando se vuelve a ejecutar la transacción y se validan los datos observados por el cliente.

El efecto es que todas las operaciones de lectura y escritura parecen haberse producido en un solo punto en el tiempo, tanto desde la perspectiva de la transacción como desde la de otros lectores y escritores de la base de datos de Spanner. En otras palabras, las operaciones de lectura y las escritura ocurren en la misma marca de tiempo (consulta una ilustración de esto en la sección Serialización y coherencia externa que se encuentra a continuación).

Transacciones que solo leen

Las garantías para una transacción de lectura y escritura que solo lee son similares: todas las operaciones de lectura dentro de esa transacción muestran datos de la misma marca de tiempo, incluso para la inexistencia de filas. Una diferencia es que, si lees datos y, luego, confirmas la transacción de lectura y escritura sin ninguna escritura, no hay garantía de que los datos no hayan cambiado en la base de datos después de la lectura y antes de la confirmación. Si deseas saber si los datos cambiaron desde que los leíste por última vez, el mejor enfoque es volver a leerlos (ya sea en una transacción de lectura y escritura o con una lectura sólida). Además, para obtener mayor eficiencia, si ya sabes que solo leerás y no escribirás, deberías usar una transacción de solo lectura en lugar de una de lectura y escritura.

Atomicidad, coherencia y durabilidad

Además de la propiedad de aislamiento, Spanner proporciona atomicidad (si se confirma alguna de las escrituras en la transacción, todas se confirman), coherencia (la base de datos permanece en un estado coherente después de la transacción) y durabilidad (los datos confirmados permanecen confirmados).

Beneficios de estas propiedades

Debido a estas propiedades, como desarrollador de aplicaciones, puedes enfocarte en la precisión de cada transacción, sin preocuparte por proteger su ejecución de otras transacciones que podrían ejecutarse al mismo tiempo.

Interfaz

Las bibliotecas cliente de Spanner proporcionan una interfaz para ejecutar un cuerpo de trabajo en el contexto de una transacción de lectura y escritura, con reintentos para anulaciones de transacciones. A continuación, se brinda un poco de contexto para explicar este punto: es posible que se deba probar una transacción de Spanner varias veces antes de que se confirme. Por ejemplo, si dos transacciones intentan trabajar en datos al mismo tiempo de una manera que podría causar un interbloqueo, Spanner anula una de ellas para que la otra transacción pueda progresar. (En pocas ocasiones, los eventos transitorios en Spanner pueden provocar la anulación de algunas transacciones). Dado que las transacciones son atómicas, una transacción anulada no tiene efecto visible en la base de datos. Por lo tanto, se deben reintentar las ejecuciones de las transacciones hasta que tengan éxito.

Cuando usas una transacción en una biblioteca cliente de Spanner, debes definir el cuerpo de la transacción (es decir, las operaciones de lectura y escritura que se realizarán en una o más tablas de una base de datos) en forma de un objeto de función. De forma interna, la biblioteca cliente de Spanner ejecuta la función de forma repetida hasta que se confirma la transacción o se encuentra un error que no se puede reintentar.

Ejemplo

Supongamos que agregaste una columna MarketingBudget a la tabla Albums que se muestra en la página Modelo de datos y esquema:

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

El departamento de marketing decide lanzar una campaña para el álbum con clave Albums (1, 1) y te pide que transfieras $200,000 del presupuesto de Albums (2, 2), pero solo si el dinero está disponible en el presupuesto de ese álbum. Debes usar una transacción de bloqueo de lectura y escritura para esta operación, ya que la transacción puede realizar operaciones de escritura según el resultado de una lectura.

A continuación, se muestra cómo ejecutar una transacción de lectura y escritura:

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.")

Rita

# 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

Serialización y coherencia externa

Spanner proporciona “serialización”, lo que significa que todas las transacciones aparecen como si se ejecutaran en serie, incluso si algunas de las lecturas, escrituras y otras operaciones de transacciones distintas ocurrieron en paralelo. Spanner asigna marcas de tiempo de confirmación que reflejan el orden de las transacciones confirmadas para implementar esta propiedad. De hecho, Spanner ofrece una garantía más sólida que la serialización llamada coherencia externa: las transacciones se confirman en un orden que se refleja en sus marcas de tiempo de confirmación, las cuales reflejan el tiempo real para que puedas compararlas con tu reloj. Las operaciones de lectura en una transacción ven todo lo que se confirmó antes de la confirmación de la transacción, y todo lo que se inicie después de confirmar la transacción puede visualizar las operaciones de escritura.

Por ejemplo, considera la ejecución de dos transacciones como las que aparecen en el siguiente diagrama:

cronograma en el que se muestra la ejecución de dos transacciones que leen los mismos datos

La transacción Txn1 en azul lee algunos datos A, almacena en búfer una escritura en A y, luego, la confirma de forma correcta. La transacción Txn2 en verde comienza después de Txn1, lee algunos datos B y, luego, lee los datos A. Dado que Txn2 lee el valor de A después de que Txn1 confirmó su escritura en A, Txn2 ve el efecto de la escritura de Txn1 en A, aunque Txn2 comenzó antes de que Txn1 se complete.

A pesar de que hay una superposición en el momento en el que Txn1 y Txn2 están en ejecución, sus marcas de tiempo de confirmación c1 y c2 respetan un orden de transacción lineal. Esto significa que todos los efectos de las operaciones de lectura y escritura de Txn1 parecen haber ocurrido en un momento determinado (c1) y todos los efectos de las operaciones de lectura y escritura de Txn2 también parecen haber ocurrido en un momento determinado (c2). Además, sucede esto c1 < c2 (está garantizado porque Txn1 y Txn2 confirmaron las lecturas; esto es así incluso si las escrituras se realizaron en máquinas diferentes), lo que respeta el orden de que Txn1 se haya producido antes que Txn2. (Sin embargo, si Txn2 solo realizó operaciones de lectura en la transacción, entonces sucede esto c1 <= c2).

Las lecturas observan un prefijo del historial de confirmaciones; si una lectura ve el efecto de Txn2, también ve el de Txn1. Todas las transacciones que se confirman de forma correcta tienen esta propiedad.

Garantías de lectura y escritura

Si una llamada para ejecutar una transacción falla, las garantías de lectura y escritura que tendrás dependerán del error con el que falló la llamada de confirmación subyacente.

Por ejemplo, un error como “No se encontró la fila” o “La fila ya existe” significa que la escritura de las mutaciones almacenadas en búfer encontró algún error; p. ej., no existe una fila que el cliente intenta actualizar. En ese caso, las lecturas tienen coherencia garantizada, las escrituras no se aplican y se garantiza que la inexistencia de la fila también será coherente con las lecturas.

Cancelación de operaciones de transacción

El usuario puede cancelar operaciones de lectura asíncronas en cualquier momento (p. ej., cuando se cancela una operación de nivel superior o si decides detener una lectura en función de los resultados iniciales recibidos de la lectura) sin afectar otras operaciones existentes dentro de la transacción.

Sin embargo, incluso si intentaste cancelar la lectura, Spanner no garantiza que esta se cancele. Después de solicitar la cancelación de una lectura, esta puede completarse de forma correcta o fallar por algún otro motivo (p. ej., anulación). Además, esa lectura cancelada podría mostrarte algunos resultados, y esos resultados posiblemente incompletos se validarán como parte de la confirmación de la transacción.

Ten en cuenta que, a diferencia de las lecturas, cancelar una operación de confirmación de transacción dará como resultado la anulación de la transacción (a menos que la transacción ya se haya confirmado o falle por otro motivo).

Rendimiento

Bloqueo

Spanner permite que varios clientes interactúen de forma simultánea con la misma base de datos. Para garantizar la coherencia de varias transacciones simultáneas, Spanner usa una combinación de bloqueos compartidos y exclusivos a fin de controlar el acceso a los datos. Cuando realizas una lectura como parte de una transacción, Spanner adquiere bloqueos de lectura compartidos, lo que permite que otras lecturas accedan a los datos hasta que la transacción esté lista para confirmarse. Cuando se confirma la transacción, y se aplican las escrituras, la transacción intenta actualizarse a un bloqueo exclusivo. Bloquea nuevos bloqueos de lectura compartidos en los datos, espera a que se borren los bloqueos de lectura compartidos existentes y, luego, establece un bloqueo exclusivo para el acceso exclusivo a los datos.

Notas sobre los bloqueos:

  • Los bloqueos se aplican según el nivel de detalle de la fila y la columna. Si la transacción T1 bloqueó la columna “A” de la fila “foo”, y la transacción T2 desea escribir la columna “B” de la fila “foo”, no se producen problemas.
  • Las operaciones de escritura en un elemento de datos que tampoco leen los datos escritos (también conocidas como “escrituras ocultas”) no entran en conflicto con otras escrituras ocultas del mismo elemento (la marca de tiempo de confirmación de cada escritura determina el orden en el que se aplica a la base de datos). Una consecuencia de esto es que Spanner solo necesita actualizarse a un bloqueo exclusivo si leíste los datos que escribes. De lo contrario, Spanner usa un bloqueo compartido llamado bloqueo compartido de escritor.
  • Cuando realices búsquedas de filas dentro de una transacción de lectura y escritura, usa índices secundarios para limitar las filas analizadas a un rango más pequeño. Esto hace que Spanner bloquee una menor cantidad de filas en la tabla, lo que permite modificaciones simultáneas en las filas fuera del rango.
  • Los bloqueos no deben usarse para garantizar el acceso exclusivo a un recurso fuera de Spanner. Spanner puede anular las transacciones por varios motivos, como cuando se permite que los datos se muevan por los recursos de procesamiento de la instancia. Si se reintenta una transacción, ya sea de forma explícita mediante el código de la aplicación o implícitamente por el código de cliente, como el controlador JDBC de Spanner, solo se garantiza que los bloqueos se retuvieron durante el intento que se confirmó en realidad.

  • Puedes usar la herramienta de introspección Estadísticas de bloqueo para investigar los conflictos de bloqueo en tu base de datos.

Detección de interbloqueo

Spanner detecta cuándo varias transacciones pueden estar interbloqueadas y fuerza la anulación de todas menos una. Por ejemplo, considera la siguiente situación: la transacción Txn1 mantiene un bloqueo en el registro A y espera un bloqueo en el registro B, y Txn2 mantiene un bloqueo en el registro B y espera un bloqueo en el registro A. La única manera de progresar en esta situación es anular una de las transacciones para que quite su bloqueo y permita que la otra continúe.

Spanner usa el algoritmo estándar de “esperar” para controlar la detección de interbloqueos. De forma interna, Spanner realiza un seguimiento de la antigüedad de cada transacción que solicita bloqueos en conflicto. También permite que las transacciones más antiguas anulen las transacciones más recientes (“más antiguas” significa que la lectura, la consulta o la confirmación ocurrieron antes).

Cuando se da prioridad a las transacciones más antiguas, Spanner garantiza que cada transacción tenga la oportunidad de adquirir bloqueos en algún momento, después de que adquiera lo suficientemente antigua como para tener una prioridad más alta que otras transacciones. Por ejemplo, una transacción más antigua que necesita un bloqueo compartido de escritura puede anular una transacción que adquirió un bloqueo compartido de lectura.

Ejecución distribuida

Spanner puede ejecutar transacciones en datos que abarcan varios servidores. Esta capacidad tiene un costo de rendimiento en comparación con las transacciones de un solo servidor.

¿Qué tipos de transacciones se pueden distribuir? De forma interna, Spanner puede dividir la responsabilidad de las filas de la base de datos en muchos servidores. El mismo servidor suele entregar una fila y las filas correspondientes de las tablas intercaladas; lo mismo sucede con dos filas de la misma tabla con claves cercanas. Spanner puede realizar transacciones en varias filas de diferentes servidores. Sin embargo, como regla general, las transacciones que afectan a muchas filas ubicadas en el mismo lugar son más rápidas y económicas que las que afectan muchas filas dispersas en la base de datos o en una tabla grande.

Las transacciones más eficientes en Spanner incluyen solo las operaciones de lectura y escritura que se deben aplicar de forma atómica. Las transacciones son más rápidas cuando todas las operaciones de lectura y escritura acceden a los datos en la misma parte del espacio de claves.

Transacciones de solo lectura

Además de bloquear transacciones de lectura y escritura, Spanner ofrece transacciones de solo lectura.

Usa una transacción de solo lectura cuando necesites ejecutar más de una lectura en la misma marca de tiempo. Si puedes expresar tu lectura mediante uno de los métodos de lectura única de Spanner, debes usar ese método. El rendimiento del uso de una llamada de lectura única debe ser similar al rendimiento de una lectura única realizada en una transacción de solo lectura.

Si lees una gran cantidad de datos, considera usar particiones para leer los datos en paralelo.

Debido a que las transacciones de solo lectura no escriben, no mantienen bloqueos y no bloquean otras transacciones. Las transacciones de solo lectura observan un prefijo coherente del historial de confirmación de transacciones, por lo que la aplicación siempre obtiene datos coherentes.

Propiedades

Una transacción de solo lectura de Spanner ejecuta un conjunto de lecturas en un único punto lógico en el tiempo, tanto desde la perspectiva de la transacción de solo lectura como desde la de otros lectores y escritores de la base de datos de Spanner. Esto significa que las transacciones de solo lectura siempre observan un estado coherente de la base de datos en un punto determinado del historial de transacciones.

Interfaz

Spanner proporciona una interfaz a fin de ejecutar un cuerpo de trabajo en el contexto de una transacción de solo lectura, con reintentos para anulaciones de transacciones.

Ejemplo

A continuación, se muestra cómo usar una transacción de solo lectura con el fin de obtener datos coherentes para dos lecturas en la misma marca de tiempo:

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);

        // Open the connection, making the implicitly created
        // transaction read only when it connects to the outer
        // transaction scope.
        await connection.OpenAsReadOnlyAsync();
        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))

Rita

# 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

Transacciones de DML particionado

Con el lenguaje de manipulación de datos particionado (DML particionado), puedes ejecutar declaraciones UPDATE y DELETE a gran escala sin lidiar con límites de transacciones ni bloquear una tabla completa. Spanner particiona el espacio de claves y ejecuta las declaraciones DML en cada partición en una transacción de lectura y escritura independiente.

Ejecuta declaraciones DML en las transacciones de lectura y escritura que creas de forma explícita en tu código. Para obtener más información, consulta la sección sobre cómo usar DML.

Propiedades

Solo puedes ejecutar una declaración DML particionada a la vez, ya sea que uses un método de biblioteca cliente o Google Cloud CLI.

Las transacciones particionadas no admiten confirmación ni reversión. Spanner ejecuta y aplica la declaración DML de inmediato. Si cancelas la operación o la operación falla, Spanner cancela todas las particiones en ejecución y no inicia ninguna de las particiones restantes. Spanner no revierte ninguna partición que ya se haya ejecutado.

Interfaz

Spanner proporciona una interfaz para ejecutar una sola declaración DML particionada.

Ejemplos

En el siguiente ejemplo de código, se actualiza la columna MarketingBudget de la tabla Albums.

C++

Usa la función ExecutePartitionedDml() para ejecutar una declaración 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#

Usa el método ExecutePartitionedUpdateAsync() para ejecutar una declaración 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

Usa el método PartitionedUpdate() para ejecutar una declaración 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

Usa el método executePartitionedUpdate() para ejecutar una declaración 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

Usa el método runPartitionedUpdate() para ejecutar una declaración 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

Usa el método executePartitionedUpdate() para ejecutar una declaración 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

Usa el método execute_partitioned_dml() para ejecutar una declaración 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))

Rita

Usa el método execute_partitioned_update() para ejecutar una declaración 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."

En el siguiente ejemplo de código, se borran las filas de la tabla Singers según la columna 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))

Rita

# 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."