Ringkasan transaksi

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

Pengantar

Transaksi di Spanner adalah kumpulan operasi baca dan tulis yang dijalankan secara acak pada satu titik 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 diperlukan, commit dua fase. Mengunci transaksi baca-tulis dapat dibatalkan, sehingga aplikasi harus mencoba lagi.

  • Hanya baca. Jenis transaksi ini memberikan konsistensi yang terjamin di seluruh beberapa operasi baca, tetapi tidak mengizinkan operasi tulis. Secara default, transaksi hanya baca dijalankan pada stempel waktu yang dipilih sistem yang menjamin konsistensi eksternal, tetapi transaksi tersebut juga dapat dikonfigurasi untuk membaca pada stempel waktu sebelumnya. Transaksi hanya baca tidak perlu di-commit dan tidak mengambil kunci. Selain itu, transaksi hanya baca mungkin menunggu penulisan yang sedang berlangsung selesai sebelum dieksekusi.

  • DML yang dipartisi. Jenis transaksi ini mengeksekusi pernyataan Bahasa Pengolahan Data (DML) sebagai DML Terpartisi. DML yang dipartisi dirancang untuk update dan penghapusan massal, terutama pembersihan dan pengisian ulang berkala. Jika Anda perlu melakukan commit pada sejumlah besar tulisan buta, tetapi tidak memerlukan transaksi atomik, Anda dapat mengubah tabel Spanner secara massal menggunakan penulisan batch. Untuk mengetahui informasi selengkapnya, lihat Mengubah data menggunakan operasi tulis batch.

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

Transaksi baca-tulis

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

  • Jika Anda melakukan operasi tulis yang bergantung pada hasil satu atau beberapa operasi baca, Anda harus melakukan operasi tulis dan baca tersebut dalam transaksi baca-tulis yang sama.
    • Contoh: melipatgandakan 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, Anda harus melakukan operasi tulis tersebut dalam transaksi baca-tulis yang sama.
    • Contoh: transfer $200 dari akun A ke akun B. Kedua operasi tulis (satu untuk mengurangi A sebesar $200 dan satu untuk menaikkan B sebesar $200) dan operasi baca saldo akun awal harus berada dalam transaksi yang sama.

  • Jika Anda mungkin melakukan satu atau beberapa operasi tulis, bergantung pada hasil satu atau beberapa operasi baca, Anda harus melakukan operasi tulis dan baca tersebut dalam transaksi baca-tulis yang sama, meskipun operasi tulis tidak akhirnya 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 operasi tulis.

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

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

Properti

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

Mengapa menggunakan transaksi baca-tulis? Transaksi baca-tulis menyediakan properti ACID database relasional (Bahkan, 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 operasi baca (atau kueri) dan tulis:

  • Semua operasi baca dalam transaksi menampilkan nilai yang mencerminkan snapshot konsisten yang diambil pada stempel waktu commit transaksi.
  • Baris atau rentang kosong tetap kosong pada waktu commit.
  • Semua operasi tulis dalam transaksi di-commit pada stempel waktu commit transaksi.
  • Operasi tulis tidak terlihat oleh transaksi apa pun hingga setelah transaksi di-commit.

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

Efeknya adalah semua operasi baca dan tulis tampaknya terjadi pada satu titik waktu, baik dari perspektif transaksi itu sendiri maupun dari perspektif pembaca dan penulis lain ke database Spanner. Dengan kata lain, pembacaan dan penulisan akhirnya terjadi pada stempel waktu yang sama (lihat ilustrasi ini di bagian Serialisasi dan konsistensi eksternal di bawah).

Transaksi yang hanya membaca

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

Atomitas, Konsistensi, Durabilitas

Selain properti Isolasi, Spanner menyediakan Atomicity (jika ada penulisan dalam commit transaksi, semuanya akan di-commit), Consistency (database tetap dalam status konsisten setelah transaksi), dan Durability (data yang di-commit tetap di-commit.)

Manfaat properti ini

Karena properti ini, sebagai developer aplikasi, Anda dapat berfokus pada kebenaran setiap transaksi itu sendiri, tanpa perlu khawatir tentang cara melindungi eksekusinya dari transaksi lain yang mungkin dieksekusi 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 menangani data secara bersamaan dengan cara yang mungkin menyebabkan deadlock, Spanner akan membatalkan salah satunya sehingga transaksi lainnya dapat membuat progres. (Lebih jarang, peristiwa sementara dalam Spanner dapat mengakibatkan beberapa transaksi dibatalkan.) Karena transaksi bersifat atomik, transaksi yang dibatalkan tidak akan memiliki efek yang terlihat pada database. Oleh karena itu, transaksi harus dieksekusi dengan mencobanya lagi hingga berhasil.

Saat menggunakan transaksi di library klien Spanner, Anda menentukan isi transaksi (yaitu, operasi baca dan tulis yang akan dilakukan pada 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 error yang tidak dapat dicoba ulang terjadi.

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 diberi kunci oleh Albums (1, 1) dan telah meminta Anda untuk memindahkan $200.000 dari anggaran Albums (2, 2), tetapi hanya jika uang tersebut tersedia dalam anggaran album tersebut. Anda harus menggunakan transaksi baca-tulis penguncian untuk operasi ini, karena transaksi mungkin melakukan operasi tulis bergantung pada hasil operasi baca.

Berikut ini 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 sebenarnya terjadi secara paralel. Spanner menetapkan stempel waktu commit yang mencerminkan urutan transaksi yang di-commit untuk menerapkan properti ini. Faktanya, Spanner menawarkan jaminan yang lebih kuat daripada serialisasi yang disebut konsistensi eksternal: transaksi melakukan commit dalam urutan yang tercermin dalam stempel waktu commit-nya, dan stempel waktu commit ini mencerminkan waktu nyata sehingga Anda dapat membandingkannya dengan smartwatch. Operasi baca dalam transaksi melihat semua yang telah di-commit sebelum transaksi di-commit, dan operasi tulis dilihat oleh semua yang dimulai setelah transaksi di-commit.

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

linimasa yang menunjukkan eksekusi
dua transaksi yang membaca data yang sama

Transaksi Txn1 berwarna biru membaca beberapa data A, buffering operasi tulis ke A, lalu berhasil melakukan commit. Transaksi Txn2 berwarna hijau dimulai setelah Txn1, membaca beberapa data B, lalu membaca data A. Karena Txn2 membaca nilai A setelah Txn1 melakukan operasi tulis ke A, Txn2 akan melihat efek operasi tulis Txn1 ke A, meskipun Txn2 dimulai sebelum Txn1 selesai.

Meskipun ada beberapa tumpang-tindih waktu saat Txn1 dan Txn2 dijalankan, stempel waktu commit-nya c1 dan c2 mengikuti urutan transaksi linear, yang berarti bahwa semua efek operasi baca dan tulis Txn1 tampaknya telah terjadi pada satu titik waktu (c1), dan semua efek operasi baca dan tulis Txn2 tampaknya telah terjadi pada satu titik waktu (c2). Selain itu, c1 < c2 (yang dijamin karena Txn1 dan Txn2 melakukan operasi tulis; hal ini berlaku meskipun operasi tulis terjadi di mesin yang berbeda), yang mengikuti urutan Txn1 yang terjadi sebelum Txn2. (Namun, jika Txn2 hanya melakukan operasi baca dalam transaksi, maka c1 <= c2).

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

Jaminan baca dan tulis

Jika panggilan untuk menjalankan transaksi gagal, jaminan baca dan tulis yang Anda miliki bergantung pada error yang menyebabkan panggilan commit yang mendasarinya gagal.

Misalnya, error seperti "Baris Tidak Ditemukan" atau "Baris Sudah Ada" berarti penulisan mutasi yang dibuffer mengalami beberapa error, misalnya baris yang dicoba diperbarui oleh klien tidak ada. Dalam hal ini, operasi baca dijamin konsisten, operasi tulis tidak diterapkan, dan tidak adanya baris juga dijamin konsisten dengan operasi baca.

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 operasi baca berdasarkan hasil awal yang diterima dari operasi baca) tanpa memengaruhi operasi lain yang ada dalam transaksi.

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

Perhatikan bahwa tidak seperti operasi baca, membatalkan operasi Commit transaksi akan mengakibatkan pembatalan transaksi (kecuali jika transaksi telah Di-commit atau gagal karena alasan lain).

Performa

Mengunci

Spanner memungkinkan beberapa klien 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 operasi baca sebagai bagian dari transaksi, Spanner akan memperoleh kunci baca bersama, yang memungkinkan operasi baca lain tetap mengakses data hingga transaksi Anda siap di-commit. Saat transaksi Anda melakukan commit dan operasi tulis diterapkan, transaksi akan mencoba mengupgrade ke kunci eksklusif. Tindakan 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 kunci:

  • 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 ada konflik.
  • Operasi tulis ke item data yang juga tidak membaca data yang ditulis (alias "tulis buta") tidak bertentangan dengan penulis buta lain dari item yang sama (stempel waktu commit setiap operasi tulis menentukan urutan penerapannya ke database). Konsekuensinya adalah Spanner hanya perlu mengupgrade ke kunci eksklusif jika Anda telah membaca data yang Anda tulis. 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 pada 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, misalnya, saat mengizinkan data berpindah di seluruh resource komputasi instance. Jika transaksi dicoba lagi, baik secara eksplisit oleh kode aplikasi atau secara implisit oleh kode klien seperti driver JDBC Spanner, hanya dijamin bahwa kunci ditahan selama upaya yang benar-benar dilakukan.

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

Deteksi deadlock

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

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

Dengan memberikan prioritas pada transaksi yang lebih lama, Spanner memastikan bahwa setiap transaksi memiliki peluang untuk memperoleh kunci pada akhirnya, setelah cukup lama sehingga memiliki prioritas 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. Kemampuan ini memiliki biaya performa dibandingkan dengan transaksi satu server.

Jenis transaksi apa yang mungkin didistribusikan? Di balik layar, Spanner dapat membagi tanggung jawab untuk baris di database di banyak server. Baris dan baris yang sesuai dalam tabel yang diselingi biasanya ditayangkan oleh server yang sama, seperti dua baris dalam tabel yang sama dengan kunci yang berdekatan. Spanner dapat melakukan transaksi di seluruh baris di server yang berbeda; namun, sebagai aturan umum, transaksi yang memengaruhi banyak baris yang berlokasi sama lebih cepat dan lebih murah daripada transaksi yang memengaruhi banyak baris yang tersebar di seluruh database, atau di seluruh tabel besar.

Transaksi yang paling efisien di Spanner hanya menyertakan operasi baca dan tulis yang harus diterapkan secara atomik. Transaksi paling cepat jika semua pembacaan dan penulisan mengakses data di bagian yang sama dari ruang kunci.

Transaksi hanya baca

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

Gunakan transaksi hanya baca jika Anda perlu menjalankan lebih dari satu operasi baca pada stempel waktu yang sama. Jika Anda dapat mengekspresikan pembacaan menggunakan salah satu metode pembacaan tunggal Spanner, Anda harus menggunakan metode pembacaan 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 guna membaca data secara paralel.

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

Properti

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

Antarmuka

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

Contoh

Berikut ini cara menggunakan transaksi hanya baca untuk mendapatkan data yang konsisten untuk dua operasi baca 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(SpannerTransactionCreationOptions.ReadOnly, options: null, cancellationToken: default);
        using var cmd = connection.CreateSelectCommand("SELECT SingerId, AlbumId, AlbumTitle FROM Albums");

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

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

Go


import (
	"context"
	"fmt"
	"io"

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

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

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

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

Java

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

Node.js

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

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

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

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

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

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

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

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

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

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

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

PHP

use Google\Cloud\Spanner\SpannerClient;

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

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

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

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

Python

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

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

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

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

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

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

Ruby

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

require "google/cloud/spanner"

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

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

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

Transaksi DML yang dipartisi

Dengan menggunakan Partitioned Data Manipulation Language (DML Berpartisi), Anda dapat mengeksekusi pernyataan UPDATE dan DELETE berskala besar tanpa mengalami batas transaksi atau mengunci seluruh tabel. Spanner mempartisi ruang kunci dan menjalankan pernyataan DML di setiap partisi dalam transaksi baca-tulis terpisah.

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

Properti

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

Transaksi yang dipartisi tidak mendukung commit atau rollback. Spanner 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 pada partisi yang telah dieksekusi.

Antarmuka

Spanner menyediakan antarmuka untuk menjalankan satu pernyataan DML Partisi.

Contoh

Contoh kode berikut memperbarui kolom MarketingBudget dari 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 Berpartisi.


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


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

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

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

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

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

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