Transaksi

Datastore mendukung transaksi. Transaksi adalah operasi atau serangkaian operasi yang bersifat atomik—baik semua operasi dalam transaksi terjadi maupun tidak ada yang terjadi. Aplikasi dapat menjalankan beberapa operasi dan penghitungan dalam satu transaksi.

Menggunakan transaksi

Transaksi adalah kumpulan operasi Datastore pada satu atau beberapa entity. Setiap transaksi dijamin bersifat atomik, yang berarti bahwa transaksi itu tidak pernah diterapkan sebagian. Semua operasi dalam transaksi akan diterapkan, atau tidak ada yang diterapkan. Transaksi memiliki durasi maksimum 60 detik dengan masa berlaku tidak ada aktivitas selama 10 detik setelah 30 detik.

Operasi mungkin gagal jika:

  • Terlalu banyak perubahan serentak yang dicoba di entity group yang sama.
  • Transaksi melebihi batas resource.
  • Datastore mengalami error internal.

Dalam semua kasus ini, Datastore API memunculkan pengecualian.

Transaksi adalah fitur opsional Datastore; Anda tidak perlu menggunakan transaksi untuk melakukan operasi Datastore.

Berikut adalah contoh pembaruan kolom bernama vacationDays dalam entity jenis Employee yang bernama Joe:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Transaction txn = datastore.beginTransaction();
try {
  Key employeeKey = KeyFactory.createKey("Employee", "Joe");
  Entity employee = datastore.get(employeeKey);
  employee.setProperty("vacationDays", 10);

  datastore.put(txn, employee);

  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Perhatikan bahwa agar contoh kita lebih ringkas, terkadang kita menghilangkan blok finally yang melakukan rollback jika transaksi masih aktif. Dalam kode produksi, penting untuk memastikan bahwa setiap transaksi di-commit atau di-roll back secara eksplisit.

Entity group

Setiap entity merupakan bagian dari entity group, kumpulan dari satu atau beberapa entity yang dapat dimanipulasi dalam satu transaksi. Hubungan entity group memberi tahu App Engine untuk menyimpan beberapa entity di bagian yang sama pada jaringan terdistribusi. Suatu transaksi akan menyiapkan operasi Datastore untuk entity group, dan semua operasi tersebut akan diterapkan sebagai grup, atau tidak sama sekali jika transaksi gagal.

Saat membuat entity, aplikasi dapat menetapkan entity lain sebagai parent entity baru. Menetapkan induk ke entity baru akan menempatkan entity baru dalam entity group yang sama dengan entity induk.

Entity tanpa induk adalah entity root. Entity yang merupakan induk untuk entity lain juga bisa memiliki induk. Rantai parent entity dari entity hingga root adalah jalur untuk entity, dan anggota jalur tersebut adalah ancestor entity. Induk entity ditentukan saat entity dibuat, dan tidak dapat diubah nanti.

Setiap entity dengan root entity yang ditentukan sebagai ancestor berada dalam grup entity yang sama. Semua entity dalam grup disimpan di node Datastore yang sama. Satu transaksi dapat mengubah beberapa entity dalam satu grup, atau menambahkan entity baru ke grup dengan menjadikan induk entity baru menjadi entity yang ada dalam grup. Cuplikan kode berikut menunjukkan transaksi di berbagai jenis entity:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity person = new Entity("Person", "tom");
datastore.put(person);

// Transactions on root entities
Transaction txn = datastore.beginTransaction();

Entity tom = datastore.get(person.getKey());
tom.setProperty("age", 40);
datastore.put(txn, tom);
txn.commit();

// Transactions on child entities
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photo = new Entity("Photo", tom.getKey());

// Create a Photo that is a child of the Person entity named "tom"
photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photo);
txn.commit();

// Transactions on entities in different entity groups
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photoNotAChild = new Entity("Photo");
photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photoNotAChild);

// Throws IllegalArgumentException because the Person entity
// and the Photo entity belong to different entity groups.
txn.commit();

Membuat entity dalam entity group tertentu

Saat aplikasi membuat entity baru, Anda dapat menetapkannya ke entity group dengan menyediakan kunci entity lain. Contoh di bawah membuat kunci entity MessageBoard, lalu menggunakan kunci tersebut untuk membuat dan mempertahankan entity Message yang berada dalam entity group yang sama dengan MessageBoard:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

String messageTitle = "Some Title";
String messageText = "Some message.";
Date postDate = new Date();

Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);

Entity message = new Entity("Message", messageBoardKey);
message.setProperty("message_title", messageTitle);
message.setProperty("message_text", messageText);
message.setProperty("post_date", postDate);

Transaction txn = datastore.beginTransaction();
datastore.put(txn, message);

txn.commit();

Menggunakan transaksi lintas grup

Transaksi lintas-grup (juga disebut transaksi XG) beroperasi di beberapa grup entity, berperilaku seperti transaksi satu grup seperti dijelaskan di atas, kecuali bahwa transaksi lintas-grup tidak akan gagal jika kode mencoba memperbarui entity dari lebih dari satu entity group.

Menggunakan transaksi lintas grup mirip dengan menggunakan transaksi grup tunggal, bedanya, Anda perlu menentukan bahwa Anda ingin transaksi tersebut menjadi lintas grup saat Anda memulai transaksi, menggunakan TransactionOptions:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
TransactionOptions options = TransactionOptions.Builder.withXG(true);
Transaction txn = datastore.beginTransaction(options);

Entity a = new Entity("A");
a.setProperty("a", 22);
datastore.put(txn, a);

Entity b = new Entity("B");
b.setProperty("b", 11);
datastore.put(txn, b);

txn.commit();

Yang dapat dilakukan dalam transaksi

Datastore memberlakukan pembatasan pada apa yang dapat dilakukan di dalam satu transaksi.

Semua operasi Datastore dalam transaksi harus beroperasi di entity dalam entity group jika transaksi merupakan transaksi satu grup, atau di entitas dalam maksimum dua puluh lima entity group jika transaksi merupakan transaksi lintas grup. Ini termasuk membuat kueri untuk entity berdasarkan ancestor, mengambil entity berdasarkan kunci, mengupdate entity, dan menghapus entity. Perhatikan bahwa setiap root entity adalah milik entity group terpisah, sehingga satu transaksi tidak dapat membuat atau beroperasi di lebih dari satu root entity kecuali jika merupakan transaksi lintas grup.

Jika dua atau beberapa transaksi secara bersamaan mencoba mengubah entity dalam satu atau beberapa entity group umum, hanya transaksi pertama yang meng-commit perubahannya yang dapat berhasil; sementara yang lain akan gagal saat commit. Karena desain ini, penggunaan entity group akan membatasi jumlah operasi tulis serentak yang dapat Anda lakukan pada entity mana pun dalam grup. Saat transaksi dimulai, Datastore menggunakan kontrol konkurensi optimis dengan memeriksa waktu update terakhir untuk entity group yang digunakan dalam transaksi. Setelah melakukan transaksi untuk entity group, Datastore kembali memeriksa waktu pembaruan terakhir untuk entity group yang digunakan dalam transaksi. Jika telah berubah sejak pemeriksaan awal, pengecualian akan ditampilkan.

Aplikasi dapat menjalankan kueri selama transaksi, tetapi hanya jika aplikasi menyertakan filter ancestor. Aplikasi juga bisa mendapatkan entity Datastore berdasarkan kunci selama transaksi. Anda dapat menyiapkan kunci sebelum transaksi, atau mem-build kunci di dalam transaksi dengan nama atau ID kunci.

Isolasi dan konsistensi

Di luar transaksi, tingkat isolasi Datastore itu paling dekat dengan batas baca yang di-commit. Di dalam transaksi, isolasi yang dapat di-serialisasi akan diterapkan. Artinya, transaksi lain tidak dapat mengubah secara serentak data yang dibaca atau diubah oleh transaksi ini.

Dalam transaksi, semua operasi baca mencerminkan status terbaru dan konsisten Datastore pada saat transaksi dimulai. Kueri dan aktivitas yang ada di dalam transaksi dijamin akan melihat satu snapshot Datastore yang konsisten sejak awal transaksi. Entity dan baris indeks dalam entity group transaksi diperbarui sepenuhnya sehingga kueri menampilkan kumpulan entity hasil yang lengkap dan benar, tanpa positif palsu (PP) atau negatif palsu (NP) yang dapat terjadi dalam kueri di luar transaksi.

Tampilan snapshot yang konsisten ini juga meluas ke operasi baca setelah operasi tulis di dalam transaksi. Tidak seperti kebanyakan database, kueri dan get di dalam transaksi Datastore tidak melihat hasil penulisan sebelumnya di dalam transaksi tersebut. Khususnya, jika suatu entity diubah atau dihapus dalam transaksi, kueri atau get akan menampilkan versi asli entity tersebut sejak awal transaksi, atau tidak sama sekali jika entity tersebut belum ada pada saat itu.

Penggunaan untuk transaksi

Contoh ini menunjukkan salah satu penggunaan transaksi: memperbarui entity dengan nilai properti baru yang relatif terhadap nilainya saat ini. Karena Datastore API tidak mencoba ulang transaksi, kita dapat menambahkan logika untuk transaksi yang akan dicoba ulang jika permintaan lain mengupdate MessageBoard yang sama atau salah satu Messages-nya di saat bersamaan.

int retries = 3;
while (true) {
  Transaction txn = datastore.beginTransaction();
  try {
    Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
    Entity messageBoard = datastore.get(boardKey);

    long count = (Long) messageBoard.getProperty("count");
    ++count;
    messageBoard.setProperty("count", count);
    datastore.put(txn, messageBoard);

    txn.commit();
    break;
  } catch (ConcurrentModificationException e) {
    if (retries == 0) {
      throw e;
    }
    // Allow retry to occur
    --retries;
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

Tindakan ini memerlukan transaksi karena nilai mungkin diperbarui oleh pengguna lain setelah kode ini mengambil objek, tetapi sebelum menyimpan objek yang diubah. Tanpa transaksi, permintaan pengguna akan menggunakan nilai count sebelum diupdate pengguna lain, dan penyimpanan akan menimpa nilai baru. Dengan transaksi, aplikasi diberi tahu tentang update pengguna lain. Jika entity diperbarui selama transaksi, transaksi akan gagal dengan ConcurrentModificationException. Aplikasi dapat mengulangi transaksi untuk menggunakan data baru.

Penggunaan umum lainnya untuk transaksi adalah mengambil entity dengan kunci bernama, atau membuatnya jika belum ada:

Transaction txn = datastore.beginTransaction();
Entity messageBoard;
Key boardKey;
try {
  boardKey = KeyFactory.createKey("MessageBoard", boardName);
  messageBoard = datastore.get(boardKey);
} catch (EntityNotFoundException e) {
  messageBoard = new Entity("MessageBoard", boardName);
  messageBoard.setProperty("count", 0L);
  boardKey = datastore.put(txn, messageBoard);
}
txn.commit();

Seperti sebelumnya, transaksi diperlukan untuk menangani kasus saat pengguna lain mencoba membuat atau memperbarui entity dengan ID string yang sama. Tanpa transaksi, jika entity tidak ada dan dua pengguna mencoba untuk membuatnya, yang kedua akan mengganti entity pertama tanpa menyadarinya. Dengan transaksi, upaya kedua gagal secara atomik. Jika perlu dilakukan, aplikasi dapat mencoba lagi untuk mengambil entity dan mengupdatenya.

Jika transaksi gagal, Anda dapat meminta aplikasi mencoba kembali transaksi tersebut hingga berhasil, atau membiarkan pengguna menangani error dengan menyebarkannya ke level antarmuka pengguna aplikasi. Anda tidak perlu membuat loop percobaan ulang di setiap transaksi.

Terakhir, Anda dapat menggunakan transaksi untuk membaca snapshot yang konsisten dari Datastore. Hal ini dapat berguna saat beberapa operasi baca diperlukan untuk merender halaman atau mengekspor data yang harus konsisten. Jenis transaksi ini sering disebut sebagai transaksi hanya baca, karena tidak melakukan penulisan. Transaksi grup tunggal hanya baca tidak pernah gagal karena perubahan serentak, sehingga Anda tidak perlu menerapkan percobaan ulang jika gagal. Namun, transaksi lintas grup dapat gagal karena perubahan serentak, sehingga transaksi tersebut harus dilakukan percobaan ulang. Melakukan commit dan roll back transaksi hanya baca bersifat tanpa pengoperasian.

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// Display information about a message board and its first 10 messages.
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);

Transaction txn = datastore.beginTransaction();

Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");

Query q = new Query("Message", boardKey);

// This is an ancestor query.
PreparedQuery pq = datastore.prepare(txn, q);
List<Entity> messages = pq.asList(FetchOptions.Builder.withLimit(10));

txn.commit();

Antrean tugas transaksional

Anda dapat menambahkan tugas ke dalam antrean sebagai bagian dari transaksi Datastore, sehingga tugas tersebut hanya diantrekan—dan dijamin diantrekan—jika transaksi berhasil di-commit. Jika transaksi benar-benar di-commit, tugas dijamin akan diantrekan. Setelah diantrekan, tugas tersebut tidak dijamin akan segera dieksekusi dan operasi apa pun yang dilakukan dalam tugas akan dieksekusi secara terpisah dari transaksi asli. Tugas akan dicoba ulang sampai berhasil. Hal ini berlaku untuk tugas apa pun yang diantrekan dalam konteks transaksi.

Tugas transaksional berguna karena memungkinkan Anda meminta tindakan non-Datastore dalam transaksi Datastore (seperti mengirim email untuk mengonfirmasi pembelian). Anda juga dapat mengaitkan tindakan Datastore ke transaksi, seperti melakukan perubahan pada entity group tambahan di luar transaksi jika dan hanya jika transaksi berhasil.

Aplikasi tidak dapat menyisipkan lebih dari lima tugas transaksional ke dalam task queue selama satu transaksi. Tugas transaksi tidak boleh menggunakan nama yang ditentukan pengguna.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();