Présentation des transactions

Cette page explique les transactions dans Spanner et inclut des exemples de code pour exécuter des transactions.

Présentation

Une transaction dans Spanner est un ensemble de lectures et d'écritures qui s'exécutent de manière atomique à un seul moment logique dans les colonnes, les lignes et les tables de 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, en deux phases. Le verrouillage des transactions en lecture-écriture peut être annulé, ce qui nécessite une nouvelle tentative d'exécution 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 code temporel choisi par le système qui garantit la cohérence externe, mais elles peuvent également être configurées pour lire à un code temporel passé. Lecture seule les transactions n'ont pas besoin d'être validées et n'utilisent pas de verrous. De plus, les transactions en lecture seule peuvent attendre que les écritures en cours soient terminées avant de en cours d'exécution.

  • LMD partitionné. Ce type de transaction exécute une manipulation de données en tant que LMD partitionné. Le LMD partitionné est conçu pour les mises à jour et les suppressions groupées, notamment un nettoyage et un rembourrage périodiques. Si vous devez engager un grand nombre en écriture aveugle, mais ne nécessitant pas de transaction atomique, vous pouvez modifier vos tables Spanner à l'aide de l'écriture par lot. Pour plus d'informations, consultez la page 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 visant à 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 au compte B. Les deux écritures (une pour soustraire 200 $ de A, l'autre pour ajouter 200 $ à B) et la lecture du solde initial des comptes doivent correspondre à 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 moment logique unique. 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

Voici 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 du commit de la transaction.
  • Les lignes ou plages vides sont restées vides au moment du commit.
  • Toutes les écritures de la transaction ont été validées au moment du commit de la transaction.
  • Les écritures n'étaient visibles par aucune transaction avant le commit de la transaction.

Certains pilotes clients 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 s'être produites à un moment donné, à la fois du point de vue de la transaction elle-même, et d'autres lecteurs et auteurs 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é d'isolation, Spanner fournit des propriétés d'atomicité (si l'une des écritures dans la transaction est validée, elles le sont toutes), de cohérence (la base de données reste dans un état cohérent après la transaction) et de 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 pour l'exécution de tâches dans le contexte d'une transaction en lecture/écriture, avec la possibilité de nouvelles tentatives d'exécution en cas d'échec de la transaction. Voici un peu de contexte pour expliquer ce point : une transaction Spanner devra sans doute être testée plusieurs fois avant d'être validée. Par exemple, si deux transactions tentent de manipuler des données en même temps et éventuellement créent un blocage, Spanner abandonne l'une d'entre elles afin que l'autre puisse progresser. Plus rarement, des événements temporaires dans Spanner 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 corps d'une transaction (c'est-à-dire les lectures et écritures à effectuer sur un ou plusieurs tableaux 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 à plusieurs reprises jusqu'au commit de la transaction ou l'apparition d'une erreur pour laquelle les nouvelles tentatives ne sont pas possibles.

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 une "sérialisabilité", ce qui signifie que toutes les transactions apparaissent comme s'ils s'exécutaient dans un ordre sérialisé, même si une partie des lectures, écritures, et d'autres opérations de transactions distinctes ont eu lieu en parallèle. Spanner attribue des horodatages de commit reflétant l'ordre des validations transactions pour implémenter cette propriété. En fait, Spanner offre plus forte que la sérialisabilité, appelée cohérence externe: les transactions sont validées. une commande qui est reflétée dans leurs horodatages de commit, Les codes temporels indiquent en temps réel que vous pouvez 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&#39;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 essayez d'annuler la lecture, Spanner n'effectue pas garantir que la lecture est effectivement annulée. 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 le même base de données. Afin de garantir la cohérence de plusieurs transactions simultanées, Spanner utilise une combinaison de verrous partagés et de verrous exclusifs pour contrôler l'accès aux données. Lorsque vous effectuez une lecture dans le cadre transaction, Spanner acquiert des verrous de lecture partagés, ce qui permet à d'autres en lecture seule pour 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 mettre à niveau à un verrou exclusif si vous avez lu les données que vous écrivez. Sinon, Spanner utilise un verrou partagé appelé "verrou partagé de rédacteur".
  • 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. Spanner verrouille donc moins de nombres de lignes, ce qui permet de modifier simultanément les lignes situées en dehors de la plage d'adresses IP.
  • Les verrous ne doivent pas être utilisés pour garantir un accès exclusif à une ressource située en dehors Spanner. Les transactions peuvent être annulées pour différentes raisons par Spanner, par exemple pour autoriser le transfert de données entre les ressources de calcul de l'instance. Si une transaction est relancée, que ce soit explicitement par un code d'application ou implicitement par un code client tel que le pilote JDBC Spanner, la seule garantie est le maintien des verrous pendant la tentative réellement effectuée.

  • 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 l'interblocage de plusieurs transactions. Force l'annulation 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 la méthode standard "wound-wait" pour gérer les interblocages la détection. En coulisses, Spanner garde une trace de l'âge de chaque 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 une chance d'acquérir des verrous, une fois obsolètes 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 les données couvrant 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 entre les lignes sur des serveurs différents ; Toutefois, en règle générale, les transactions qui affectent de nombreuses lignes colocalisées sont plus rapides et moins chères Des 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 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 offre les 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 au cours d'une à un moment précis, du point de vue de la transaction en lecture seule du point de vue des autres lecteurs et écrivains, 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 pour l'exécution de tâches dans le contexte d'une transaction en lecture seule, avec la possibilité de nouvelles tentatives d'exécution en cas d'échec 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(SpannerTransactionCreationOptions.ReadOnly, options: null, cancellationToken: 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 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 en mode LMD partitionné à la fois, que vous utilisiez une méthode de bibliothèque cliente ou la CLI Google Cloud.

Les transactions partitionnées ne sont pas compatibles avec les commits ou les restaurations. Spanner s'exécute et applique immédiatement l'instruction LMD. Si vous annulez l'opération, ou si l'opération échoue, Spanner annule toutes les opérations et ne démarre aucune des partitions restantes. Spanner n'effectue aucun rollback des partitions déjà exécutées.

Interface

Spanner fournit une interface pour l'exécution d'un seul 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."