Présentation des transactions

Cette page décrit les transactions dans Spanner et inclut un exemple de code permettant d'exécuter des transactions.

Présentation

Dans Spanner, une transaction est un ensemble de lectures et d'écritures qui s'exécutent de manière atomique à un même instant logique dans les colonnes, les lignes et les tables d'une base de données.

Spanner est compatible avec les modes de transaction suivants:

  • Verrouillage des lectures-écritures. Ces transactions reposent sur un verrouillage pessimiste et, si nécessaire, sur un commit en deux phases. Le verrouillage des transactions en lecture-écriture peut être annulé, ce qui nécessite une nouvelle tentative de l'application.

  • Lecture seule. Ce type de transaction offre une cohérence garantie pour plusieurs lectures, mais n'autorise pas les écritures. Par défaut, les transactions en lecture seule s'exécutent à un horodatage choisi par le système qui garantit une cohérence externe, mais elles peuvent également être configurées pour lire un horodatage dans le passé. Les transactions en lecture seule n'ont pas besoin d'être validées et ne peuvent pas être verrouillées. De plus, les transactions en lecture seule peuvent attendre la fin des écritures en cours avant de les exécuter.

  • LMD partitionné. Ce type de transaction exécute une instruction LMD (langage de manipulation de données) en tant que LMD partitionné. Le LMD partitionné est conçu pour les mises à jour et les suppressions groupées, en particulier le nettoyage et le remplissage périodiques. Si vous devez effectuer un commit d'un grand nombre d'écritures à l'aveugle sans avoir à effectuer de transaction atomique, vous pouvez modifier vos tables Spanner de manière groupée à l'aide d'une écriture par lot. Pour en savoir plus, consultez la section Modifier des données à l'aide d'écritures par lot.

Cette page décrit les propriétés générales et la sémantique des transactions dans Spanner, et présente les interfaces de transaction en lecture/écriture, en lecture seule et en LMD partitionné dans Spanner.

Transactions en lecture-écriture

Voici des scénarios dans lesquels il est recommandé d'utiliser le verrouillage des transactions en lecture-écriture :

  • Si vous effectuez une écriture qui dépend du résultat d'une ou de plusieurs lectures, effectuez cette écriture et la ou les lectures de la même transaction en lecture-écriture.
    • Exemple: doubler le solde du compte bancaire A. La lecture du solde de A doit avoir lieu dans la même transaction que l'écriture pour remplacer le solde par la valeur doublée.

  • Si vous effectuez une ou plusieurs écritures qui doivent être validées de manière atomique, faites-le dans la même transaction en lecture-écriture.
    • Exemple: Transférer 200 € du compte A vers le compte B. Les deux écritures (une pour diminuer A de 200 $et une pour augmenter B de 200 $) et les lectures du solde initial du compte doivent se faire dans la même transaction.

  • Si vous pouvez effectuer une ou plusieurs écritures, en fonction des résultats d'une ou de plusieurs lectures, effectuez ces écritures et ces lectures dans la même transaction en lecture-écriture, même si les écritures ne sont pas exécutées.
    • Exemple : transférer 200 $ du compte en banque A au compte en banque B si le solde actuel de A est supérieur à 500 $. Votre transaction doit contenir une lecture du solde de A et une instruction conditionnelle contenant les écritures.

Voici un scénario dans lequel vous n'avez pas besoin d'utiliser le verrouillage des transactions en lecture-écriture :

  • Si vous n'effectuez que des lectures et que vous pouvez les exprimer à l'aide d'une méthode de lecture unique, utilisez cette méthode ou une transaction en lecture seule. Les lectures uniques ne se verrouillent pas, contrairement aux transactions en lecture-écriture.

Propriétés

Une transaction en lecture-écriture dans Spanner exécute un ensemble de lectures et d'écritures de manière atomique à un seul moment logique. En outre, l'horodatage d'exécution des transactions en lecture-écriture correspond à l'heure de l'horloge murale, et l'ordre de sérialisation correspond à l'ordre d'horodatage.

Pourquoi utiliser une transaction en lecture-écriture ? Les transactions en lecture-écriture fournissent les propriétés ACID des bases de données relationnelles. En fait, les transactions en lecture-écriture de Spanner offrent des garanties encore plus solides que les transactions ACID traditionnelles (consultez la section Sémantique ci-dessous).

Isolation

Vous trouverez ci-dessous les propriétés d'isolation pour les transactions en lecture-écriture et en lecture seule.

Transactions en lecture et écriture

Voici les propriétés d'isolation que vous obtenez après avoir validé une transaction contenant une série de lectures (ou de requêtes) et d'écritures:

  • Toutes les lectures de la transaction ont renvoyé des valeurs qui reflètent un instantané cohérent pris au moment de l'horodatage de commit de la transaction.
  • Les lignes ou plages vides étaient conservées au moment du commit.
  • Toutes les écritures dans la transaction ont été validées au moment de l'horodatage de commit de la transaction.
  • Les écritures n'étaient visibles par aucune transaction avant le commit de la transaction.

Certains pilotes client Spanner contiennent une logique de nouvelle tentative de transaction pour masquer les erreurs temporaires. Pour ce faire, ils réexécutent la transaction et valident les données observées par le client.

Il en résulte que toutes les lectures et écritures semblent avoir eu lieu à un moment donné, à la fois du point de vue de la transaction elle-même, ainsi que du point de vue des autres lecteurs et rédacteurs de la base de données Spanner. En d'autres termes, les opérations de lecture et d'écriture se produisent au même horodatage (une illustration de ce phénomène se trouve dans la section Sérialisabilité et cohérence externe ci-dessous).

Transactions en lecture seule

Les garanties pour une transaction en lecture-écriture qui ne fait qu'effectuer des lectures sont similaires : toutes les lectures au sein de cette transaction renvoient des données du même horodatage, même pour les lignes inexistantes. Une des principales différences entre les deux types de transactions est que, si vous lisez des données, puis validez la transaction en lecture-écriture sans aucune écriture, rien ne garantit que les données n'aient pas été modifiées dans la base de données après la lecture et avant le commit. Si vous voulez savoir si les données ont été modifiées depuis votre dernière lecture, la meilleure approche consiste à les relire (soit dans une transaction en lecture-écriture, soit en utilisant une lecture forte). De même, pour des raisons d'efficacité, si vous savez à l'avance que vous allez seulement lire et pas écrire, vous devez utiliser une transaction en lecture seule au lieu d'une transaction en lecture-écriture.

Atomicité, cohérence, durabilité

En plus de la propriété Isolation, Spanner fournit l'atomicité (si l'une des écritures de la transaction est validée, elles sont toutes validées), la cohérence (la base de données reste dans un état cohérent après la transaction) et la durabilité (les données validées restent validées).

Avantages de ces propriétés

Ces propriétés aident les développeurs d'applications à se concentrer sur l'exactitude de chaque transaction, sans se soucier de la protection de son exécution par rapport à d'autres transactions pouvant être exécutées simultanément.

Interface

Les bibliothèques clientes Spanner fournissent une interface permettant d'exécuter un ensemble de tâches dans le contexte d'une transaction en lecture/écriture, avec de nouvelles tentatives en cas d'échec de la transaction. Voici un peu de contexte pour expliquer ce point: il se peut qu'une transaction Spanner doive être essayée plusieurs fois avant d'être validée. Par exemple, si deux transactions tentent de travailler sur les données en même temps d'une manière susceptible de provoquer un interblocage, Spanner annule l'une d'entre elles pour que l'autre puisse progresser. (Plus rarement, des événements temporaires dans Spanner peuvent entraîner l'annulation de certaines transactions.) Comme les transactions sont atomiques, une transaction annulée n'a aucun effet visible sur la base de données. Par conséquent, les transactions doivent être réexécutées jusqu'à ce qu'elles aboutissent.

Lorsque vous utilisez une transaction dans une bibliothèque cliente Spanner, vous définissez le corps d'une transaction (c'est-à-dire les lectures et les écritures à effectuer sur une ou plusieurs tables d'une base de données) sous la forme d'un objet fonction. En arrière-plan, la bibliothèque cliente Spanner exécute la fonction de manière répétée jusqu'à ce que la transaction soit validée ou qu'une erreur ne puisse pas faire l'objet d'une nouvelle tentative.

Exemple

Supposons que vous avez ajouté une colonne MarketingBudget à la table Albums affichée sur la page "Schéma et modèle de données" :

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

Votre service marketing décide de lancer une campagne marketing pour l'album correspondant à Albums (1, 1) et vous a demandé de transférer 200 000 $ du budget de Albums (2, 2), mais uniquement si la somme est disponible dans le budget de cet album. Vous devez utiliser le verrouillage des transactions en lecture-écriture pour cette opération, car la transaction peut effectuer des écritures en fonction du résultat de la lecture.

L'exemple suivant montre comment exécuter une transaction en lecture-écriture :

C++

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

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

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

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

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

C#


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

public class ReadWriteWithTransactionAsyncSample
{
    public async Task<int> ReadWriteWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        // This sample transfers 200,000 from the MarketingBudget
        // field of the second Album to the first Album. Make sure to run
        // the Add Column and Write Data To New Column samples first,
        // in that order.

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

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

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

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

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

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

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

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

Go


import (
	"context"
	"fmt"
	"io"

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

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

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

Java

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

Node.js

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

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

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

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

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

const transferAmount = 200000;

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

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

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

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

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

      console.log(firstBudget, secondBudget);

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

PHP

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

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

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

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

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

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

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

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

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

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

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

Python

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

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

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

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

        transfer_amount = 200000

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

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

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

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

    database.run_in_transaction(update_albums)

    print("Transaction complete.")

Ruby

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

require "google/cloud/spanner"

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

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

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

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

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

puts "Transaction complete"

Sémantique

Sérialisabilité et cohérence externe

Spanner offre la sérialisabilité, ce qui signifie que toutes les transactions apparaissent comme si elles s'étaient exécutées dans un ordre sérialisé, même si certaines des lectures, écritures et autres opérations de transactions distinctes se sont effectivement produites en parallèle. Pour implémenter cette propriété, Spanner attribue des horodatages de commit qui reflètent l'ordre des transactions validées. En fait, Spanner offre une garantie plus solide que la sérialisabilité appelée cohérence externe: les transactions sont validées dans un ordre reflété dans leur horodatage de commit, et ces horodatages de commit reflètent le temps réel afin que vous puissiez les comparer à votre montre. Les lectures effectuées au sein d'une transaction voient tout ce qui a été validé avant le commit de la transaction et les écritures sont vues par tout ce qui commence après le commit de la transaction.

Par exemple, considérons l'exécution de deux transactions comme illustré dans le diagramme ci-dessous :

calendrier qui affiche l'exécution de deux transactions lisant les mêmes données

La transaction Txn1 en bleu lit des données A, met en mémoire tampon une écriture dans A, puis est validée. La transaction Txn2 en vert commence après Txn1, lit des données B, puis lit les données A. Comme Txn2 lit la valeur de A après que Txn1 a effectué un commit de son écriture dans A, Txn2 voit les effets de l'écriture de Txn1 dans A, même si Txn2 a démarré avant que Txn1 n'ait abouti.

Même si Txn1 et Txn2 se chevauchent dans le temps, leurs horodatages de commit c1 et c2 respectent un ordre de transaction linéaire, ce qui signifie que tous les effets des lectures et écritures de Txn1 semblent s'être produits à un seul moment donné (c1), et, de la même manière, tous les effets des lectures et des écritures de Txn2 semblent s'être produits à un seul moment donné (c2). De plus, c1 < c2 (garanti, car Txn1 et Txn2 ont toutes les deux effectué le commit de leur écriture ; c'est le cas même si les écritures ont eu lieu sur des machines différentes), ce qui respecte l'ordre d'exécution de Txn1 avant Txn2. Cependant, si Txn2 n'a exécuté que des lectures dans la transaction, alors c1 <= c2.

Les lectures observent un préfixe de l'historique de commit ; si une lecture voit les effets de Txn2, elle voit aussi ceux de Txn1. Toutes les transactions validées comportent cette propriété.

Garanties liées aux lectures et aux écritures

Si un appel à exécuter une transaction échoue, les garanties en lecture et en écriture dépendent de l'erreur qui a entraîné l'échec de l'appel de commit sous-jacent.

Par exemple, une erreur telle que "Ligne non trouvée" ou "Ligne déjà existante" signifie que l'écriture des mutations mises en mémoire tampon a rencontré une erreur, par exemple une des lignes que le client tente de mettre à jour n'existe pas. Dans ce cas, les lectures sont garanties cohérentes, les écritures ne sont pas appliquées et la non-existence de la ligne est également garantie cohérente avec les lectures.

Annuler des opérations de transaction

L'utilisateur peut annuler à tout moment les opérations de lecture asynchrone (par exemple, lorsqu'une opération de niveau supérieur est annulée ou si vous décidez d'arrêter une lecture en fonction des résultats initiaux reçus de la lecture) sans affecter les autres opérations existantes dans la transaction.

Toutefois, même si vous avez tenté d'annuler la lecture, Spanner ne garantit pas son annulation. Une fois que vous avez demandé l'annulation d'une lecture, celle-ci peut quand même aboutir ou échouer pour une autre raison (par exemple, en cas d'annulation). En outre, cette lecture annulée peut en réalité renvoyer des résultats ; ces résultats, éventuellement incomplets, seront validés dans le cadre du commit de la transaction.

Notez que, contrairement aux lectures, l'annulation d'une opération de commit entraînera l'abandon de la transaction (sauf si la transaction a déjà été validée ou a échoué pour une autre raison).

Performances

Verrouillage

Spanner permet à plusieurs clients d'interagir simultanément avec la même base de données. Afin d'assurer la cohérence de plusieurs transactions simultanées, Spanner utilise une combinaison de verrous partagés et exclusifs pour contrôler l'accès aux données. Lorsque vous effectuez une lecture dans le cadre d'une transaction, Spanner acquiert des verrous de lecture partagés, qui permettent aux autres lectures de continuer à accéder aux données jusqu'à ce que la transaction soit prête à être validée. Une fois la transaction validée et les écritures appliquées, la transaction tente de passer à un verrou exclusif. Elle bloque les nouveaux verrous en lecture partagés sur les données, attend que les verrous en lecture partagés existants soient annulés, puis place un verrou exclusif pour un accès exclusif aux données.

Notes au sujet des verrous :

  • Les verrous sont placés de façon granulaire au niveau des lignes et des colonnes. Si la transaction T1 a verrouillé la colonne "A" de la ligne "foo", et que la transaction T2 souhaite écrire dans la colonne "B" de la ligne "foo", il n'y a pas de conflit.
  • Les écritures sur un élément de données qui n'effectuent pas la lecture des données en cours d'écriture ("écriture cachée") n'entrent pas en conflit avec d'autres auteurs cachés du même élément (l'horodatage de commit de chaque écriture détermine l'ordre dans lequel elle est appliquée dans la base de données). En conséquence, Spanner n'a besoin de passer à un verrou exclusif que si vous avez lu les données que vous écrivez. Dans le cas contraire, Spanner utilise un verrou partagé appelé "verrou partagé pour l'auteur".
  • Lorsque vous effectuez des recherches sur des lignes au sein d'une transaction en lecture/écriture, utilisez des index secondaires pour limiter le nombre de lignes analysées à une plage plus petite. Cela oblige Spanner à verrouiller un nombre inférieur de lignes dans la table, ce qui permet une modification simultanée de lignes en dehors de la plage.
  • Les verrous ne doivent pas être utilisés pour garantir un accès exclusif à une ressource en dehors de Spanner. Les transactions peuvent être annulées pour plusieurs raisons par Spanner, par exemple lors du déplacement des données dans les ressources de calcul de l'instance. Lorsqu'une transaction est relancée, que ce soit explicitement par le code d'application ou implicitement par le biais d'un code client tel que le pilote JDBC Spanner, il est seulement garanti que les verrous ont été conservés lors de la tentative de commit effective.

  • L'outil d'introspection Verrouiller les statistiques vous permet d'examiner les conflits de verrouillage dans votre base de données.

Détection des blocages

Spanner détecte le blocage potentiel de plusieurs transactions et force l'abandon de toutes les transactions sauf une. Par exemple, considérons le scénario suivant : la transaction Txn1 maintient un verrou sur l'enregistrement A et attend un verrou sur l'enregistrement B, tandis que Txn2 maintient un verrou sur l'enregistrement B et attend un verrou sur l'enregistrement A. Le seul moyen d'avancer dans cette situation consiste à annuler l'une des transactions pour qu'elle procède au déverrouillage, permettant ainsi à l'autre transaction de progresser.

Spanner utilise l'algorithme standard "wound-wait" pour gérer la détection des blocages. En arrière-plan, Spanner suit l'âge de chaque transaction qui demande des verrous en conflit. et permet également aux transactions plus anciennes d'abandonner les transactions plus récentes ("ancien" désignant la lecture, la requête ou le commit de transaction ayant commencé plus tôt).

En donnant la priorité aux transactions plus anciennes, Spanner garantit que chaque transaction a la possibilité d'acquérir des verrous, une fois qu'elle est suffisamment ancienne pour avoir une priorité plus élevée que les autres transactions. Par exemple, une transaction qui obtient un verrou partagé pour le lecteur peut être annulée par une transaction plus ancienne nécessitant un verrou partagé pour l'auteur.

Exécution distribuée

Spanner peut exécuter des transactions sur des données réparties sur plusieurs serveurs. Cette puissance a un coût en termes de performances comparé aux transactions sur un seul serveur.

Quels types de transactions peuvent être distribués ? En arrière-plan, Spanner peut répartir la responsabilité des lignes de la base de données sur plusieurs serveurs. Une ligne et les lignes correspondantes dans les tables entrelacées sont généralement diffusées par le même serveur, comme le sont deux lignes d'une même table ayant des clés proches. Spanner peut effectuer des transactions sur plusieurs lignes de différents serveurs. Toutefois, en règle générale, les transactions qui affectent de nombreuses lignes colocalisées sont plus rapides et moins coûteuses que les transactions qui affectent de nombreuses lignes dispersées dans la base de données ou dans une grande table.

Les transactions les plus efficaces dans Spanner n'incluent que les lectures et les écritures qui doivent être appliquées de manière atomique. Les transactions sont plus rapides lorsque toutes les lectures et écritures accèdent aux données dans une même partie de l'espace clé.

Transactions en lecture seule

En plus de verrouiller les transactions en lecture-écriture, Spanner propose des transactions en lecture seule.

Utilisez une transaction en lecture seule lorsque vous devez exécuter plusieurs lectures au même horodatage. Si vous pouvez exprimer votre lecture à l'aide de l'une des méthodes de lecture unique de Spanner, utilisez plutôt cette méthode. Les performances liées à l'utilisation d'un appel en lecture unique devraient être comparables à celles d'une lecture unique effectuée dans une transaction en lecture seule.

Si vous lisez une grande quantité de données, envisagez d'utiliser des partitions pour lire les données en parallèle.

Parce que les transactions en lecture seule n'effectuent aucune écriture, elles ne peuvent ni être verrouillées, ni bloquer les autres transactions. Les transactions en lecture seule observent un préfixe cohérent de l'historique de commit des transactions. De la sorte, votre application obtient toujours des données cohérentes.

Propriétés

Une transaction Spanner en lecture seule exécute un ensemble de lectures à un moment logique unique, à la fois du point de vue de la transaction en lecture seule elle-même, et du point de vue des autres lecteurs et rédacteurs de la base de données Spanner. Ainsi, les transactions en lecture seule observent toujours un état cohérent de la base de données à un moment donné de l'historique des transactions.

Interface

Spanner fournit une interface permettant d'exécuter un ensemble de tâches dans le contexte d'une transaction en lecture seule, avec des tentatives d'exécution en cas d'annulation de la transaction.

Exemple

Le code qui suit montre comment utiliser une transaction en lecture seule afin d'obtenir des données cohérentes pour deux lectures au même horodatage :

C++

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

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

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

C#


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

public class QueryDataWithTransactionAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
    }

    public async Task<List<Album>> QueryDataWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        var albums = new List<Album>();
        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        using var connection = new SpannerConnection(connectionString);

        // Opens the connection so that the Spanner transaction included in the TransactionScope
        // is read-only TimestampBound.Strong.
        await connection.OpenAsync(AmbientTransactionOptions.ForTimestampBoundReadOnly(), default);
        using var cmd = connection.CreateSelectCommand("SELECT SingerId, AlbumId, AlbumTitle FROM Albums");

        // Read #1.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : " + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : " + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : " + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }

        // Read #2. Even if changes occur in-between the reads,
        // the transaction ensures that Read #1 and Read #2
        // return the same data.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                albums.Add(new Album
                {
                    AlbumId = reader.GetFieldValue<int>("AlbumId"),
                    SingerId = reader.GetFieldValue<int>("SingerId"),
                    AlbumTitle = reader.GetFieldValue<string>("AlbumTitle")
                });
            }
        }
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

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

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

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

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

Java

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

Node.js

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

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

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

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

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

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

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

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

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

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

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

PHP

use Google\Cloud\Spanner\SpannerClient;

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

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

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

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

Python

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

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

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

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

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

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

Ruby

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

require "google/cloud/spanner"

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

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

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

Transactions à LMD partitionné

À l'aide du langage de manipulation de données partitionné (LMD partitionné), vous pouvez exécuter des instructions UPDATE et DELETE à grande échelle sans vous heurter à des limites de transaction ni verrouiller une table entière. Spanner partitionne l'espace clé et exécute les instructions LMD sur chaque partition dans une transaction en lecture-écriture distincte.

Les instructions LMD s'exécutent dans des transactions en lecture-écriture que vous créez explicitement dans votre code. Pour plus d'informations, consultez la section Utiliser LMD.

Propriétés

Vous ne pouvez exécuter qu'une seule instruction LMD partitionné à la fois, que vous utilisiez une méthode de bibliothèque cliente ou la Google Cloud CLI.

Les transactions partitionnées ne sont pas compatibles avec les commits ou les restaurations. Spanner exécute et applique immédiatement l'instruction LMD. Si vous annulez l'opération ou si celle-ci échoue, Spanner annule toutes les partitions en cours d'exécution et ne démarre aucune des partitions restantes. Spanner ne restaure pas les partitions déjà exécutées.

Interface

Spanner fournit une interface permettant d'exécuter une seule instruction en mode LMD partitionné.

Examples

L'exemple de code suivant met à jour la colonne MarketingBudget de la table Albums.

C++

Vous utilisez la fonction ExecutePartitionedDml() pour exécuter une instruction DML partitionnée.

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#

Utilisez la méthode ExecutePartitionedUpdateAsync() pour exécuter une instruction en mode LMD partitionné.


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

Utilisez la méthode PartitionedUpdate() pour exécuter une instruction en mode LMD partitionné.


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

Utilisez la méthode executePartitionedUpdate() pour exécuter une instruction en mode LMD partitionné.

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

Utilisez la méthode runPartitionedUpdate() pour exécuter une instruction en mode LMD partitionné.

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

Utilisez la méthode executePartitionedUpdate() pour exécuter une instruction en mode LMD partitionné.

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

Utilisez la méthode execute_partitioned_dml() pour exécuter une instruction en mode LMD partitionné.

# 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

Utilisez la méthode execute_partitioned_update() pour exécuter une instruction en mode LMD partitionné.

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

L'exemple de code suivant supprime les lignes de la table Singers, en fonction de la colonne SingerId.

C++

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

C#


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

public class DeleteUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> DeleteUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

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

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

Go


import (
	"context"
	"fmt"
	"io"

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

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

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

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

Java

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

Node.js

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

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

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

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

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

PHP

use Google\Cloud\Spanner\SpannerClient;

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

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

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

Python

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

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

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

Ruby

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

require "google/cloud/spanner"

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

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

puts "#{row_count} records deleted."