Panoramica delle transazioni

Questa pagina descrive le transazioni in Spanner e include un codice campione per l'esecuzione di transazioni.

introduzione

Una transazione in Spanner è un insieme di operazioni di lettura e scrittura che vengono eseguite a livello atomico in un singolo momento logico su colonne, righe e tabelle di un database.

Spanner supporta le seguenti modalità di transazione:

  • Blocco lettura/scrittura. Queste transazioni si basano su blocchi pessimistici e, se necessario, su un commit in due fasi. Il blocco delle transazioni di lettura-scrittura potrebbe essere interrotto, richiedendo all'applicazione di riprovare.

  • Sola lettura. Questo tipo di transazione offre coerenza garantita tra diverse letture, ma non consente scritture. Per impostazione predefinita, le transazioni di sola lettura vengono eseguite a un timestamp scelto dal sistema, che garantisce la coerenza esterna, ma possono anche essere configurate per la lettura a un timestamp passato. Le transazioni di sola lettura non richiedono alcun commit e non accettano blocchi. Inoltre, le transazioni di sola lettura potrebbero attendere il completamento delle scritture in corso prima dell'esecuzione.

  • DML partizionato. Questo tipo di transazione esegue un'istruzione DML (Data Manipulation Language) come DML partizionato. Il DML partizionato è progettato per gli aggiornamenti e le eliminazioni collettive, in particolare la pulizia e il backfill periodici. Se devi eseguire il commit di un numero elevato di scritture blind, ma non hai bisogno di una transazione atomica, puoi modificare in blocco le tabelle Spanner utilizzando la scrittura batch. Per ulteriori informazioni, consulta Modificare i dati tramite scritture batch.

Questa pagina descrive le proprietà generali e la semantica delle transazioni in Spanner e introduce le interfacce delle transazioni DML partizionate, di lettura-scrittura, di sola lettura e in Spanner.

Transazioni di lettura/scrittura

Ecco gli scenari in cui è necessario utilizzare una transazione di lettura-scrittura con blocco:

  • Se esegui una scrittura che dipende dal risultato di una o più letture, dovresti farlo e le operazioni di lettura nella stessa transazione di lettura/scrittura.
    • Esempio: il doppio del saldo del conto bancario A. La lettura del saldo di A deve avvenire nella stessa transazione della scrittura per sostituire il saldo con il valore raddoppiato.

  • Se esegui una o più scritture che devono essere sottoposte a commit atomico, devi eseguirle nella stessa transazione di lettura/scrittura.
    • Esempio: trasferisci 200 € dall'account A all'account B. Entrambe le operazioni di scrittura (una per ridurre A di 200 $e l'altra per aumentare B di 200 $) e le letture dei saldi iniziali dell'account devono avvenire nella stessa transazione.

  • Se puoi eseguire una o più scritture, a seconda dei risultati di una o più letture, dovresti eseguirle nella stessa transazione di lettura-scrittura, anche se le scritture non vengono eseguite.
    • Esempio: trasferisci 200 $dal conto bancario A al conto bancario B se il saldo attuale di A è superiore a 500 $. La transazione dovrebbe contenere una lettura del saldo di A e un'affermazione condizionale contenente le scritture.

Ecco uno scenario in cui non dovresti utilizzare una transazione di lettura-scrittura di blocco:

  • Se stai eseguendo solo operazioni di lettura e puoi esprimere la lettura utilizzando un singolo metodo di lettura, dovresti usare questo singolo metodo di lettura o una transazione di sola lettura. Le singole letture non si bloccano, a differenza delle transazioni di lettura-scrittura.

Proprietà

Una transazione di lettura-scrittura in Spanner esegue un set di letture e scritture a livello logico in un unico momento logico. Inoltre, il timestamp in cui vengono eseguite le transazioni di lettura/scrittura corrisponde all'ora reale e l'ordine di serializzazione corrisponde a quello del timestamp.

Perché utilizzare una transazione di lettura e scrittura? Le transazioni di lettura e scrittura forniscono le proprietà ACID dei database relazionali. Infatti, le transazioni di lettura e scrittura di Spanner offrono garanzie ancora più efficaci rispetto all'ACID tradizionale; consulta la sezione Semantica di seguito.

Isolamento

Di seguito sono riportate le proprietà di isolamento per le transazioni di lettura/scrittura e di sola lettura.

Transazioni in lettura e scrittura

Di seguito sono riportate le proprietà di isolamento che ottieni dopo aver eseguito correttamente il commit di una transazione contenente una serie di letture (o query) e scritture:

  • Tutte le letture all'interno della transazione hanno restituito valori che riflettono uno snapshot coerente realizzato al timestamp di commit della transazione.
  • Le righe o gli intervalli vuoti rimanevano al momento del commit.
  • Tutte le scritture all'interno della transazione sono state confermate al timestamp del commit della transazione.
  • Le scritture non sono state visibili ad alcuna transazione fino a dopo l'emissione della transazione.

Alcuni driver client Spanner contengono una logica di nuovo tentativo di transazione per mascherare gli errori temporanei, che eseguono di nuovo la transazione e convalidano i dati osservati dal client.

Il risultato è che tutte le letture e le scritture sembrano essersi verificate in un singolo punto nel tempo, sia dal punto di vista della transazione stessa che dal punto di vista di altri lettori e autori del database Spanner. In altre parole, le letture e le scritture si verificano con lo stesso timestamp (vedi un'illustrazione in merito nella sezione Serializzabilità e coerenza esterna di seguito).

Transazioni che leggono

Le garanzie per una transazione di lettura/scrittura che legge solo sono simili: tutte le letture all'interno di quella transazione restituiscono dati dallo stesso timestamp, anche in assenza di una riga. Una differenza è che se leggi i dati e in seguito esegui il commit della transazione di lettura e scrittura senza operazioni di scrittura, non vi è alcuna garanzia che i dati non siano stati modificati nel database dopo la lettura e prima del commit. Se vuoi sapere se i dati sono cambiati dall'ultima volta che li hai letti, l'approccio migliore consiste nel rileggerli (in una transazione di lettura e scrittura o utilizzando una lettura efficace). Inoltre, per maggiore efficienza, se sai in anticipo che potrai solo leggere e non scrivere, dovresti utilizzare una transazione di sola lettura anziché una transazione di lettura-scrittura.

Atomicità, costanza, durabilità

Oltre alla proprietà Isolamento, Spanner fornisce Atomicità (se una qualsiasi delle scritture nel commit della transazione esegue il commit), Coerenza (il database rimane in uno stato coerente dopo la transazione) e Durabilità (i dati impegnati rimangono impegnati).

Vantaggi di queste strutture

Grazie a queste proprietà, in qualità di sviluppatore di applicazioni puoi concentrarti sulla correttezza di ogni transazione in autonomia, senza preoccuparti di come proteggerne l'esecuzione da altre transazioni che potrebbero essere eseguite contemporaneamente.

Interfaccia

Le librerie client di Spanner forniscono un'interfaccia per l'esecuzione di un corpo del lavoro nel contesto di una transazione di lettura-scrittura, con nuovi tentativi per l'interruzione della transazione. Ecco un po' di contesto per spiegare questo punto: potrebbe essere necessario provare più volte una transazione Spanner prima di eseguire il commit. Ad esempio, se due transazioni tentano di lavorare sui dati contemporaneamente in un modo che potrebbe causare un deadlock, Spanner interrompe una di esse in modo che l'altra transazione possa progredire. Più raramente, gli eventi temporanei in Spanner potrebbero comportare l'interruzione di alcune transazioni. Poiché le transazioni sono atomiche, una transazione interrotta non ha alcun effetto visibile sul database. Di conseguenza, le transazioni devono essere eseguite riprovando finché l'operazione non va a buon fine.

Quando utilizzi una transazione in una libreria client Spanner, definisci il corpo di una transazione (ovvero le operazioni di lettura e scrittura da eseguire su una o più tabelle di un database) sotto forma di oggetto funzione. In sostanza, la libreria client di Spanner esegue la funzione ripetutamente fino a quando non viene eseguito il commit della transazione o non si verifica un errore non ripetibile.

Esempio

Supponi di aver aggiunto una colonna MarketingBudget alla tabella Albums mostrata nella pagina Schema e modello di dati:

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

Il tuo reparto marketing decide di effettuare una spinta di marketing per l'album inserito da Albums (1, 1) e ti ha chiesto di trasferire 200.000 $dal budget di Albums (2, 2), ma solo se il denaro è disponibile nel budget dell'album. Devi utilizzare una transazione di blocco di lettura/scrittura per questa operazione, perché la transazione potrebbe eseguire scritture a seconda del risultato di una lettura.

Di seguito viene mostrato come eseguire una transazione di lettura-scrittura:

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"

Semantica

Serializzabilità e coerenza esterna

Spanner offre la "serializzabilità", il che significa che tutte le transazioni vengono visualizzate come se fossero eseguite in un ordine seriale, anche se alcune delle letture, delle scritture e altre operazioni di transazioni distinte si sono effettivamente verificate in parallelo. Spanner assegna timestamp di commit che riflettono l'ordine delle transazioni impegnate per implementare questa proprietà. Di fatto, Spanner offre una garanzia più efficace rispetto alla serializzabilità chiamata coerenza esterna: le transazioni vengono commit in un ordine indicato nei timestamp di commit e questi timestamp di commit riflettono in tempo reale, quindi puoi confrontarli con il tuo smartwatch. Le letture di una transazione visualizzano tutto ciò che è stato impegnato prima dell'esecuzione della transazione, mentre le scritture sono visibili da tutto ciò che inizia dopo il commit della transazione.

Ad esempio, considera l'esecuzione di due transazioni come illustrato nello schema riportato di seguito:

sequenza temporale che mostra l&#39;esecuzione
di due transazioni che leggono gli stessi dati

La transazione Txn1 in blu legge alcuni dati A, esegue il buffering di una scrittura in A ed esegue il commit correttamente. La transazione Txn2 in verde inizia dopo il giorno Txn1, legge alcuni dati B, quindi legge i dati A. Poiché Txn2 legge il valore di A dopo il commit di Txn1 in A, Txn2 vede l'effetto della scrittura di Txn1 in A, anche se Txn2 viene avviato prima del completamento di Txn1.

Anche se si verifica una certa sovrapposizione nel tempo in cui Txn1 e Txn2 vengono entrambi in esecuzione, i relativi timestamp di commit c1 e c2 rispettano un ordine di transazione lineare, il che significa che tutti gli effetti delle letture e delle scritture di Txn1 sembrano essersi verificati in un singolo momento (c1) e tutti gli effetti delle letture e delle scritture di Txn2 sembrano essersi verificati in un singolo punto di tempo (c2), anche se le scritture di Txn2 si sono verificate in un unico momento (c2).Txn1Txn1Txn2c1 < c2 Tuttavia, se Txn2 ha soltanto letto nella transazione, allora c1 <= c2.

Le letture osservano un prefisso della cronologia di commit; se una lettura rileva l'effetto di Txn2, vede anche l'effetto di Txn1. Tutte le transazioni di cui è stato eseguito il commit hanno questa proprietà.

Garanzie di lettura e scrittura

Se una chiamata per l'esecuzione di una transazione non va a buon fine, la funzionalità di lettura e scrittura garantisce che tu debba dipendere dall'errore in cui la chiamata di commit sottostante non ha avuto esito positivo.

Ad esempio, un errore come "Riga non trovata" o "Riga già esistente" indica che la scrittura delle mutazioni nel buffer ha riscontrato un errore, ad esempio una riga che il client sta cercando di aggiornare non esiste. In questo caso, le letture sono garantite coerenti, le scritture non vengono applicate e l'inesistenza della riga è garantita per essere coerente con le letture.

Annullamento delle operazioni di transazione

Le operazioni di lettura asincrone possono essere annullate in qualsiasi momento dall'utente (ad esempio, quando viene annullata un'operazione di livello superiore o decidi di interrompere una lettura in base ai risultati iniziali ricevuti dalla lettura), senza influire su altre operazioni esistenti nella transazione.

Tuttavia, anche se hai tentato di annullare la lettura, Spanner non garantisce che la lettura venga effettivamente annullata. Dopo aver richiesto l'annullamento di una lettura, questa potrebbe comunque essere completata o non riuscire per qualche altro motivo (ad esempio, l'interruzione). Inoltre, la lettura annullata potrebbe restituirti alcuni risultati, che verranno convalidati come parte del commit della transazione.

Tieni presente che, a differenza delle letture, l'annullamento di un'operazione di commit di una transazione comporterà l'interruzione della transazione (a meno che la transazione non sia già stata confermata o non è andata a buon fine per un altro motivo).

Prestazioni

Chiusura

Spanner consente a più client di interagire contemporaneamente con lo stesso database. Per garantire la coerenza di più transazioni simultanee, Spanner utilizza una combinazione di blocchi condivisi e blocchi esclusivi per controllare l'accesso ai dati. Quando esegui una lettura come parte di una transazione, Spanner acquisisce blocchi di lettura condivisi, che consentono ad altre letture di accedere ai dati finché la transazione non è pronta per il commit. Quando è in corso il commit della transazione e le scritture vengono applicate, la transazione tenta di eseguire l'upgrade a un blocco esclusivo. Blocca i nuovi blocchi di lettura condivisi sui dati, attende la cancellazione di quelli condivisi esistenti, quindi inserisce un blocco esclusivo per l'accesso esclusivo ai dati.

Note sulle serrature:

  • I blocchi vengono calcolati con la granularità di riga e colonna. Se la transazione T1 ha bloccato la colonna "A" della riga "foo" e la transazione T2 vuole scrivere la colonna "B" della riga "foo", non ci sono conflitti.
  • Scrive in un elemento di dati che non legge anche i dati scritti ("scritture cieche") non in conflitto con altri blind writer dello stesso elemento (il timestamp di commit di ogni scrittura determina l'ordine in cui viene applicato al database). Di conseguenza, Spanner deve eseguire l'upgrade a un blocco esclusivo solo se hai letto i dati che stai scrivendo. Altrimenti Spanner utilizza un blocco condiviso chiamato blocco condiviso dello scrittore.
  • Quando esegui ricerche di righe all'interno di una transazione di lettura-scrittura, utilizza gli indici secondari per limitare le righe analizzate a un intervallo inferiore. In questo modo Spanner blocca un numero inferiore di righe nella tabella, consentendo la modifica contemporanea di righe esterne all'intervallo.
  • Non utilizzare i blocchi per garantire l'accesso esclusivo a una risorsa esterna a Spanner. Le transazioni possono essere interrotte da diversi motivi da parte di Spanner come, ad esempio, quando i dati possono essere spostati tra le risorse di calcolo dell'istanza. Se viene eseguito un nuovo tentativo di transazione, esplicitamente tramite codice dell'applicazione o implicitamente dal codice client come il driver JDBC di Spanner, viene garantito solo che i blocchi siano stati bloccati durante il tentativo di cui è stato eseguito il commit.

  • Puoi utilizzare lo strumento di introduzione alle statistiche di blocco per esaminare i conflitti di blocco nel database.

Rilevamento deadlock

Spanner rileva quando più transazioni potrebbero essere soggette a deadlock e forza l'interruzione di tutte le transazioni tranne una. Ad esempio, considera il seguente scenario: la transazione Txn1 prevede un blocco per il record A ed è in attesa di un blocco per il record B, mentre Txn2 mantiene un blocco per il record B ed è in attesa di un blocco per il record A. L'unico modo per procedere in questa situazione è interrompere una delle transazioni in modo da rilasciare il blocco, consentendo l'avanzamento dell'altra transazione.

Spanner utilizza l'algoritmo standard di "wound-wait" per gestire il rilevamento dei deadlock. In background, Spanner tiene traccia dell'età di ogni transazione che richiede blocchi in conflitto. Consente inoltre alle transazioni meno recenti di interrompere quelle meno recenti (dove "precedenti" significa che la lettura, la query o il commit precedenti alla transazione si sono verificati prima).

Dando la priorità alle transazioni meno recenti, Spanner garantisce che ogni transazione abbia la possibilità di acquisire blocchi, una volta che le transazioni sono sufficientemente vecchie da avere una priorità maggiore rispetto alle altre transazioni. Ad esempio, una transazione che acquisisce un blocco condiviso del lettore può essere interrotta da una transazione meno recente che richiede un blocco condiviso del lettore.

Esecuzione distribuita

Spanner può eseguire transazioni su dati su più server. Questa potenza ha un costo in termini di prestazioni rispetto alle transazioni su singolo server.

Quali tipi di transazioni potrebbero essere distribuiti? In sostanza, Spanner può suddividere la responsabilità delle righe del database tra molti server. Una riga e le righe corrispondenti nelle tabelle con interleaving vengono generalmente gestite dallo stesso server, così come due righe nella stessa tabella con chiavi vicine. Spanner può eseguire transazioni tra righe su server diversi. Tuttavia, come regola generale, le transazioni che interessano molte righe co-location sono più veloci ed economiche rispetto alle transazioni che interessano molte righe sparse nel database o in una tabella di grandi dimensioni.

Le transazioni più efficienti in Spanner includono solo le operazioni di lettura e scrittura da applicare a livello atomico. Le transazioni sono più veloci quando tutte le operazioni di lettura e scrittura accedono ai dati nella stessa parte dello spazio della chiave.

Transazioni di sola lettura

Oltre a bloccare le transazioni di lettura/scrittura, Spanner offre transazioni di sola lettura.

Utilizza una transazione di sola lettura quando devi eseguire più operazioni di lettura allo stesso timestamp. Se riesci a esprimere la lettura utilizzando uno dei metodi di lettura singola di Spanner, utilizza invece questo singolo metodo di lettura. Le prestazioni dell'utilizzo di una chiamata di lettura singola devono essere paragonabili a quelle di una singola lettura eseguita in una transazione di sola lettura.

Se stai leggendo una grande quantità di dati, valuta l'utilizzo delle partizioni per leggere i dati in parallelo.

Poiché le transazioni di sola lettura non scrivono, non prevedono blocchi e non bloccano altre transazioni. Le transazioni di sola lettura osservano un prefisso coerente nella cronologia di commit delle transazioni, in modo che la tua applicazione riceva sempre dati coerenti.

Proprietà

Una transazione di sola lettura di Spanner esegue un insieme di letture in un singolo momento logico nel tempo, sia dal punto di vista della transazione di sola lettura in sé sia dal punto di vista di altri lettori e scrittori al database di Spanner. Ciò significa che le transazioni di sola lettura osservano sempre uno stato coerente del database in un determinato momento nella cronologia delle transazioni.

Interfaccia

Spanner fornisce un'interfaccia per l'esecuzione di una parte del lavoro nel contesto di una transazione di sola lettura, con nuovi tentativi per l'interruzione delle transazioni.

Esempio

Di seguito viene mostrato come utilizzare una transazione di sola lettura per ottenere dati coerenti per due letture con lo stesso timestamp:

C++

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

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

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

C#


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

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

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

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

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

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

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

Go


import (
	"context"
	"fmt"
	"io"

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

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

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

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

Java

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

Node.js

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

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

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

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

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

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

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

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

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

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

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

PHP

use Google\Cloud\Spanner\SpannerClient;

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

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

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

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

Python

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

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

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

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

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

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

Ruby

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

require "google/cloud/spanner"

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

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

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

Transazioni DML partizionate

Utilizzando il Partitioned Data Manipulation Language (DML partizionato), puoi eseguire istruzioni UPDATE e DELETE su larga scala senza dover rispettare limiti delle transazioni o bloccare un'intera tabella. Spanner partiziona lo spazio delle chiavi ed esegue le istruzioni DML su ogni partizione in una transazione di lettura-scrittura separata.

Puoi eseguire istruzioni DML nelle transazioni di lettura/scrittura create in modo esplicito nel codice. Per maggiori informazioni, consulta la pagina relativa all'utilizzo di DML.

Proprietà

Puoi eseguire una sola istruzione DML partizionata alla volta, indipendentemente dal fatto che tu stia utilizzando un metodo di libreria client o Google Cloud CLI.

Le transazioni partizionate non supportano il commit o il rollback. Spanner esegue e applica immediatamente l'istruzione DML. Se annulli l'operazione o l'operazione non va a buon fine, Spanner annulla tutte le partizioni in esecuzione e non avvia nessuna delle partizioni rimanenti. Spanner non esegue il rollback di nessuna partizione già eseguita.

Interfaccia

Spanner fornisce un'interfaccia per l'esecuzione di una singola istruzione DML partizionata.

Esempi

Il seguente esempio di codice aggiorna la colonna MarketingBudget della tabella Albums.

C++

Puoi usare la funzione ExecutePartitionedDml() per eseguire un'istruzione DML partizionata.

void DmlPartitionedUpdate(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("UPDATE Albums SET MarketingBudget = 100000"
                            "  WHERE SingerId > 1"));
  if (!result) throw std::move(result).status();
  std::cout << "Updated at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_update]\n";
}

C#

Usa il metodo ExecutePartitionedUpdateAsync() per eseguire un'istruzione DML partizionata.


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

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

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

        using var cmd = connection.CreateDmlCommand("UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

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

Go

Usa il metodo PartitionedUpdate() per eseguire un'istruzione DML partizionata.


import (
	"context"
	"fmt"
	"io"

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

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

	stmt := spanner.Statement{SQL: "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "%d record(s) updated.\n", rowCount)
	return nil
}

Java

Usa il metodo executePartitionedUpdate() per eseguire un'istruzione DML partizionata.

static void updateUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records updated.\n", rowCount);
}

Node.js

Usa il metodo runPartitionedUpdate() per eseguire un'istruzione DML partizionata.

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

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

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

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

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1',
  });
  console.log(`Successfully updated ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

Usa il metodo executePartitionedUpdate() per eseguire un'istruzione DML partizionata.

use Google\Cloud\Spanner\SpannerClient;

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

    $rowCount = $database->executePartitionedUpdate(
        'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1'
    );

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

Python

Usa il metodo execute_partitioned_dml() per eseguire un'istruzione DML partizionata.

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml(
    "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

print("{} records updated.".format(row_ct))

Ruby

Usa il metodo execute_partitioned_update() per eseguire un'istruzione DML partizionata.

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

Il seguente esempio di codice elimina le righe dalla tabella Singers, in base alla colonna 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."