Transacciones

Introducción

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

Cloud Spanner es compatible con los siguientes modos de transacción:

  • Bloqueo de lectura y escritura. Este tipo de transacción es el único que permite escribir datos en Cloud Spanner. Estas transacciones dependen del bloqueo pesimista y, si es necesario, la confirmación en dos fases. Es posible que las transacciones de bloqueo de lectura y escritura se anulen, 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. Las transacciones de solo lectura se pueden configurar para leer marcas de tiempo en el pasado. Las transacciones de solo lectura no necesitan confirmación ni aceptan bloqueos.

  • 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.

En esta página, se describen la semántica y las propiedades generales de las transacciones en Cloud Spanner y se presentan las interfaces de lectura y escritura, solo lectura y DML particionado en Cloud 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 operación de escritura que depende del resultado de una o más operaciones de lectura, debes leer y escribir 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 operaciones de escritura según los resultados de una o más operaciones de lectura, deberías hacer esas escrituras y lecturas en la misma transacción de lectura y escritura, incluso si las escrituras 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 Cloud Spanner ejecuta de forma atómica un conjunto de operaciones de lectura y escritura en un único 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 de ACID de las bases de datos relacionales (de hecho, las transacciones de lectura y escritura de Cloud Spanner ofrecen garantías aún más sólidas que el ACID tradicional; consulta la sección Semántica que se encuentra a continuación).

Aislamiento

Transacciones que leen y escriben

Estas son las propiedades de aislamiento que se obtienen para una transacción de lectura y escritura que contiene una serie de operaciones de lectura y escritura:

  • Todas las lecturas dentro de esa transacción muestran datos de la misma marca de tiempo.
  • Si una transacción se confirma de manera correcta, entonces ningún otro escritor modificó los datos que se leyeron en la transacción después de su lectura.
  • Estas propiedades se conservan incluso para las operaciones de lectura que no mostraron filas y las brechas entre filas que muestran los rangos de lecturas: la inexistencia de filas cuenta como datos.
  • Todas las escrituras dentro de esa transacción se confirman en la misma marca de tiempo.
  • Todas las escrituras dentro de esa transacción solo son visibles después de que la transacción se confirma.

El efecto es que todas las operaciones de lectura y escritura parecen haber ocurrido en un momento determinado, desde la perspectiva de la transacción y desde la perspectiva de otros lectores y escritores en la base de datos de Cloud 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, Cloud Spanner proporciona atomicidad (si una de las operaciones de escritura en la transacción se confirma, todas se confirman), coherencia (la base de datos permanece en un estado coherente después de la transacción) y durabilidad (la base de datos confirmada permanece así).

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

Cloud Spanner proporciona una interfaz a fin de ejecutar un cuerpo de trabajo en el contexto de una transacción de lectura y escritura, con reintentos para anulaciones de transacciones. Aquí se proporciona un poco de contexto para explicar este punto: una transacción de Cloud Spanner se debe probar varias veces antes de que se confirme. Por ejemplo, si dos transacciones intentan trabajar con datos al mismo tiempo de una manera que podría causar un interbloqueo, Cloud Spanner anula una de ellas para que la otra transacción pueda progresar. (En pocas ocasiones, los eventos temporales dentro de Cloud 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 la API de Cloud 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 en una base de datos) en formato de un objeto de función. De forma interna, Cloud Spanner ejecuta la función de manera reiterada hasta que la transacción se confirma o se produce un error irreproducible.

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#

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 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 < transferAmount)
                    {
                        throw new Exception("The second album's "
                            + $"budget {secondBudget} "
                            + "is less than the "
                            + "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
		}
		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(
          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.
              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($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

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

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

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

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

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

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

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

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

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

Python

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

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

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

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

        transfer_amount = 200000

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

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

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

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

    database.run_in_transaction(update_albums)

    print('Transaction complete.')

Ruby

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

require "google/cloud/spanner"

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

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

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

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

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

puts "Transaction complete"

Semántica

Serialización y coherencia externa

Cloud Spanner proporciona “serialización”, lo que significa que todas las transacciones aparecen como si se ejecutaran en serie, aunque algunas de las lecturas, escrituras y otras operaciones de transacciones distintas ocurrieran en paralelo. Cloud Spanner asigna marcas de tiempo de confirmación que reflejan el orden de las transacciones confirmadas para implementar esta propiedad. De hecho, Cloud 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 las marcas de tiempo de confirmación y estas, a su vez, reflejan la hora real para que puedas compararlas con el 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 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 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 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.

Las lecturas de una transacción no ven las escrituras de la misma transacción. Es importante tener en cuenta que las lecturas no ven las escrituras almacenadas en búfer en la misma transacción. Las escrituras se almacenan en búfer hasta el final de la transacción y no son visibles para ninguna lectura hasta que se confirma la transacción. Esto se debe a que cuando almacenas en búfer las mutaciones, estas se guardan de forma local en tu cliente y no se envían al servidor hasta el momento de la confirmación. Sin embargo, las lecturas se envían directamente al servidor.

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, Cloud Spanner no garantiza que se haya cancelado realmente. 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

Cloud Spanner permite que varios clientes interactúen en simultáneo con la misma base de datos. Para garantizar la coherencia de varias transacciones simultáneas, Cloud Spanner usa una combinación de bloqueos compartidos y exclusivos con el fin de controlar el acceso a los datos. Cuando realizas una lectura como parte de una transacción, Cloud 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 Cloud Spanner solo necesita actualizarse a un bloqueo exclusivo si leíste los datos que estás escribiendo. De lo contrario, Cloud Spanner usa un bloqueo compartido llamado bloqueo compartido de escritura.

Detección de interbloqueo

Cloud Spanner detecta cuándo varias transacciones podrían estar interbloqueadas y fuerza la anulación de todas excepto 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.

Cloud Spanner usa el algoritmo “de prevención” estándar para controlar la detección de interbloqueo. De forma interna, Cloud Spanner realiza un seguimiento de la antigüedad de cada transacción que solicite 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).

Debido a que Cloud Spanner da prioridad a las transacciones más antiguas, garantiza que todas las transacciones tendrán la posibilidad de adquirir bloqueos en algún momento, una vez que tengan la antigüedad suficiente para tener mayor prioridad 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.

Transacciones distribuidas

Cloud Spanner puede realizar transacciones distribuidas, que son transacciones que involucran muchas partes de la base de datos, incluso si están en servidores diferentes. Esta capacidad tiene un costo de rendimiento en comparación con las transacciones de un solo sitio.

¿Qué tipos de transacciones se pueden distribuir? De forma interna, Cloud 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. Cloud Spanner puede realizar transacciones entre filas en servidores diferentes. Sin embargo, como regla general, las transacciones que afectan muchas filas en la misma ubicación 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 eficaces en Cloud Spanner incluyen solo las operaciones de lectura y escritura que se deben aplicar de manera 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 las transacciones de bloqueo de lectura y escritura, Cloud 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 con uno de los métodos de lectura única de Cloud 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.

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 Cloud Spanner ejecuta un conjunto de lecturas en un único momento lógico, desde la perspectiva de la transacción de solo lectura y desde la de otros lectores y escritores de la base de datos de Cloud 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

Cloud 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#

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));
    }
    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($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

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. Cloud Spanner particiona el espacio de clave 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 la herramienta de línea de comandos de gcloud.

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

Interfaz

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

Ejemplos

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

C#

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

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

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($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

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

Ruby

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

Con el siguiente ejemplo de código, se borran filas de la tabla Singers en función de la columna 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."