Ringkasan transaksi

Halaman ini menjelaskan transaksi di Spanner dan menyertakan kode contoh untuk menjalankan transaksi.

Pengantar

Transaksi di Spanner adalah sekumpulan operasi baca dan tulis yang dieksekusi secara atomik pada satu waktu logis di seluruh kolom, baris, dan tabel dalam database.

Spanner mendukung mode transaksi berikut:

  • Mengunci baca-tulis. Transaksi ini mengandalkan penguncian pesimis dan, jika perlu, commit dengan dua fase. Mengunci transaksi baca-tulis dapat dibatalkan, sehingga aplikasi harus mencoba lagi.

  • Hanya baca. Jenis transaksi ini memberikan jaminan konsistensi di beberapa pembacaan, tetapi tidak memungkinkan penulisan. Secara default, transaksi hanya baca dijalankan pada stempel waktu yang dipilih sistem yang menjamin konsistensi eksternal, tetapi transaksi juga dapat dikonfigurasi untuk dibaca pada stempel waktu di masa lalu. Transaksi hanya baca tidak perlu di-commit dan tidak terkunci. Selain itu, transaksi hanya baca mungkin menunggu hingga operasi tulis yang sedang berlangsung selesai sebelum dieksekusi.

  • DML Berpartisi. Jenis transaksi ini mengeksekusi pernyataan Bahasa Manipulasi Data (DML) sebagai DML Berpartisi. DML yang dipartisi dirancang untuk update dan penghapusan massal, terutama pembersihan dan pengisian ulang berkala. Jika Anda perlu melakukan commit dalam jumlah besar blind write, tetapi tidak memerlukan transaksi atomik, Anda dapat mengubah tabel Spanner secara massal menggunakan batch write. Untuk mengetahui informasi selengkapnya, lihat Mengubah data menggunakan batch operasi.

Halaman ini menjelaskan properti umum dan semantik transaksi di Spanner serta memperkenalkan antarmuka transaksi DML Terpartisi, operasi baca-tulis, dan DML Terpartisi di Spanner.

Transaksi baca-tulis

Berikut adalah skenario saat Anda harus menggunakan transaksi baca-tulis penguncian:

  • Jika operasi tulis yang dilakukan bergantung pada hasil dari satu atau beberapa operasi baca, Anda harus melakukannya dan operasi baca tersebut dalam transaksi baca-tulis yang sama.
    • Contoh: gandakan saldo rekening bank A. Pembacaan saldo A harus berada dalam transaksi yang sama dengan penulisan untuk mengganti saldo dengan nilai yang digandakan.

  • Jika Anda melakukan satu atau beberapa operasi tulis yang perlu di-commit secara atomik, penulisan tersebut harus dilakukan dalam transaksi baca-tulis yang sama.
    • Contoh: transfer $200 dari akun A ke akun B. Kedua operasi tulis (satu penulisan untuk menurunkan A sebesar $200 dan yang lainnya untuk menaikkan B sebesar $200) dan pembacaan saldo rekening awal harus dilakukan dalam transaksi yang sama.

  • Jika Anda mungkin melakukan satu atau beberapa operasi tulis, bergantung pada hasil dari satu atau beberapa operasi baca, Anda harus melakukan operasi tulis dan baca tersebut dalam transaksi baca-tulis yang sama, meskipun jika pada akhirnya tidak dieksekusi.
    • Contoh: transfer $200 dari rekening bank A ke rekening bank B jika saldo A saat ini lebih besar dari $500. Transaksi Anda harus berisi pembacaan saldo A dan pernyataan kondisional yang berisi penulisan.

Berikut adalah skenario di mana Anda tidak boleh menggunakan transaksi baca-tulis yang mengunci:

  • Jika Anda hanya melakukan pembacaan, dan dapat mengekspresikan operasi baca menggunakan metode baca tunggal, Anda harus menggunakan metode baca tunggal tersebut atau transaksi hanya baca. Pembacaan tunggal tidak mengunci, tidak seperti transaksi baca-tulis.

Properti

Transaksi baca-tulis di Spanner mengeksekusi serangkaian pembacaan dan penulisan secara atomik pada satu titik waktu logis. Selain itu, stempel waktu saat transaksi baca-tulis dijalankan cocok dengan waktu aktual, dan urutan serialisasi cocok dengan urutan stempel waktu.

Mengapa menggunakan transaksi baca-tulis? Transaksi baca-tulis memberikan properti ACID dari database relasional (Faktanya, transaksi baca-tulis Spanner menawarkan jaminan yang lebih kuat daripada ACID tradisional; lihat bagian Semantik di bawah.).

Isolasi

Berikut adalah properti isolasi untuk transaksi baca-tulis dan hanya baca.

Transaksi yang membaca dan menulis

Berikut adalah properti isolasi yang Anda dapatkan setelah berhasil melakukan transaksi yang berisi serangkaian pembacaan (atau kueri) dan penulisan:

  • Semua operasi baca dalam transaksi yang menampilkan nilai menampilkan snapshot konsisten yang diambil pada stempel waktu commit transaksi.
  • Baris atau rentang kosong tetap demikian pada waktu commit.
  • Semua penulisan dalam transaksi di-commit pada stempel waktu commit transaksi.
  • Penulisan tidak dapat dilihat oleh transaksi apa pun hingga transaksi tersebut di-commit.

Driver klien Spanner tertentu berisi logika percobaan ulang transaksi untuk menyamarkan error sementara, yang dilakukan dengan menjalankan ulang transaksi dan memvalidasi data yang diamati klien.

Akibatnya, semua pembacaan dan penulisan tampaknya terjadi pada satu titik waktu, baik dari perspektif transaksi itu sendiri maupun dari perspektif pembaca dan penulis lain terhadap database Spanner. Dengan kata lain, operasi baca dan tulis berakhir pada stempel waktu yang sama (lihat ilustrasinya di bagian Serialisabilitas dan konsistensi eksternal di bawah).

Transaksi yang hanya dibaca

Jaminan untuk transaksi baca-tulis yang hanya membaca serupa: semua bacaan dalam transaksi tersebut akan menampilkan data dari stempel waktu yang sama, bahkan untuk tidak adanya baris. Satu perbedaannya adalah jika Anda membaca data, lalu meng-commit transaksi baca-tulis tanpa penulisan apa pun, tidak ada jaminan bahwa data tidak akan berubah dalam database setelah pembacaan dan sebelum commit. Jika ingin mengetahui apakah data telah berubah sejak Anda membacanya terakhir, pendekatan terbaik adalah membacanya lagi (baik dalam transaksi baca-tulis, atau menggunakan pembacaan yang kuat). Selain itu, untuk efisiensi, jika Anda sudah tahu sebelumnya bahwa Anda hanya akan membaca dan tidak menulis, Anda harus menggunakan transaksi hanya baca, bukan transaksi baca-tulis.

Atomisitas, Konsistensi, Ketahanan

Selain properti Isolation, Spanner menyediakan Atomicity (jika salah satu penulisan dalam commit transaksi, semuanya di-commit), Konsistensi (database tetap dalam status konsisten setelah transaksi), dan Ketahanan (data yang di-commit tetap di-commit).

Manfaat properti ini

Karena properti ini, sebagai developer aplikasi, Anda dapat berfokus pada kebenaran setiap transaksi sendiri, tanpa perlu memikirkan cara melindungi eksekusinya dari transaksi lain yang mungkin dijalankan pada waktu yang sama.

Antarmuka

Library klien Spanner menyediakan antarmuka untuk menjalankan isi pekerjaan dalam konteks transaksi baca-tulis, dengan percobaan ulang untuk pembatalan transaksi. Berikut adalah sedikit konteks untuk menjelaskan poin ini: transaksi Spanner mungkin harus dicoba beberapa kali sebelum di-commit. Misalnya, jika dua transaksi mencoba mengerjakan data secara bersamaan dengan cara yang dapat menyebabkan deadlock, Spanner akan membatalkan salah satunya, sehingga transaksi lainnya dapat berlanjut. (Peristiwa sementara dalam Spanner yang lebih jarang dapat mengakibatkan pembatalan beberapa transaksi.) Karena transaksi bersifat atomik, transaksi yang dibatalkan tidak memiliki efek yang terlihat pada database. Oleh karena itu, transaksi harus dijalankan dengan mencoba lagi hingga berhasil.

Saat menggunakan transaksi di library klien Spanner, Anda menentukan isi transaksi (yaitu, pembacaan dan penulisan yang akan dilakukan di satu atau beberapa tabel dalam database) dalam bentuk objek fungsi. Di balik layar, library klien Spanner menjalankan fungsi berulang kali hingga transaksi di-commit atau terjadi error yang tidak dapat dicoba lagi.

Contoh

Misalkan Anda menambahkan kolom MarketingBudget ke tabel Albums yang ditampilkan di halaman Skema dan Model Data:

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

Departemen pemasaran Anda memutuskan untuk melakukan upaya pemasaran untuk album yang dikunci oleh Albums (1, 1) dan telah meminta Anda untuk memindahkan $200.000 dari anggaran Albums (2, 2), tetapi hanya jika uangnya tersedia dalam anggaran album tersebut. Anda harus menggunakan transaksi baca-tulis penguncian untuk operasi ini, karena transaksi tersebut dapat melakukan penulisan, bergantung pada hasil pembacaan.

Berikut cara menjalankan transaksi baca-tulis:

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"

Semantik

Serialisabilitas dan konsistensi eksternal

Spanner menyediakan 'serialisasi', yang berarti semua transaksi muncul seolah-olah dijalankan dalam urutan serial, meskipun beberapa operasi baca, tulis, dan operasi lain dari transaksi yang berbeda benar-benar terjadi secara paralel. Spanner menetapkan stempel waktu commit yang mencerminkan urutan transaksi yang dilakukan untuk menerapkan properti ini. Faktanya, Spanner menawarkan jaminan yang lebih kuat daripada serialisabilitas yang disebut konsistensi eksternal: transaksi dilakukan dalam urutan yang tercermin dalam stempel waktu commit ini, dan stempel waktu commit ini mencerminkan secara real time sehingga Anda dapat membandingkannya dengan smartwatch Anda. Operasi baca dalam transaksi melihat semua yang telah di-commit sebelum transaksi di-commit, dan operasi tulis terlihat oleh semua yang dimulai setelah transaksi di-commit.

Misalnya, pertimbangkan eksekusi dua transaksi seperti yang diilustrasikan dalam diagram di bawah ini:

linimasa yang menunjukkan eksekusi
dua transaksi yang membaca data yang sama

Transaksi Txn1 yang berwarna biru membaca beberapa A data, mem-buffer operasi tulis ke A, lalu berhasil melakukan commit. Transaksi Txn2 yang berwarna hijau dimulai setelah Txn1, membaca beberapa data B, lalu membaca A data. Karena Txn2 membaca nilai A setelah Txn1 meng-commit penulisannya ke A, Txn2 melihat efek penulisan Txn1 ke A, meskipun Txn2 dimulai sebelum Txn1 selesai.

Meskipun ada beberapa tumpang tindih waktu saat Txn1 dan Txn2 dieksekusi, stempel waktu commitnya c1 dan c2 mengikuti urutan transaksi linear, yang berarti bahwa semua efek dari pembacaan dan penulisan Txn1 tampaknya terjadi pada satu titik waktu (c1), dan semua efek dari operasi baca dan tulis Txn2 tampaknya terjadi pada satu titik waktu (c2). Selain itu, c1 < c2 (yang dijamin adalah operasi tulis yang sama dengan Txn1 dan Txn2 yang terjadi pada mesin penulisan yang berbeda).Txn1Txn2 (Namun, jika Txn2 hanya melakukan pembacaan dalam transaksi, selanjutnya c1 <= c2).

Operasi baca mengamati awalan histori commit; jika operasi baca melihat efek Txn2, operasi baca juga akan melihat efek Txn1. Semua transaksi yang melakukan commit berhasil memiliki properti ini.

Jaminan operasi baca dan tulis

Jika panggilan untuk menjalankan transaksi gagal, pembacaan dan penulisan menjamin bahwa Anda bergantung pada error yang menyebabkan panggilan commit yang mendasarinya gagal.

Misalnya, error seperti "Row Not Found" atau "Row Sudah Ada" berarti penulisan mutasi yang di-buffer mengalami beberapa error, misalnya baris yang coba diperbarui oleh klien tidak ada. Dalam hal ini, operasi baca dijamin konsisten, operasi tulis tidak diterapkan, dan ketiadaan baris dijamin juga konsisten dengan pembacaan.

Membatalkan operasi transaksi

Operasi baca asinkron dapat dibatalkan kapan saja oleh pengguna (misalnya, saat operasi tingkat yang lebih tinggi dibatalkan atau Anda memutuskan untuk menghentikan pembacaan berdasarkan hasil awal yang diterima dari pembacaan) tanpa memengaruhi operasi lain yang sudah ada dalam transaksi.

Namun, meskipun Anda telah mencoba membatalkan pembacaan, Spanner tidak menjamin bahwa pembacaan tersebut benar-benar dibatalkan. Setelah Anda meminta pembatalan pembacaan, operasi baca tersebut masih dapat berhasil diselesaikan atau gagal dengan beberapa alasan lain (misalnya, Batalkan). Selain itu, pembacaan yang dibatalkan tersebut mungkin menampilkan beberapa hasil kepada Anda, dan hasil yang mungkin tidak lengkap tersebut akan divalidasi sebagai bagian dari Commit transaksi.

Perlu diperhatikan bahwa tidak seperti pembacaan, membatalkan operasi Commit transaksi akan mengakibatkan pembatalan transaksi (kecuali jika transaksi tersebut telah Berkomitmen atau gagal dengan alasan lain).

Performa

Mengunci

Spanner dapat digunakan beberapa klien untuk berinteraksi secara bersamaan dengan database yang sama. Untuk memastikan konsistensi beberapa transaksi serentak, Spanner menggunakan kombinasi kunci bersama dan kunci eksklusif untuk mengontrol akses ke data. Saat Anda melakukan pembacaan sebagai bagian dari transaksi, Spanner akan memperoleh kunci baca bersama, sehingga operasi baca lainnya tetap dapat mengakses data hingga transaksi Anda siap di-commit. Saat transaksi Anda di-commit dan operasi tulis diterapkan, transaksi akan mencoba mengupgrade ke kunci eksklusif. Fitur ini memblokir kunci baca bersama baru pada data, menunggu kunci baca bersama yang ada dihapus, lalu menempatkan kunci eksklusif untuk akses eksklusif ke data.

Catatan tentang smart lock:

  • Kunci diambil pada tingkat perincian baris dan kolom. Jika transaksi T1 telah mengunci kolom "A" dari baris "foo", dan transaksi T2 ingin menulis kolom "B" dari baris "foo", maka tidak akan ada konflik.
  • Menulis ke item data yang juga tidak membaca data yang sedang ditulis (alias "blind write") tidak bertentangan dengan penulis tunanetra lainnya dari item yang sama (stempel waktu commit setiap penulisan menentukan urutan penerapannya ke database). Konsekuensinya, Spanner hanya perlu diupgrade ke kunci eksklusif jika Anda telah membaca data yang ditulis. Jika tidak, Spanner akan menggunakan kunci bersama yang disebut kunci bersama penulis.
  • Saat melakukan pencarian baris di dalam transaksi baca-tulis, gunakan indeks sekunder untuk membatasi baris yang dipindai ke rentang yang lebih kecil. Hal ini menyebabkan Spanner mengunci lebih sedikit baris dalam tabel, sehingga memungkinkan modifikasi serentak ke baris di luar rentang.
  • Kunci tidak boleh digunakan untuk memastikan akses eksklusif ke resource di luar Spanner. Transaksi dapat dibatalkan karena beberapa alasan oleh Spanner seperti, misalnya, saat mengizinkan data berpindah di resource komputasi instance. Jika transaksi dicoba lagi, baik secara eksplisit dengan kode aplikasi maupun secara implisit oleh kode klien seperti driver JDBC Spanner, hanya ada jaminan bahwa kunci ditahan selama upaya yang benar-benar dilakukan.

  • Anda dapat menggunakan alat introspeksi Statistik kunci untuk menyelidiki konflik kunci di database Anda.

Deteksi deadlock

Spanner mendeteksi ketika beberapa transaksi mungkin mengalami deadlock, dan memaksa semua transaksi kecuali satu transaksi untuk dibatalkan. Misalnya, pertimbangkan skenario berikut: transaksi Txn1 menyimpan kunci pada kumpulan data A dan menunggu kunci pada kumpulan data B, serta Txn2 menyimpan kunci pada kumpulan data B dan menunggu kunci pada kumpulan data A. Satu-satunya cara untuk membuat progres dalam situasi ini adalah dengan membatalkan salah satu transaksi agar melepaskan kuncinya, sehingga transaksi lainnya dapat dilanjutkan.

Spanner menggunakan algoritma "wound-wait" standar untuk menangani deteksi deadlock. Di balik layar, Spanner memantau usia setiap transaksi yang meminta kunci yang bertentangan. Hal ini juga memungkinkan transaksi lama untuk membatalkan transaksi yang lebih lama ("yang lebih lama" berarti pembacaan, kueri, atau commit paling awal dari transaksi tersebut terjadi lebih cepat).

Dengan memberikan prioritas pada transaksi yang lebih lama, Spanner memastikan bahwa setiap transaksi memiliki peluang untuk memperoleh kunci pada akhirnya, setelah cukup lama untuk memiliki prioritas yang lebih tinggi daripada transaksi lainnya. Misalnya, transaksi yang memperoleh kunci bersama pembaca dapat dibatalkan oleh transaksi lama yang memerlukan kunci bersama penulis.

Eksekusi terdistribusi

Spanner dapat menjalankan transaksi pada data yang mencakup beberapa server. Kekuatan ini memiliki biaya performa dibandingkan dengan transaksi server tunggal.

Jenis transaksi apa yang mungkin didistribusikan? Di balik layar, Spanner dapat membagi tanggung jawab untuk baris dalam {i>database<i} ke banyak server. Baris dan baris yang sesuai dalam tabel sisipan biasanya disalurkan oleh server yang sama, seperti dua baris dalam tabel yang sama dengan kunci di dekatnya. Spanner dapat melakukan transaksi lintas baris pada server yang berbeda. Namun, sebagai pedoman, transaksi yang memengaruhi banyak baris yang ditempatkan bersama akan lebih cepat dan lebih murah dibandingkan transaksi yang memengaruhi banyak baris yang tersebar di seluruh database, atau di seluruh tabel besar.

Transaksi yang paling efisien di Spanner hanya menyertakan pembacaan dan penulisan yang harus diterapkan secara atomik. Transaksi akan berjalan paling cepat jika semua membaca dan menulis mengakses data di bagian yang sama dalam ruang kunci.

Transaksi hanya baca

Selain mengunci transaksi baca-tulis, Spanner juga menawarkan transaksi hanya baca.

Gunakan transaksi hanya baca jika Anda perlu mengeksekusi lebih dari satu operasi baca pada stempel waktu yang sama. Jika Anda dapat mengekspresikan operasi baca menggunakan salah satu metode baca tunggal Spanner, Anda harus menggunakan metode baca tunggal tersebut. Performa penggunaan satu panggilan baca tersebut harus sebanding dengan performa satu pembacaan yang dilakukan dalam transaksi hanya baca.

Jika Anda membaca data dalam jumlah besar, pertimbangkan untuk menggunakan partisi untuk membaca data secara paralel.

Karena transaksi hanya-baca tidak ditulis, transaksi tersebut tidak menahan kunci dan tidak memblokir transaksi lain. Transaksi hanya baca mengamati awalan yang konsisten dari histori commit transaksi, sehingga aplikasi Anda selalu mendapatkan data yang konsisten.

Properti

Transaksi hanya baca Spanner mengeksekusi sekumpulan operasi baca pada satu titik waktu logis, baik dari perspektif transaksi hanya baca itu sendiri maupun dari perspektif pembaca dan penulis lain hingga database Spanner. Ini berarti bahwa transaksi hanya baca selalu mengamati status database yang konsisten pada titik yang dipilih dalam histori transaksi.

Antarmuka

Spanner menyediakan antarmuka untuk menjalankan isi tugas dalam konteks transaksi hanya baca, dengan percobaan ulang untuk pembatalan transaksi.

Contoh

Berikut ini cara menggunakan transaksi hanya baca guna mendapatkan data yang konsisten untuk dua pembacaan pada stempel waktu yang sama:

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

Transaksi DML yang dipartisi

Dengan Bahasa Manipulasi Data Terpartisi (DML Berpartisi), Anda dapat mengeksekusi pernyataan UPDATE dan DELETE skala besar tanpa menemui batas transaksi atau mengunci seluruh tabel. Spanner mempartisi ruang kunci dan menjalankan pernyataan DML pada setiap partisi dalam transaksi baca-tulis terpisah.

Anda menjalankan pernyataan DML dalam transaksi baca-tulis yang Anda buat secara eksplisit dalam kode Anda. Untuk mengetahui informasi selengkapnya, lihat Menggunakan DML.

Properti

Anda hanya dapat menjalankan satu pernyataan DML yang dipartisi pada satu waktu, baik saat menggunakan metode library klien maupun Google Cloud CLI.

Transaksi yang dipartisi tidak mendukung commit atau rollback. Spanner akan segera mengeksekusi dan menerapkan pernyataan DML. Jika Anda membatalkan operasi, atau operasi gagal, Spanner akan membatalkan semua partisi yang dieksekusi dan tidak memulai partisi yang tersisa. Spanner tidak melakukan rollback partisi apa pun yang telah dieksekusi.

Antarmuka

Spanner menyediakan antarmuka untuk menjalankan pernyataan DML Terpartisi.

Contoh

Contoh kode berikut memperbarui kolom MarketingBudget pada tabel Albums.

C++

Anda menggunakan fungsi ExecutePartitionedDml() untuk mengeksekusi pernyataan DML yang dipartisi.

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#

Anda menggunakan metode ExecutePartitionedUpdateAsync() untuk mengeksekusi pernyataan DML yang dipartisi.


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

Anda menggunakan metode PartitionedUpdate() untuk mengeksekusi pernyataan DML yang dipartisi.


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

Anda menggunakan metode executePartitionedUpdate() untuk mengeksekusi pernyataan DML yang dipartisi.

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

Anda menggunakan metode runPartitionedUpdate() untuk mengeksekusi pernyataan DML yang dipartisi.

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

Anda menggunakan metode executePartitionedUpdate() untuk mengeksekusi pernyataan DML yang dipartisi.

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

Anda menggunakan metode execute_partitioned_dml() untuk mengeksekusi pernyataan DML yang dipartisi.

# 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

Anda menggunakan metode execute_partitioned_update() untuk mengeksekusi pernyataan DML yang dipartisi.

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

Contoh kode berikut menghapus baris dari tabel Singers, berdasarkan kolom 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."