Transaksi

Catatan: Developer yang membuat aplikasi baru sangat dianjurkan untuk menggunakan Library Klien NDB, yang memiliki beberapa manfaat dibandingkan dengan library klien ini, seperti menyimpan entity dalam cache secara otomatis melalui Memcache API. Jika saat ini Anda menggunakan Library Klien DB yang lebih lama, baca Panduan Migrasi DB ke NDB

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 menjalankan operasi Datastore.

Aplikasi dapat mengeksekusi kumpulan pernyataan dan operasi penyimpanan data dalam satu transaksi, sehingga jika ada pernyataan atau operasi yang memunculkan pengecualian, tidak ada operasi Datastore dalam kumpulan yang diterapkan. Aplikasi ini menentukan tindakan yang akan dilakukan dalam transaksi menggunakan fungsi Python. Aplikasi memulai transaksi menggunakan salah satu metode run_in_transaction, bergantung pada apakah transaksi tersebut mengakses entity dalam satu entity group atau apakah transaksi tersebut merupakan transaksi lintas grup.

Untuk kasus penggunaan umum pada fungsi yang hanya digunakan dalam transaksi, gunakan dekorator @db.transactional:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

@db.transactional
def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

increment_counter(acc.key(), 5)

Jika fungsi terkadang dipanggil tanpa transaksi, daripada mendekorasinya, panggil db.run_in_transaction() dengan fungsi tersebut sebagai argumen:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

db.run_in_transaction() mengambil objek fungsi, serta argumen posisi dan kata kunci untuk diteruskan ke fungsi. Jika fungsi menampilkan nilai, db.run_in_transaction() akan menampilkan nilai tersebut.

Jika fungsi tersebut ditampilkan, berarti transaksi akan di-commit, dan semua efek dari operasi Datastore akan diterapkan. Jika fungsi tersebut memunculkan pengecualian, transaksi akan "di-roll back", dan efeknya tidak diterapkan. Lihat catatan di atas tentang pengecualian.

Jika satu fungsi transaksi dipanggil dari dalam transaksi lain, @db.transactional dan db.run_in_transaction() memiliki perilaku default yang berbeda. @db.transactional akan mengizinkan hal ini, dan transaksi dalam menjadi transaksi yang sama dengan transaksi luar. Memanggil db.run_in_transaction() akan mencoba "menyusun" transaksi lain dalam transaksi yang ada; tetapi perilaku ini belum didukung, dan akan memunculkan db.BadRequestError. Anda dapat menentukan perilaku lainnya; lihat referensi fungsi di opsi transaksi untuk detailnya.

Menggunakan Transaksi Lintas Grup (XG)

Transaksi lintas-grup, yang beroperasi di beberapa entity group, berperilaku seperti transaksi satu grup, tetapi tidak akan gagal jika kode mencoba memperbarui entity di lebih dari satu entity group. Untuk memanggil transaksi lintas grup, gunakan opsi transaksi.

Menggunakan @db.transactional:

from google.appengine.ext import db

@db.transactional(xg=True)
def make_things():
  thing1 = Thing(a=3)
  thing1.put()
  thing2 = Thing(a=7)
  thing2.put()

make_things()

Menggunakan db.run_in_transaction_options:

from google.appengine.ext import db

xg_on = db.create_transaction_options(xg=True)

def my_txn():
    x = MyModel(a=3)
    x.put()
    y = MyModel(a=7)
    y.put()

db.run_in_transaction_options(xg_on, my_txn)

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 membangun kunci di dalam transaksi dengan nama atau ID kunci.

Semua kode Python lainnya diizinkan di dalam fungsi transaksi. Anda dapat menentukan apakah cakupan saat ini disusun dalam fungsi transaksi menggunakan db.is_in_transaction(). Fungsi transaksi tidak boleh memiliki efek samping selain operasi Datastore. Fungsi transaksi dapat dipanggil beberapa kali jika operasi Datastore gagal karena ada pengguna lain yang memperbarui entity dalam entity group secara bersamaan. Jika hal ini terjadi, Datastore API akan mencoba kembali transaksi dalam jumlah yang tetap. Jika semuanya gagal, db.run_in_transaction() akan memunculkan TransactionFailedError. Anda dapat menyesuaikan frekuensi transaksi dicoba ulang menggunakan db.run_in_transaction_custom_retries(), alih-alih db.run_in_transaction().

Demikian pula, fungsi transaksi tidak boleh memiliki efek samping yang bergantung pada keberhasilan transaksi, kecuali jika kode yang memanggil fungsi transaksi mengetahui cara mengurungkan efek tersebut. Misalnya, jika transaksi menyimpan entity Datastore baru, menyimpan ID entity yang dibuat untuk digunakan nanti, lalu transaksi akan gagal, ID yang disimpan tidak merujuk ke entity yang dimaksud karena pembuatan entity tersebut telah di-roll back. Dalam hal ini, kode panggilan harus berhati-hati agar tidak menggunakan ID yang disimpan.

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.

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

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 dari lain. Jika entitas diperbarui selama transaksi, transaksi tersebut akan dicoba lagi hingga semua langkah selesai tanpa gangguan.

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

class SalesAccount(db.Model):
    address = db.PostalAddressProperty()
    phone_number = db.PhoneNumberProperty()

def get_or_create(parent_key, account_id, address, phone_number):
    obj = db.get(db.Key.from_path("SalesAccount", account_id, parent=parent_key))
    if not obj:
        obj = SalesAccount(key_name=account_id,
                           parent=parent_key,
                           address=address,
                           phone_number=phone_number)
        obj.put()
    else:
        obj.address = address
        obj.phone_number = phone_number

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 dicoba ulang, menyadari bahwa entity tersebut sekarang ada, dan mengupdate entity sebagai gantinya.

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.

Get-or-create sangatlah berguna sehingga tersedia metode bawaan untuknya: Model.get_or_insert() mengambil nama kunci, induk opsional, dan argumen untuk diteruskan ke model konstruktor jika entity dengan nama dan jalur tersebut tidak ada. Upaya get dan create terjadi dalam satu transaksi, sehingga (jika transaksi berhasil), metode akan selalu menampilkan instance model yang mewakili entity sebenarnya.

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.

class Customer(db.Model):
    user = db.StringProperty()

class Account(db.Model):
    """An Account has a Customer as its parent."""
    address = db.PostalAddressProperty()
    balance = db.FloatProperty()

def get_all_accounts():
    """Returns a consistent view of the current user's accounts."""
    accounts = []
    for customer in Customer.all().filter('user =', users.get_current_user().user_id()):
        accounts.extend(Account.all().ancestor(customer))
    return accounts

Antrean tugas transaksional

Anda dapat mengantrekan tugas sebagai bagian dari transaksi Datastore, sehingga tugas hanya diantrekan jika transaksi berhasil di-commit. Jika transaksi tidak di-commit, tugas tidak akan diantrekan. Jika transaksi di-commit, tugas akan diantrekan. Setelah diantrekan, tugas tidak akan langsung dieksekusi sehingga tugas tersebut tidak atomik dengan transaksi. Namun, setelah ada dalam antrean, tugas akan dicoba lagi hingga berhasil. Hal ini berlaku untuk setiap tugas yang diantrekan selama fungsi run_in_transaction().

Tugas transaksional berguna karena memungkinkan Anda menggabungkan tindakan non-Datastore ke transaksi yang bergantung pada keberhasilan transaksi (seperti mengirim email untuk mengonfirmasi pembelian). Anda juga dapat mengaitkan tindakan Datastore ke transaksi, misalnya untuk melakukan perubahan pada entity group 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 memiliki nama yang ditentukan pengguna.

def do_something_in_transaction(...)
    taskqueue.add(url='/path/to/my/worker', transactional=True)
  ...

db.run_in_transaction(do_something_in_transaction, ....)