Djangoデータベースのトランザクション管理に多くの時間を費やしたことがあるなら、それがどれほど混乱する可能性があるかをご存知でしょう。過去には、ドキュメントはかなりの深さを提供していましたが、理解は構築と実験によってのみもたらされました。
commit_on_success
のように、使用できるデコレータは多数ありました。 、commit_manually
、commit_unless_managed
、rollback_unless_managed
、enter_transaction_management
、leave_transaction_management
、ほんの数例を挙げると。幸いなことに、Django 1.6では、すべてが戸外に出ています。あなたは本当に今、いくつかの機能について知る必要があるだけです。そして、私たちはすぐにそれらに到達します。まず、次のトピックについて説明します。
- トランザクション管理とは何ですか?
- Django 1.6より前のトランザクション管理の何が問題になっていますか?
ジャンプする前に:
- Django 1.6のトランザクション管理について何が正しいですか?
そして、詳細な例を扱います:
- ストライプの例
- トランザクション
- 推奨される方法
- デコレータの使用
- HTTPリクエストごとのトランザクション
- セーブポイント
- ネストされたトランザクション
トランザクションとは何ですか?
SQL-92によると、「SQLトランザクション(単に「トランザクション」と呼ばれることもあります)は、リカバリに関してアトミックなSQLステートメントの一連の実行です」。つまり、すべてのSQLステートメントが一緒に実行およびコミットされます。同様に、ロールバックすると、すべてのステートメントが一緒にロールバックされます。
例:
# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT
したがって、トランザクションはデータベース内の単一の作業単位です。そして、その単一の作業単位は、トランザクションの開始と、コミットまたは明示的なロールバックによって区切られます。
Django 1.6より前のトランザクション管理の何が問題になっていますか?
この質問に完全に答えるには、データベース、クライアントライブラリ、およびDjango内でトランザクションがどのように処理されるかを検討する必要があります。
データベース
トランザクションにステートメントが1つしかない場合でも、データベース内のすべてのステートメントをトランザクションで実行する必要があります。
ほとんどのデータベースにはAUTOCOMMIT
があります 設定。通常、デフォルトとしてTrueに設定されます。このAUTOCOMMIT
ステートメントが成功した場合にすぐにコミットされるトランザクション内のすべてのステートメントをラップします。もちろん、START_TRANSACTION
のようなものを手動で呼び出すことができます AUTOCOMMIT
を一時的に停止します COMMIT_TRANSACTION
を呼び出すまで またはROLLBACK
。
ただし、ここで重要なのは、AUTOCOMMIT
設定は、各ステートメントの後に暗黙のコミットを適用します 。
クライアントライブラリ
次に、Pythonのクライアントライブラリがあります sqlite3やmysqldbのように、Pythonプログラムがデータベース自体とインターフェイスできるようにします。このようなライブラリは、データベースにアクセスしてクエリを実行する方法に関する一連の標準に従います。その標準であるDBAPI2.0は、PEP 249で説明されています。少し乾燥した読み取りになる可能性がありますが、重要なポイントは、PEP249がデータベースのAUTOCOMMIT
を示していることです。 オフにする必要があります デフォルトで。
これは、データベース内で起こっていることと明らかに矛盾します:
- SQLステートメントは常にトランザクションで実行する必要があります。トランザクションは通常、データベースが
AUTOCOMMIT
を介して開きます。 。 - ただし、PEP 249によると、これは発生しないはずです。
- クライアントライブラリは、データベース内で発生することをミラーリングする必要がありますが、
AUTOCOMMIT
を有効にすることは許可されていないためです。 デフォルトでは、データベースと同じように、SQLステートメントをトランザクションでラップするだけです。
わかった。もう少し私と一緒にいてください。
Django
Djangoと入力します。 Django また、トランザクション管理についても言いたいことがあります。 Django 1.5以前では、Djangoは基本的にオープントランザクションで実行され、データベースにデータを書き込むときにそのトランザクションを自動コミットしました。したがって、model.save()
のようなものを呼び出すたびに またはmodel.update()
、Djangoは適切なSQLステートメントを生成し、トランザクションをコミットしました。
また、Django 1.5以前では、TransactionMiddleware
を使用することをお勧めしました トランザクションをHTTPリクエストにバインドします。各リクエストにはトランザクションが与えられました。応答が例外なく返された場合、Djangoはトランザクションをコミットしますが、ビュー関数がエラーをスローした場合は、ROLLBACK
と呼ばれるでしょう。これは事実上、AUTOCOMMIT
をオフにしました 。標準のデータベースレベルの自動コミットスタイルのトランザクション管理が必要な場合は、トランザクションを自分で管理する必要がありました。通常は、@transaction.commit_manually
などのビュー関数でトランザクションデコレータを使用します。 、または@transaction.commit_on_success
。
呼吸する。または2つ。
これはどういう意味ですか?
ええ、そこではたくさんのことが起こっています。ほとんどの開発者は、標準のデータベースレベルの自動コミットを望んでいることがわかりました。つまり、手動で調整する必要があるまで、トランザクションはバックグラウンドで実行されます。
Django 1.6のトランザクション管理について何が正しいですか?
では、Django1.6へようこそ。今話したすべてを忘れるように最善を尽くし、Django1.6ではデータベースAUTOCOMMIT
を使用することを覚えておいてください。 必要に応じて手動でトランザクションを管理します。基本的に、データベースが最初に行うように設計されたものを基本的に実行する、はるかに単純なモデルがあります。
十分な理論。コードを書きましょう。
ストライプの例
ここに、ユーザーの登録とクレジットカード処理のためのStripeへの呼び出しを処理するこのビュー関数の例があります。
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)
)
このビューは最初にCustomer.create
を呼び出します これは実際にStripeを呼び出してクレジットカードの処理を処理します。次に、新しいユーザーを作成します。 Stripeから応答が返ってきたら、新しく作成した顧客をstripe_id
で更新します。 。顧客が戻ってこない場合(Stripeがダウンしている場合)、UnpaidUsers
にエントリを追加します 新しく作成された顧客の電子メールを含むテーブル。後でクレジットカードの詳細を再試行するように依頼できます。
Stripeがダウンしている場合でも、ユーザーは登録してサイトの使用を開始できるという考え方です。クレジットカード情報については、後日もう一度お尋ねします。
これは少し不自然な例である可能性があることを理解しています。必要に応じてそのような機能を実装する方法ではありませんが、目的はトランザクションを示すことです。
以降。トランザクションについて考え、デフォルトでDjango1.6がAUTOCOMMIT
を提供することを覚えておいてください データベースの動作について、データベース関連のコードをもう少し詳しく見てみましょう。
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:
# ...
問題を見つけることができますか? UnpaidUsers(email=cd['email']).save()
の場合はどうなりますか 回線に障害が発生しましたか?
システムに登録されているユーザーがいて、システムはクレジットカードを確認したと見なしますが、実際にはカードを確認していません。
2つの結果のうちの1つだけが必要です:
- ユーザーは(データベース内に)作成され、
stripe_id
を持っています 。 - ユーザーは(データベース内に)作成され、
stripe_id
を持っていません ANDUnpaidUsers
の関連する行 同じメールアドレスのテーブルが生成されます。
これは、2つの別々のデータベースステートメントで両方をコミットするか、両方をロールバックすることを意味します。謙虚な取引に最適なケースです。
まず、物事が希望どおりに動作することを確認するためのテストをいくつか作成しましょう。
@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)
テストの上部にあるデコレータは、UnpaidUsers
に保存しようとすると「IntegrityError」をスローするモックです。 テーブル。
これは、「UnpaidUsers(email=cd['email']).save()
の場合はどうなるかという質問に答えるためのものです。 行が失敗しますか?」コードの次のビットは、登録機能に必要な適切な情報を使用して、モックセッションを作成するだけです。そして、with mock.patch
Stripeがダウンしているとシステムに信じ込ませます…ついにテストに取り掛かります。
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)
したがって、テストを実行すると失敗するはずです:
======================================================================
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
----------------------------------------------------------------------
良い。言うのはおかしいようですが、それはまさに私たちが望んでいたことです。 覚えておいてください:ここではTDDを実践しています。 エラーメッセージは、ユーザーが実際にデータベースに保存されていることを示しています。これは、ユーザーが支払いをしなかったため、まさに私たちが望んでいないことです。
救助への取引…
トランザクション
Django1.6でトランザクションを作成する方法は実際にはいくつかあります。
いくつか見てみましょう。
推奨される方法
Django 1.6のドキュメントによると:
「Djangoはデータベーストランザクションを制御するための単一のAPIを提供します。 […]Atomicityは、データベーストランザクションの定義プロパティです。アトミックを使用すると、データベースのアトミック性が保証されるコードのブロックを作成できます。コードのブロックが正常に完了すると、変更はデータベースにコミットされます。例外がある場合、変更はロールバックされます。」
Atomicは、デコレータまたはcontext_managerの両方として使用できます。したがって、これをコンテキストマネージャーとして使用すると、register関数のコードは次のようになります。
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')
with transaction.atomic()
の行に注意してください 。そのブロック内のすべてのコードは、トランザクション内で実行されます。したがって、テストを再実行すると、すべて合格するはずです。トランザクションは単一の作業単位であるため、UnpaidUsers
の場合、コンテキストマネージャー内のすべてが一緒にロールバックされることに注意してください。 呼び出しは失敗します。
デコレータの使用
デコレータとしてアトミックを追加することもできます。
@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')
テストを再実行すると、以前と同じエラーで失敗します。
何故ですか?トランザクションが正しくロールバックされなかったのはなぜですか?その理由は、transaction.atomic
ある種の例外を探していますが、そのエラー(つまり、IntegrityError
)を検出しました。 ブロックを除いて私たちの試みでは)、したがってtransaction.atomic
それを見たことがないので、標準のAUTOCOMMIT
機能が引き継がれました。
ただし、もちろん、tryを削除すると、例外がコールチェーンにスローされ、別の場所で爆発する可能性があります。ですから、それもできません。
したがって、最初のソリューションで行ったブロックを除いて、アトミックコンテキストマネージャーをtry内に配置するのがコツです。正しいコードをもう一度見てください:
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')
UnpaidUsers
の場合 IntegrityError
を発生させます transaction.atomic()
コンテキストマネージャーはそれをキャッチしてロールバックを実行します。コードが例外ハンドラーで実行されるまでに(つまり、form.addError
line)ロールバックが実行され、必要に応じてデータベース呼び出しを安全に行うことができます。 transaction.atomic()
の前後のデータベース呼び出しにも注意してください。 context_managerの最終結果に関係なく、コンテキストマネージャーは影響を受けません。
HTTPリクエストごとのトランザクション
Django 1.6(1.5など)では、「リクエストごとのトランザクション」モードで操作することもできます。このモードでは、Djangoはビュー関数をトランザクションで自動的にラップします。関数が例外をスローした場合、Djangoはトランザクションをロールバックします。それ以外の場合、トランザクションをコミットします。
設定するには、ATOMIC_REQUEST
を設定する必要があります この動作を実行する各データベースのデータベース構成でTrueに設定します。したがって、「settings.py」では次のように変更します。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(SITE_ROOT, 'test.db'),
'ATOMIC_REQUEST': True,
}
}
実際には、これは、デコレータをビュー関数に配置した場合とまったく同じように動作します。したがって、ここでは目的を果たしません。
ただし、両方のATOMIC_REQUESTS
で注意する価値があります および@transaction.atomic
デコレータは、ビューからスローされた後でも、これらのエラーをキャッチ/処理することができます。これらのエラーをキャッチするには、カスタムミドルウェアを実装するか、urls.hadler500をオーバーライドするか、500.htmlテンプレートを作成する必要があります。
セーブポイント
トランザクションはアトミックですが、さらにセーブポイントに分割できます。セーブポイントは部分的なトランザクションと考えてください。
したがって、4つのSQLステートメントを完了するのに必要なトランザクションがある場合は、2番目のステートメントの後にセーブポイントを作成できます。そのセーブポイントが作成されると、3番目または4番目のステートメントが失敗した場合でも、部分的なロールバックを実行して、3番目と4番目のステートメントを削除しますが、最初の2つは保持します。
つまり、基本的には、トランザクションをより小さな軽量のトランザクションに分割して、部分的なロールバックまたはコミットを実行できるようにするようなものです。
ただし、メイントランザクションがどこでロールバックされるかを覚えておいてください(おそらく
IntegrityError
が原因です) それが発生し、キャッチされなかった場合、すべてのセーブポイントもロールバックされます。
セーブポイントがどのように機能するかの例を見てみましょう。
@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)
ここでは、関数全体がトランザクション内にあります。新しいユーザーを作成した後、セーブポイントを作成し、セーブポイントへの参照を取得します。次の3つのステートメント-
user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()
-既存のセーブポイントの一部ではないため、次のsavepoint_rollback
の一部になる可能性があります。 、またはsavepoint_commit
。 savepoint_rollback
の場合 、行user = User.create('jj','inception','jj','1234')
残りの更新はコミットされませんが、データベースにコミットされます。
言い換えると、次の2つのテストは、セーブポイントがどのように機能するかを説明しています。
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')
また、セーブポイントをコミットまたはロールバックした後も、同じトランザクションで作業を続行できます。そして、その作業は、前のセーブポイントの結果の影響を受けません。
たとえば、save_points
を更新した場合 そのように機能する:
@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')
savepoint_commit
かどうかに関係なく またはsavepoint_rollback
「リンボ」ユーザーと呼ばれていましたが、引き続き正常に作成されます。他の何かが原因でトランザクション全体がロールバックされない限り。
ネストされたトランザクション
savepoint()
を使用して、セーブポイントを手動で指定することに加えて 、savepoint_commit
、およびsavepoint_rollback
、ネストされたトランザクションを作成すると、自動的にセーブポイントが作成され、エラーが発生した場合はロールバックされます。
例をもう少し拡張すると、次のようになります。
@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
ここでは、セーブポイントを処理した後、transaction.atomic
を使用していることがわかります。 「リンボ」ユーザーの作成をケースに入れるコンテキストマネージャー。そのコンテキストマネージャーが呼び出されると、実際にはセーブポイントが作成され(すでにトランザクションに参加しているため)、そのセーブポイントはコンテキストマネージャーの終了時にコミットまたはロールバックされます。
したがって、次の2つのテストはそれらの動作を説明しています。
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)
したがって、実際には、atomic
のいずれかを使用できます。 またはsavepoint
トランザクション内にセーブポイントを作成します。 atomic
savepoint
の場合と同様に、コミット/ロールバックについて明示的に心配する必要はありません。 それがいつ発生するかを完全に制御できます。
結論
以前のバージョンのDjangoトランザクションを使用した経験がある場合は、トランザクションモデルがどれほど単純であるかを確認できます。 AUTOCOMMIT
もあります デフォルトでオンにすると、DjangoとPythonの両方が提供することに誇りを持っている「正常な」デフォルトの優れた例です。多くのシステムでは、トランザクションを直接処理する必要はありません。AUTOCOMMIT
を使用するだけです。 その仕事をします。ただし、そうする場合は、この投稿で、プロのようにDjangoでトランザクションを管理するために必要な情報が得られることを願っています。