Jika Anda pernah mencurahkan banyak waktu untuk manajemen transaksi basis data Django, Anda tahu betapa membingungkannya hal itu. Di masa lalu, dokumentasi memberikan sedikit kedalaman, tetapi pemahaman hanya datang melalui membangun dan bereksperimen.
Ada banyak dekorator untuk dikerjakan, seperti commit_on_success
, commit_manually
, commit_unless_managed
, rollback_unless_managed
, enter_transaction_management
, leave_transaction_management
, hanya untuk beberapa nama. Untungnya, dengan Django 1.6 itu semua keluar dari pintu. Anda benar-benar hanya perlu tahu tentang beberapa fungsi sekarang. Dan kita akan mendapatkannya hanya dalam sedetik. Pertama, kita akan membahas topik ini:
- Apa itu manajemen transaksi?
- Apa yang salah dengan manajemen transaksi sebelum Django 1.6?
Sebelum melompat ke:
- Apa yang benar tentang manajemen transaksi di Django 1.6?
Dan kemudian berurusan dengan contoh terperinci:
- Contoh Garis
- Transaksi
- Cara yang disarankan
- Menggunakan dekorator
- Transaksi per Permintaan HTTP
- SavePoints
- Transaksi Bertingkat
Apa itu transaksi?
Menurut SQL-92, "Transaksi SQL (kadang-kadang hanya disebut "transaksi") adalah urutan eksekusi pernyataan SQL yang bersifat atomik sehubungan dengan pemulihan". Dengan kata lain, semua pernyataan SQL dieksekusi dan dikomit bersama. Demikian juga, ketika digulung kembali, semua pernyataan digulung kembali.
Misalnya:
# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT
Jadi transaksi adalah satu unit kerja dalam database. Dan satu unit kerja itu dibatasi oleh transaksi awal dan kemudian komit atau rollback eksplisit.
Apa yang salah dengan manajemen transaksi sebelum Django 1.6?
Untuk menjawab pertanyaan ini sepenuhnya, kita harus membahas bagaimana transaksi ditangani dalam basis data, pustaka klien, dan di dalam Django.
Database
Setiap pernyataan dalam database harus dijalankan dalam suatu transaksi, meskipun transaksi tersebut hanya mencakup satu pernyataan.
Sebagian besar database memiliki AUTOCOMMIT
pengaturan, yang biasanya disetel ke True sebagai default. AUTOCOMMIT
. ini membungkus setiap pernyataan dalam transaksi yang segera dilakukan jika pernyataan berhasil. Tentu saja Anda dapat memanggil sesuatu seperti START_TRANSACTION
. secara manual yang akan menangguhkan sementara AUTOCOMMIT
sampai Anda menelepon COMMIT_TRANSACTION
atau ROLLBACK
.
Namun, kesimpulannya di sini adalah AUTOCOMMIT
pengaturan menerapkan komit implisit setelah setiap pernyataan .
Perpustakaan Klien
Lalu ada pustaka klien Python seperti sqlite3 dan mysqldb, yang memungkinkan program Python untuk berinteraksi dengan database itu sendiri. Pustaka semacam itu mengikuti serangkaian standar tentang cara mengakses dan mengkueri database. Standar tersebut, DB API 2.0, dijelaskan dalam PEP 249. Meskipun mungkin membuat beberapa pembacaan agak kering, hal penting yang perlu diperhatikan adalah bahwa PEP 249 menyatakan bahwa database AUTOCOMMIT
harus MATI secara default.
Ini jelas bertentangan dengan apa yang terjadi di dalam database:
- Pernyataan SQL selalu harus dijalankan dalam transaksi, yang biasanya dibuka oleh database untuk Anda melalui
AUTOCOMMIT
. - Namun, menurut PEP 249, hal ini tidak boleh terjadi.
- Library klien harus mencerminkan apa yang terjadi di dalam database, tetapi karena mereka tidak diizinkan untuk mengubah
AUTOCOMMIT
aktif secara default, mereka hanya membungkus pernyataan SQL Anda dalam sebuah transaksi, seperti database.
Oke. Tetap bersamaku sedikit lebih lama.
Django
Masukkan Django. Django juga memiliki sesuatu untuk dikatakan tentang manajemen transaksi. Di Django 1.5 dan sebelumnya, Django pada dasarnya menjalankan dengan transaksi terbuka dan melakukan transaksi itu secara otomatis saat Anda menulis data ke database. Jadi setiap kali Anda memanggil sesuatu seperti model.save()
atau model.update()
, Django menghasilkan pernyataan SQL yang sesuai dan melakukan transaksi.
Juga di Django 1.5 dan sebelumnya, disarankan agar Anda menggunakan TransactionMiddleware
untuk mengikat transaksi ke permintaan HTTP. Setiap permintaan diberi transaksi. Jika respons dikembalikan tanpa pengecualian, Django akan melakukan transaksi tetapi jika fungsi tampilan Anda menimbulkan kesalahan, ROLLBACK
akan dipanggil. Akibatnya, matikan AUTOCOMMIT
. Jika Anda menginginkan manajemen transaksi gaya komit otomatis tingkat basis data, Anda harus mengelola sendiri transaksi - biasanya dengan menggunakan penghias transaksi pada fungsi tampilan Anda seperti @transaction.commit_manually
, atau @transaction.commit_on_success
.
Mengambil napas. Atau dua.
Apa artinya ini?
Ya, ada banyak hal yang terjadi di sana, dan ternyata sebagian besar pengembang hanya menginginkan autocommit tingkat basis data standar - artinya transaksi tetap berada di belakang layar, melakukan tugasnya, hingga Anda perlu menyesuaikannya secara manual.
Apa yang benar tentang manajemen transaksi di Django 1.6?
Sekarang, selamat datang di Django 1.6. Lakukan yang terbaik untuk melupakan semua yang baru saja kita bicarakan dan cukup ingat bahwa di Django 1.6, Anda menggunakan basis data AUTOCOMMIT
dan mengelola transaksi secara manual bila diperlukan. Pada dasarnya, kami memiliki model yang jauh lebih sederhana yang pada dasarnya melakukan apa yang awalnya dirancang untuk dilakukan oleh database.
Cukup teori. Ayo kode.
Contoh Garis
Di sini kita memiliki contoh fungsi tampilan yang menangani pendaftaran pengguna dan memanggil Stripe untuk pemrosesan kartu kredit.
def register(request):
user = None
if request.method == 'POST':
form = UserForm(request.POST)
if form.is_valid():
customer = Customer.create("subscription",
email = form.cleaned_data['email'],
description = form.cleaned_data['name'],
card = form.cleaned_data['stripe_token'],
plan="gold",
)
cd = form.cleaned_data
try:
user = User.create(cd['name'], cd['email'], cd['password'],
cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
else:
request.session['user'] = user.pk
return HttpResponseRedirect('/')
else:
form = UserForm()
return render_to_response(
'register.html',
{
'form': form,
'months': range(1, 12),
'publishable': settings.STRIPE_PUBLISHABLE,
'soon': soon(),
'user': user,
'years': range(2011, 2036),
},
context_instance=RequestContext(request)
)
Tampilan ini pertama kali memanggil Customer.create
yang sebenarnya memanggil Stripe untuk menangani pemrosesan kartu kredit. Kemudian kita membuat pengguna baru. Jika kami mendapat tanggapan kembali dari Stripe, kami memperbarui pelanggan yang baru dibuat dengan stripe_id
. Jika kami tidak mendapatkan pelanggan kembali (Stripe tidak aktif), kami akan menambahkan entri ke UnpaidUsers
tabel dengan email pelanggan yang baru dibuat, sehingga kami dapat meminta mereka untuk mencoba kembali detail kartu kredit mereka nanti.
Idenya adalah bahwa meskipun Stripe tidak aktif, pengguna masih dapat mendaftar dan mulai menggunakan situs kami. Kami hanya akan meminta mereka lagi di kemudian hari untuk info kartu kredit.
Saya mengerti ini mungkin sedikit contoh yang dibuat-buat, dan ini bukan cara saya menerapkan fungsi tersebut jika harus, tetapi tujuannya adalah untuk mendemonstrasikan transaksi.
Maju. Memikirkan tentang transaksi, dan mengingat bahwa secara default Django 1.6 memberi kita AUTOCOMMIT
perilaku untuk database kita, mari kita lihat kode terkait database sedikit lebih lama.
cd = form.cleaned_data
try:
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
# ...
Dapatkah Anda menemukan masalah? Nah apa jadinya jika UnpaidUsers(email=cd['email']).save()
saluran gagal?
Anda akan memiliki pengguna yang terdaftar di sistem, yang menurut sistem telah memverifikasi kartu kredit mereka, tetapi pada kenyataannya mereka belum memverifikasi kartu.
Kami hanya menginginkan satu dari dua hasil:
- Pengguna dibuat (dalam database) dan memiliki
stripe_id
. - Pengguna dibuat (dalam database) dan tidak memiliki
stripe_id
DAN baris terkait diUnpaidUsers
tabel dengan alamat email yang sama dibuat.
Yang berarti kita ingin dua pernyataan database terpisah melakukan keduanya atau keduanya rollback. Kasing yang sempurna untuk transaksi sederhana.
Pertama, mari kita tulis beberapa pengujian untuk memverifikasi bahwa segala sesuatunya berperilaku seperti yang kita inginkan.
@mock.patch('payments.models.UnpaidUsers.save', side_effect = IntegrityError)
def test_registering_user_when_strip_is_down_all_or_nothing(self, save_mock):
#create the request used to test the view
self.request.session = {}
self.request.method='POST'
self.request.POST = {'email' : '[email protected]',
'name' : 'pyRock',
'stripe_token' : '...',
'last_4_digits' : '4242',
'password' : 'bad_password',
'ver_password' : 'bad_password',
}
#mock out stripe and ask it to throw a connection error
with mock.patch('stripe.Customer.create', side_effect =
socket.error("can't connect to stripe")) as stripe_mock:
#run the test
resp = register(self.request)
#assert there is no record in the database without stripe id.
users = User.objects.filter(email="[email protected]")
self.assertEquals(len(users), 0)
#check the associated table also didn't get updated
unpaid = UnpaidUsers.objects.filter(email="[email protected]")
self.assertEquals(len(unpaid), 0)
Dekorator di bagian atas pengujian adalah tiruan yang akan memunculkan 'IntegrityError' ketika kami mencoba menyimpan ke UnpaidUsers
tabel.
Ini untuk menjawab pertanyaan, “Apa yang terjadi jika UnpaidUsers(email=cd['email']).save()
garis gagal?” Bit kode berikutnya hanya membuat sesi tiruan, dengan info yang sesuai yang kita butuhkan untuk fungsi pendaftaran kita. Dan kemudian with mock.patch
memaksa sistem untuk percaya bahwa Stripe sedang down ... akhirnya kita sampai pada ujian.
resp = register(self.request)
Baris di atas hanya memanggil fungsi tampilan register kita yang meneruskan permintaan yang diolok-olok. Kemudian kami hanya memeriksa untuk memastikan tabel tidak diperbarui:
#assert there is no record in the database without stripe_id.
users = User.objects.filter(email="[email protected]")
self.assertEquals(len(users), 0)
#check the associated table also didn't get updated
unpaid = UnpaidUsers.objects.filter(email="[email protected]")
self.assertEquals(len(unpaid), 0)
Jadi seharusnya gagal jika kita menjalankan tes:
======================================================================
FAIL: test_registering_user_when_strip_is_down_all_or_nothing (tests.payments.testViews.RegisterPageTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/j1z0/.virtualenvs/django_1.6/lib/python2.7/site-packages/mock.py", line 1201, in patched
return func(*args, **keywargs)
File "/Users/j1z0/Code/RealPython/mvp_for_Adv_Python_Web_Book/tests/payments/testViews.py", line 266, in test_registering_user_when_strip_is_down_all_or_nothing
self.assertEquals(len(users), 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Bagus. Tampaknya lucu untuk dikatakan tetapi itulah yang kami inginkan. Ingat:kami berlatih TDD di sini. Pesan kesalahan memberi tahu kami bahwa Pengguna memang disimpan dalam database - yang sebenarnya tidak kami inginkan karena mereka tidak membayar!
Transaksi untuk menyelamatkan ...
Transaksi
Sebenarnya ada beberapa cara untuk membuat transaksi di Django 1.6.
Mari kita bahas beberapa.
Cara yang disarankan
Menurut dokumentasi Django 1.6:
“Django menyediakan satu API untuk mengontrol transaksi basis data. […] Atomicity adalah properti yang mendefinisikan transaksi database. atom memungkinkan kita untuk membuat blok kode di mana atom pada database dijamin. Jika blok kode berhasil diselesaikan, perubahan akan dilakukan ke database. Jika ada pengecualian, perubahan akan dibatalkan.”
Atomic dapat digunakan sebagai dekorator atau sebagai context_manager. Jadi jika kita menggunakannya sebagai pengelola konteks, kode dalam fungsi register kita akan terlihat seperti ini:
from django.db import transaction
try:
with transaction.atomic():
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
Perhatikan baris with transaction.atomic()
. Semua kode di dalam blok itu akan dieksekusi di dalam transaksi. Jadi jika kami menjalankan kembali pengujian kami, semuanya harus lulus! Ingat transaksi adalah satu unit kerja, jadi semua yang ada di dalam pengelola konteks akan digulung kembali saat UnpaidUsers
panggilan gagal.
Menggunakan dekorator
Kami juga dapat mencoba menambahkan atom sebagai dekorator.
@transaction.atomic():
def register(request):
# ...snip....
try:
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
Jika kami menjalankan kembali pengujian kami, mereka akan gagal dengan kesalahan yang sama seperti sebelumnya.
Mengapa demikian? Mengapa transaksi tidak dikembalikan dengan benar? Alasannya karena transaction.atomic
sedang mencari semacam Pengecualian dan yah, kami menangkap kesalahan itu (yaitu IntegrityError
di try kami kecuali blok), jadi transaction.atomic
tidak pernah melihatnya dan dengan demikian standar AUTOCOMMIT
fungsi mengambil alih.
Tapi tentu saja menghapus percobaan kecuali akan menyebabkan pengecualian hanya dilemparkan ke rantai panggilan dan kemungkinan besar meledak di tempat lain. Jadi kami juga tidak bisa melakukannya.
Jadi triknya adalah menempatkan manajer konteks atom di dalam blok try kecuali yang kami lakukan dalam solusi pertama kami. Melihat kode yang benar lagi:
from django.db import transaction
try:
with transaction.atomic():
user = User.create(
cd['name'], cd['email'],
cd['password'], cd['last_4_digits'])
if customer:
user.stripe_id = customer.id
user.save()
else:
UnpaidUsers(email=cd['email']).save()
except IntegrityError:
form.addError(cd['email'] + ' is already a member')
Ketika UnpaidUsers
menjalankan IntegrityError
transaction.atomic()
manajer konteks akan menangkapnya dan melakukan rollback. Pada saat kode kami dieksekusi di pengendali pengecualian, (yaitu form.addError
line) rollback akan dilakukan dan kami dapat dengan aman melakukan panggilan basis data jika perlu. Perhatikan juga setiap panggilan database sebelum atau sesudah transaction.atomic()
pengelola konteks tidak akan terpengaruh terlepas dari hasil akhir pengelola_konteks.
Transaksi per Permintaan HTTP
Django 1.6 (seperti 1.5) juga memungkinkan Anda untuk beroperasi dalam mode “Transaksi per permintaan”. Dalam mode ini Django akan secara otomatis membungkus fungsi tampilan Anda dalam sebuah transaksi. Jika fungsi melempar pengecualian, Django akan memutar kembali transaksi, jika tidak, ia akan melakukan transaksi.
Untuk menyiapkannya, Anda harus mengatur ATOMIC_REQUEST
ke True dalam konfigurasi database untuk setiap database yang Anda inginkan untuk memiliki perilaku ini. Jadi di "settings.py" kami, kami membuat perubahan seperti ini:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(SITE_ROOT, 'test.db'),
'ATOMIC_REQUEST': True,
}
}
Dalam praktiknya, ini berperilaku persis seperti jika Anda meletakkan dekorator pada fungsi tampilan kami. Jadi itu tidak sesuai dengan tujuan kami di sini.
Namun perlu diperhatikan bahwa dengan kedua ATOMIC_REQUESTS
dan @transaction.atomic
dekorator masih mungkin untuk menangkap/menangani kesalahan tersebut setelah dilempar dari tampilan. Untuk mengetahui kesalahan tersebut, Anda harus menerapkan beberapa middleware khusus, atau Anda dapat mengganti urls.hadler500 atau dengan membuat template 500.html.
Simpan Poin
Meskipun transaksi bersifat atomik, mereka dapat dipecah lebih lanjut menjadi savepoints. Pikirkan savepoint sebagai transaksi parsial.
Jadi, jika Anda memiliki transaksi yang memerlukan empat pernyataan SQL untuk diselesaikan, Anda dapat membuat savepoint setelah pernyataan kedua. Setelah savepoint dibuat, bahkan jika pernyataan ke-3 atau ke-4 gagal, Anda dapat melakukan rollback sebagian, menghilangkan pernyataan ke-3 dan ke-4 tetapi mempertahankan dua yang pertama.
Jadi pada dasarnya seperti membagi transaksi menjadi transaksi ringan yang lebih kecil yang memungkinkan Anda melakukan sebagian rollback atau commit.
Tetapi perlu diingat jika transaksi utama tempat untuk dibatalkan (mungkin karena
IntegrityError
yang dinaikkan dan tidak tertangkap, maka semua savepoint akan dikembalikan juga).
Mari kita lihat contoh cara kerja savepoint.
@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
Di sini seluruh fungsi ada dalam sebuah transaksi. Setelah membuat pengguna baru, kami membuat savepoint dan mendapatkan referensi ke savepoint. Tiga pernyataan berikutnya-
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
-bukan bagian dari savepoint yang ada, jadi mereka berpeluang menjadi bagian dari savepoint_rollback
berikutnya , atau savepoint_commit
. Dalam kasus savepoint_rollback
, baris user = User.create('jj','inception','jj','1234')
akan tetap dikomit ke database meskipun pembaruan lainnya tidak.
Dengan kata lain, dua tes berikut ini menjelaskan cara kerja savepoint:
def test_savepoint_rollbacks(self):
self.save_points(False)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#note the values here are from the original create call
self.assertEquals(users[0].stripe_id, '')
self.assertEquals(users[0].name, 'jj')
def test_savepoint_commit(self):
self.save_points(True)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#note the values here are from the update calls
self.assertEquals(users[0].stripe_id, '4')
self.assertEquals(users[0].name, 'starting down the rabbit hole')
Juga setelah kami melakukan atau mengembalikan savepoint kami dapat terus melakukan pekerjaan dalam transaksi yang sama. Dan pekerjaan itu tidak akan terpengaruh oleh hasil dari savepoint sebelumnya.
Misalnya jika kita memperbarui save_points
our berfungsi sebagai berikut:
@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.save()
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
user.create('limbo','illbehere@forever','mind blown',
'1111')
Terlepas dari apakah savepoint_commit
atau savepoint_rollback
disebut pengguna 'limbo' akan tetap berhasil dibuat. Kecuali ada hal lain yang menyebabkan seluruh transaksi dibatalkan.
Transaksi Bertingkat
Selain menentukan savepoint secara manual, dengan savepoint()
, savepoint_commit
, dan savepoint_rollback
, membuat Transaksi bersarang akan secara otomatis membuat savepoint untuk kita, dan mengembalikannya jika kita mendapatkan kesalahan.
Memperluas contoh kita sedikit lebih jauh, kita mendapatkan:
@transaction.atomic()
def save_points(self,save=True):
user = User.create('jj','inception','jj','1234')
sp1 = transaction.savepoint()
user.name = 'starting down the rabbit hole'
user.save()
user.stripe_id = 4
user.save()
if save:
transaction.savepoint_commit(sp1)
else:
transaction.savepoint_rollback(sp1)
try:
with transaction.atomic():
user.create('limbo','illbehere@forever','mind blown',
'1111')
if not save: raise DatabaseError
except DatabaseError:
pass
Di sini kita dapat melihat bahwa setelah kita berurusan dengan savepoint kita, kita menggunakan transaction.atomic
manajer konteks untuk membungkus kreasi pengguna 'limbo' kami. Saat pengelola konteks itu dipanggil, itu berlaku untuk membuat savepoint (karena kita sudah dalam transaksi) dan savepoint itu akan dikomit atau dibatalkan setelah keluar dari pengelola konteks.
Jadi, dua tes berikut menggambarkan perilaku mereka:
def test_savepoint_rollbacks(self):
self.save_points(False)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#savepoint was rolled back so we should have original values
self.assertEquals(users[0].stripe_id, '')
self.assertEquals(users[0].name, 'jj')
#this save point was rolled back because of DatabaseError
limbo = User.objects.filter(email="illbehere@forever")
self.assertEquals(len(limbo),0)
def test_savepoint_commit(self):
self.save_points(True)
#verify that everything was stored
users = User.objects.filter(email="inception")
self.assertEquals(len(users), 1)
#savepoint was committed
self.assertEquals(users[0].stripe_id, '4')
self.assertEquals(users[0].name, 'starting down the rabbit hole')
#save point was committed by exiting the context_manager without an exception
limbo = User.objects.filter(email="illbehere@forever")
self.assertEquals(len(limbo),1)
Jadi pada kenyataannya Anda dapat menggunakan atomic
atau savepoint
untuk membuat savepoints di dalam transaksi. Dengan atomic
Anda tidak perlu khawatir secara eksplisit tentang komit / rollback, sedangkan dengan savepoint
Anda memiliki kendali penuh saat itu terjadi.
Kesimpulan
Jika Anda memiliki pengalaman sebelumnya dengan versi sebelumnya dari transaksi Django, Anda dapat melihat betapa sederhananya model transaksi tersebut. Juga memiliki AUTOCOMMIT
on secara default adalah contoh yang bagus dari default "waras" yang Django dan Python keduanya banggakan dalam penyampaiannya. Untuk banyak sistem Anda tidak perlu berurusan langsung dengan transaksi, biarkan AUTOCOMMIT
melakukan pekerjaannya. Tetapi jika Anda melakukannya, semoga posting ini memberi Anda informasi yang Anda butuhkan untuk mengelola transaksi di Django seperti seorang profesional.