Transactions

Présentation

Une transaction dans Cloud Spanner est un ensemble de lectures et d'écritures qui s'exécutent de manière atomique à un moment logique unique dans des colonnes, des lignes et des tables d'une base de données.

Cloud Spanner est compatible avec les modes de transaction suivants :

  • Verrouillage des lectures-écritures. Ce type de transaction est le seul compatible avec l'écriture de données dans Cloud Spanner. 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 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. Les transactions en lecture seule peuvent être configurées pour correspondre à des horodatages passés. Les transactions en lecture seule n'ont pas besoin d'être validées et ne peuvent pas être verrouillées.

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

Cette page décrit les propriétés générales et la sémantique des transactions dans Cloud Spanner, et présente les interfaces de transaction en lecture-écriture, en lecture seule et en LMD partitionné dans Cloud 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 Cloud 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 Cloud Spanner offrent des garanties encore plus solides que les transactions ACID traditionnelles ; consultez la section Sémantique ci-dessous.)

    Isolation

    Transactions en lecture et écriture

    Voici les propriétés d'isolation que vous obtenez pour une transaction en lecture-écriture contenant une série de lectures et d'écritures :

    • Toutes les lectures de cette transaction renvoient des données du même horodatage.
    • Si une transaction est validée, aucun autre auteur n'a modifié les données lues dans la transaction après lecture.
    • Ces propriétés sont valables même pour les lectures qui n'affichent aucune ligne et pour les espaces entre les lignes renvoyées par les lectures de plage : les lignes inexistantes comptent comme des données.
    • Toutes les écritures au sein de cette transaction sont validées au même horodatage.
    • Toutes les écritures au sein de cette transaction ne deviennent visibles qu'après le commit de cette dernière.

    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 Cloud 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 l'isolation, Cloud 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 Cloud 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 Cloud 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, Cloud Spanner abandonne l'une d'entre elles afin que l'autre puisse progresser. (Plus rarement, des événements transitoires dans Cloud 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 la bibliothèque cliente Cloud Spanner, vous définissez le corps de la 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 Cloud 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::runtime_error(commit.status().message());
      std::cout << "Transfer was successful [spanner_read_write_transaction]\n";
    }

    C#

    public static async Task ReadWriteWithTransactionAsync(
        string projectId,
        string instanceId,
        string databaseId)
    {
        // This sample transfers 200,000 from the MarketingBudget
        // field of the second Album to the first Album. Make sure to run
        // the addColumn and writeDataToNewColumn samples first,
        // in that order.
    
        string connectionString =
        $"Data Source=projects/{projectId}/instances/{instanceId}"
        + $"/databases/{databaseId}";
    
        using (TransactionScope scope = new TransactionScope(
            TransactionScopeAsyncFlowOption.Enabled))
        {
            decimal transferAmount = 200000;
            decimal secondBudget = 0;
            decimal firstBudget = 0;
    
            // Create connection to Cloud Spanner.
            using (var connection =
                new SpannerConnection(connectionString))
            {
                // Create statement to select the second album's data.
                var cmdLookup = connection.CreateSelectCommand(
                "SELECT * FROM Albums WHERE SingerId = 2 AND AlbumId = 2");
                // Excecute the select query.
                using (var reader = await cmdLookup.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        // Read the second album's budget.
                        secondBudget =
                          reader.GetFieldValue<decimal>("MarketingBudget");
                        // Confirm second Album's budget is sufficient and
                        // if not raise an exception. Raising an exception
                        // will automatically roll back the transaction.
                        if (secondBudget < transferAmount)
                        {
                            throw new Exception("The second album's "
                                + $"budget {secondBudget} "
                                + "is less than the "
                                + "amount to transfer.");
                        }
                    }
                }
                // Read the first album's budget.
                cmdLookup = connection.CreateSelectCommand(
                "SELECT * FROM Albums WHERE SingerId = 1 and AlbumId = 1");
                using (var reader = await cmdLookup.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        firstBudget =
                          reader.GetFieldValue<decimal>("MarketingBudget");
                    }
                }
    
                // Specify update command parameters.
                var cmd = connection.CreateUpdateCommand("Albums",
                    new SpannerParameterCollection {
                    {"SingerId", SpannerDbType.Int64},
                    {"AlbumId", SpannerDbType.Int64},
                    {"MarketingBudget", SpannerDbType.Int64},
                });
                // Update second album to remove the transfer amount.
                secondBudget -= transferAmount;
                cmd.Parameters["SingerId"].Value = 2;
                cmd.Parameters["AlbumId"].Value = 2;
                cmd.Parameters["MarketingBudget"].Value = secondBudget;
                await cmd.ExecuteNonQueryAsync();
                // Update first album to add the transfer amount.
                firstBudget += transferAmount;
                cmd.Parameters["SingerId"].Value = 1;
                cmd.Parameters["AlbumId"].Value = 1;
                cmd.Parameters["MarketingBudget"].Value = firstBudget;
                await cmd.ExecuteNonQueryAsync();
                scope.Complete();
                Console.WriteLine("Transaction complete.");
            }
        }
    }

    Go

    
    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(
              new TransactionCallable<Void>() {
                @Override
                public Void run(TransactionContext transaction) throws Exception {
                  // Transfer marketing budget from one album to another. We do it in a transaction to
                  // ensure that the transfer is atomic.
                  Struct row =
                      transaction.readRow("Albums", Key.of(2, 2), Arrays.asList("MarketingBudget"));
                  long album2Budget = row.getLong(0);
                  // Transaction will only be committed if this condition still holds at the time of
                  // commit. Otherwise it will be aborted and the callable will be rerun by the
                  // client library.
                  long transfer = 200000;
                  if (album2Budget >= transfer) {
                    long album1Budget =
                        transaction
                            .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget"))
                            .getLong(0);
                    album1Budget += transfer;
                    album2Budget -= transfer;
                    transaction.buffer(
                        Mutation.newUpdateBuilder("Albums")
                            .set("SingerId")
                            .to(1)
                            .set("AlbumId")
                            .to(1)
                            .set("MarketingBudget")
                            .to(album1Budget)
                            .build());
                    transaction.buffer(
                        Mutation.newUpdateBuilder("Albums")
                            .set("SingerId")
                            .to(2)
                            .set("AlbumId")
                            .to(2)
                            .set("MarketingBudget")
                            .to(album2Budget)
                            .build());
                  }
                  return null;
                }
              });
    }

    Node.js

    // This sample transfers 200,000 from the MarketingBudget field
    // of the second Album to the first Album, as long as the second
    // Album has enough money in its budget. Make sure to run the
    // addColumn and updateData samples first (in that order).
    
    // Imports the Google Cloud client library
    const {Spanner} = require('@google-cloud/spanner');
    
    /**
     * TODO(developer): Uncomment the following lines before running the sample.
     */
    // const projectId = 'my-project-id';
    // const instanceId = 'my-instance';
    // const databaseId = 'my-database';
    
    // Creates a client
    const spanner = new Spanner({
      projectId: projectId,
    });
    
    // Gets a reference to a Cloud Spanner instance and database
    const instance = spanner.instance(instanceId);
    const database = instance.database(databaseId);
    
    const transferAmount = 200000;
    
    database.runTransaction(async (err, transaction) => {
      if (err) {
        console.error(err);
        return;
      }
      let firstBudget, secondBudget;
      const queryOne = {
        columns: ['MarketingBudget'],
        keys: [[2, 2]], // SingerId: 2, AlbumId: 2
      };
    
      const queryTwo = {
        columns: ['MarketingBudget'],
        keys: [[1, 1]], // SingerId: 1, AlbumId: 1
      };
    
      Promise.all([
        // Reads the second album's budget
        transaction.read('Albums', queryOne).then(results => {
          // Gets second album's budget
          const rows = results[0].map(row => row.toJSON());
          secondBudget = rows[0].MarketingBudget;
          console.log(`The second album's marketing budget: ${secondBudget}`);
    
          // Makes sure the second album's budget is large enough
          if (secondBudget < transferAmount) {
            throw new Error(
              `The second album's budget (${secondBudget}) is less than the transfer amount (${transferAmount}).`
            );
          }
        }),
    
        // Reads the first album's budget
        transaction.read('Albums', queryTwo).then(results => {
          // Gets first album's budget
          const rows = results[0].map(row => row.toJSON());
          firstBudget = rows[0].MarketingBudget;
          console.log(`The first album's marketing budget: ${firstBudget}`);
        }),
      ])
        .then(() => {
          console.log(firstBudget, secondBudget);
          // Transfers the budgets between the albums
          firstBudget += transferAmount;
          secondBudget -= transferAmount;
    
          console.log(firstBudget, secondBudget);
    
          // Updates the database
          // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
          // must be converted (back) to strings before being inserted as INT64s.
          transaction.update('Albums', [
            {
              SingerId: '1',
              AlbumId: '1',
              MarketingBudget: firstBudget.toString(),
            },
            {
              SingerId: '2',
              AlbumId: '2',
              MarketingBudget: secondBudget.toString(),
            },
          ]);
        })
        .then(() => {
          // Commits the transaction and send the changes to the database
          return transaction.commit();
        })
        .then(() => {
          console.log(
            `Successfully executed read-write transaction to transfer ${transferAmount} from Album 2 to Album 1.`
          );
        })
        .catch(err => {
          console.error('ERROR:', err);
        })
        .then(() => {
          transaction.end();
          // Closes the database when finished
          return database.close();
        });
    });

    PHP

    use Google\Cloud\Spanner\SpannerClient;
    use Google\Cloud\Spanner\Transaction;
    use UnexpectedValueException;
    
    /**
     * Performs a read-write transaction to update two sample records in the
     * database.
     *
     * This will transfer 200,000 from the `MarketingBudget` field for the second
     * Album to the first Album. If the `MarketingBudget` for the second Album is
     * too low, it will raise an exception.
     *
     * Before running this sample, you will need to run the `update_data` sample
     * to populate the fields.
     * Example:
     * ```
     * read_write_transaction($instanceId, $databaseId);
     * ```
     *
     * @param string $instanceId The Spanner instance ID.
     * @param string $databaseId The Spanner database ID.
     */
    function read_write_transaction($instanceId, $databaseId)
    {
        $spanner = new SpannerClient();
        $instance = $spanner->instance($instanceId);
        $database = $instance->database($databaseId);
    
        $database->runTransaction(function (Transaction $t) use ($spanner) {
            $transferAmount = 200000;
    
            // Read the second album's budget.
            $secondAlbumKey = [2, 2];
            $secondAlbumKeySet = $spanner->keySet(['keys' => [$secondAlbumKey]]);
            $secondAlbumResult = $t->read(
                'Albums',
                $secondAlbumKeySet,
                ['MarketingBudget'],
                ['limit' => 1]
            );
    
            $firstRow = $secondAlbumResult->rows()->current();
            $secondAlbumBudget = $firstRow['MarketingBudget'];
            if ($secondAlbumBudget < $transferAmount) {
                // Throwing an exception will automatically roll back the transaction.
                throw new UnexpectedValueException(
                    'The second album\'s budget is lower than the transfer amount: ' . $transferAmount
                );
            }
    
            $firstAlbumKey = [1, 1];
            $firstAlbumKeySet = $spanner->keySet(['keys' => [$firstAlbumKey]]);
            $firstAlbumResult = $t->read(
                'Albums',
                $firstAlbumKeySet,
                ['MarketingBudget'],
                ['limit' => 1]
            );
    
            // Read the first album's budget.
            $firstRow = $firstAlbumResult->rows()->current();
            $firstAlbumBudget = $firstRow['MarketingBudget'];
    
            // Update the budgets.
            $secondAlbumBudget -= $transferAmount;
            $firstAlbumBudget += $transferAmount;
            printf('Setting first album\'s budget to %s and the second album\'s ' .
                'budget to %s.' . PHP_EOL, $firstAlbumBudget, $secondAlbumBudget);
    
            // Update the rows.
            $t->updateBatch('Albums', [
                ['SingerId' => 1, 'AlbumId' => 1, 'MarketingBudget' => $firstAlbumBudget],
                ['SingerId' => 2, 'AlbumId' => 2, 'MarketingBudget' => $secondAlbumBudget],
            ]);
    
            // Commit the transaction!
            $t->commit();
    
            print('Transaction complete.' . PHP_EOL);
        });
    }

    Python

    def read_write_transaction(instance_id, database_id):
        """Performs a read-write transaction to update two sample records in the
        database.
    
        This will transfer 200,000 from the `MarketingBudget` field for the second
        Album to the first Album. If the `MarketingBudget` is too low, it will
        raise an exception.
    
        Before running this sample, you will need to run the `update_data` sample
        to populate the fields.
        """
        spanner_client = spanner.Client()
        instance = spanner_client.instance(instance_id)
        database = instance.database(database_id)
    
        def update_albums(transaction):
            # Read the second album budget.
            second_album_keyset = spanner.KeySet(keys=[(2, 2)])
            second_album_result = transaction.read(
                table='Albums', columns=('MarketingBudget',),
                keyset=second_album_keyset, limit=1)
            second_album_row = list(second_album_result)[0]
            second_album_budget = second_album_row[0]
    
            transfer_amount = 200000
    
            if second_album_budget < transfer_amount:
                # Raising an exception will automatically roll back the
                # transaction.
                raise ValueError(
                    'The second album doesn\'t have enough funds to transfer')
    
            # Read the first album's budget.
            first_album_keyset = spanner.KeySet(keys=[(1, 1)])
            first_album_result = transaction.read(
                table='Albums', columns=('MarketingBudget',),
                keyset=first_album_keyset, limit=1)
            first_album_row = list(first_album_result)[0]
            first_album_budget = first_album_row[0]
    
            # Update the budgets.
            second_album_budget -= transfer_amount
            first_album_budget += transfer_amount
            print(
                'Setting first album\'s budget to {} and the second album\'s '
                'budget to {}.'.format(
                    first_album_budget, second_album_budget))
    
            # Update the rows.
            transaction.update(
                table='Albums',
                columns=(
                    'SingerId', 'AlbumId', 'MarketingBudget'),
                values=[
                    (1, 1, first_album_budget),
                    (2, 2, second_album_budget)])
    
        database.run_in_transaction(update_albums)
    
        print('Transaction complete.')

    Ruby

    # project_id  = "Your Google Cloud project ID"
    # instance_id = "Your Spanner instance ID"
    # database_id = "Your Spanner database ID"
    
    require "google/cloud/spanner"
    
    spanner         = Google::Cloud::Spanner.new project: project_id
    client          = spanner.client instance_id, database_id
    transfer_amount = 200_000
    
    client.transaction do |transaction|
      first_album  = transaction.read("Albums", [:MarketingBudget], keys: [[1, 1]]).rows.first
      second_album = transaction.read("Albums", [:MarketingBudget], keys: [[2, 2]]).rows.first
    
      raise "The second album does not have enough funds to transfer" if second_album[:MarketingBudget] < transfer_amount
    
      new_first_album_budget  = first_album[:MarketingBudget] + transfer_amount
      new_second_album_budget = second_album[:MarketingBudget] - transfer_amount
    
      transaction.update "Albums", [
        { SingerId: 1, AlbumId: 1, MarketingBudget: new_first_album_budget  },
        { SingerId: 2, AlbumId: 2, MarketingBudget: new_second_album_budget }
      ]
    end
    
    puts "Transaction complete"

    Sémantique

    Sérialisabilité et cohérence externe

    Cloud Spanner fournit une "sérialisabilité", ce qui signifie que toutes les transactions apparaissent comme si elles étaient exécutées dans un ordre sériel, même si certaines des lectures, écritures et autres opérations de transactions distinctes ont effectivement eu lieu en parallèle. Pour mettre en œuvre cette propriété, Cloud Spanner attribue des horodatages de commit qui reflètent l'ordre des transactions validées. En fait, Cloud 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. Ces horodatages de commit reflètent le temps réel, ce qui vous permet de 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.

    Cependant, même si vous avez tenté d'annuler la lecture, Cloud 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

    Cloud Spanner permet à plusieurs clients d'interagir simultanément avec la même base de données. Afin de garantir la cohérence de plusieurs transactions simultanées, Cloud 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 d'une transaction, Cloud Spanner acquiert des verrous en lecture partagés, ce qui permet 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, Cloud Spanner n'a besoin de passer à un verrou exclusif que si vous avez lu les données que vous écrivez. Sinon, Cloud Spanner utilise un verrou partagé appelé "verrou partagé pour l'auteur".
    • Lorsque vous effectuez des recherches sur des lignes au cours d'une transaction en lecture/écriture, utilisez des index secondaires pour limiter le nombre de lignes analysées à une plage plus petite. Cloud Spanner verrouille ainsi moins de lignes dans la table, ce qui permet de modifier simultanément des lignes en dehors de la plage.

    Détection des blocages

    Cloud Spanner détecte le blocage potentiel de plusieurs transactions et impose 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.

    Cloud Spanner utilise l'algorithme standard "wound-wait" pour gérer la détection des blocages. En arrière-plan, Cloud Spanner conserve une trace de l'âge de chaque transaction ayant entraîné des conflits de verrouillage 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, Cloud Spanner garantit que chaque transaction a la possibilité d'obtenir un verrou, une fois que leur ancienneté est devenue suffisante pour leur donner une priorité plus élevée par rapport aux 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

    Cloud 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, Cloud 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 les mêmes transactions sur plusieurs lignes de serveurs différents. Cependant, 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 Cloud 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 du verrouillage des transactions en lecture-écriture, Cloud Spanner offre 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 Cloud 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 de 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 en lecture seule Cloud Spanner exécute un ensemble de lectures à un moment logique unique, à la fois du point de vue de la transaction en lecture seule, et des autres lecteurs et auteurs de la base de données Cloud 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

    Cloud 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 const& row : spanner::StreamOf<RowType>(rows1)) {
        if (!row) throw std::runtime_error(row.status().message());
        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 const& row : spanner::StreamOf<RowType>(rows2)) {
        if (!row) throw std::runtime_error(row.status().message());
        std::cout << "SingerId: " << std::get<0>(*row)
                  << " AlbumId: " << std::get<1>(*row)
                  << " AlbumTitle: " << std::get<2>(*row) << "\n";
      }
    }

    C#

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

    Go

    
    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($instanceId, $databaseId)
    {
        $spanner = new SpannerClient();
        $instance = $spanner->instance($instanceId);
        $database = $instance->database($databaseId);
    
        $snapshot = $database->snapshot();
        $results = $snapshot->execute(
            'SELECT SingerId, AlbumId, AlbumTitle FROM Albums'
        );
        print('Results from the first read:' . PHP_EOL);
        foreach ($results as $row) {
            printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
                $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
        }
    
        // Perform another read using the `read` method. Even if the data
        // is updated in-between the reads, the snapshot ensures that both
        // return the same data.
        $keySet = $spanner->keySet(['all' => true]);
        $results = $database->read(
            'Albums',
            $keySet,
            ['SingerId', 'AlbumId', 'AlbumTitle']
        );
    
        print('Results from the second read:' . PHP_EOL);
        foreach ($results->rows() as $row) {
            printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
                $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
        }
    }

    Python

    def read_only_transaction(instance_id, database_id):
        """Reads data inside of a read-only transaction.
    
        Within the read-only transaction, or "snapshot", the application sees
        consistent view of the database at a particular timestamp.
        """
        spanner_client = spanner.Client()
        instance = spanner_client.instance(instance_id)
        database = instance.database(database_id)
    
        with database.snapshot(multi_use=True) as snapshot:
            # Read using SQL.
            results = snapshot.execute_sql(
                'SELECT SingerId, AlbumId, AlbumTitle FROM Albums')
    
            print('Results from first read:')
            for row in results:
                print(u'SingerId: {}, AlbumId: {}, AlbumTitle: {}'.format(*row))
    
            # Perform another read using the `read` method. Even if the data
            # is updated in-between the reads, the snapshot ensures that both
            # return the same data.
            keyset = spanner.KeySet(all_=True)
            results = snapshot.read(
                table='Albums',
                columns=('SingerId', 'AlbumId', 'AlbumTitle',),
                keyset=keyset,)
    
            print('Results from second read:')
            for row in results:
                print(u'SingerId: {}, AlbumId: {}, AlbumTitle: {}'.format(*row))

    Ruby

    # project_id  = "Your Google Cloud project ID"
    # instance_id = "Your Spanner instance ID"
    # database_id = "Your Spanner database ID"
    
    require "google/cloud/spanner"
    
    spanner = Google::Cloud::Spanner.new project: project_id
    client  = spanner.client instance_id, database_id
    
    client.snapshot do |snapshot|
      snapshot.execute("SELECT SingerId, AlbumId, AlbumTitle FROM Albums").rows.each do |row|
        puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
      end
    
      # Even if changes occur in-between the reads, the transaction ensures that
      # both return the same data.
      snapshot.read("Albums", [:AlbumId, :AlbumTitle, :SingerId]).rows.each do |row|
        puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
      end
    end

    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. Cloud 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 en mode LMD partitionné à la fois, que vous utilisiez une méthode de bibliothèque cliente ou l'outil de ligne de commande gcloud.

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

    Interface

    Cloud Spanner fournit une interface pour l'exécution d'une seule instruction en mode LMD partitionné.

    Exemples

    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::runtime_error(result.status().message());
      std::cout << "Update was successful [spanner_dml_partitioned_update]\n";
    }

    C#

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

    public static async Task UpdateUsingPartitionedDmlCoreAsync(
        string projectId,
        string instanceId,
        string databaseId)
    {
        string connectionString =
            $"Data Source=projects/{projectId}/instances/{instanceId}"
            + $"/databases/{databaseId}";
    
        // Create connection to Cloud Spanner.
        using (var connection =
            new SpannerConnection(connectionString))
        {
            await connection.OpenAsync();
    
            SpannerCommand cmd = connection.CreateDmlCommand(
                "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
            );
            long rowCount = await cmd.ExecutePartitionedUpdateAsync();
            Console.WriteLine($"{rowCount} row(s) updated...");
        }
    }

    Go

    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($instanceId, $databaseId)
    {
        $spanner = new SpannerClient();
        $instance = $spanner->instance($instanceId);
        $database = $instance->database($databaseId);
    
        $rowCount = $database->executePartitionedUpdate(
            "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
        );
    
        printf('Updated %d row(s).' . PHP_EOL, $rowCount);
    }

    Python

    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::runtime_error(result.status().message());
      std::cout << "Delete was successful [spanner_dml_partitioned_delete]\n";
    }

    C#

    public static async Task DeleteUsingPartitionedDmlCoreAsync(
        string projectId,
        string instanceId,
        string databaseId)
    {
        string connectionString =
            $"Data Source=projects/{projectId}/instances/{instanceId}"
            + $"/databases/{databaseId}";
    
        // Create connection to Cloud Spanner.
        using (var connection =
            new SpannerConnection(connectionString))
        {
            await connection.OpenAsync();
    
            SpannerCommand cmd = connection.CreateDmlCommand(
                "DELETE FROM Singers WHERE SingerId > 10"
            );
            long rowCount = await cmd.ExecutePartitionedUpdateAsync();
            Console.WriteLine($"{rowCount} row(s) deleted...");
        }
    }

    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($instanceId, $databaseId)
    {
        $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."