こんにちは、グローバルウェイの林です。
今回はデータベースアクセスの際に必須となる機能であるトランザクションについて、Djangoではどのように実現しているのかを説明していきたいと思います。
この記事は以下の方を対象としています。
★4 Python開発経験が3年以上。
★3 Python開発経験が1年以上。
★2 Python 初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。
トランザクションとは
トランザクションとはデータベースにおいて整合性を担保するための仕組みです。
たとえば銀行などでお金の出金を行うプログラムにてエラーが発生した場合、お金が払い出されていないにもかかわらず、残高が減っていたとしたら問題になると思いませんか?原則としてデータの確定は全て正常に行われた場合に行い、途中でエラーなどが発生した場合は実行前の状態にもどすというのが原則となります。
このようなデータ復元可能区間を定義することをトランザクション処理と呼び、復元区間の開始をトランザクションの開始、変更の確定を行う処理をコミット、復元ポイントの状態に変更を復元することをロールバックと呼びます。
あまり意識せずにプログラミングをしている方もいると思いますが、重要な機能ですので是非とも理解してください。
Djangoにおけるトランザクションの基本
デフォルト動作について
Djangoで特に設定を行わない状態でDBへの更新をかけた場合、各処理の実行と同時にデータベースへの保存が確定します。たとえば以下のようなプログラムが存在した場合、①の行が実行された時点で①の内容でデータベースへの保存が実行されます。
def withdrawMoney(account, ammount) account. balance -= ammount # 口座の残高を減算 account.save() # ①データベースを変更した値で更新 lib.withdraaw(ammount) # ②実際の集金処理を呼び出し |
このようなプログラムの場合、②でエラーが発生した場合に、残高は更新されるがユーザーにはお金の払い出しが行われないというデータの不整合が生じてしまいます。
自動トランザクション
Djangoにてトランザクションを設定するには、setting.pyのデータベース設定にて、「ATOMIC_REQUESTS」を設定するのが一般的です。
この設定を行うと、リクエストに応じた処理(urls.py)で指定されたメソッドが開始されると同時にトランザクションが開始されます。
この設定の場合、実行された側の処理で記載は必要になりません。
このためトランザクションの設定漏れといったコードバグが発生しにくくなります。
DATABASES = { “default”: { “ENGINE”: “django.db.backends.postgresql”, “NAME”: “mydatabase”, “USER”: “mydatabaseuser”, “PASSWORD”: “mypassword”, “HOST”: “127.0.0.1”, “PORT”: “5432”, ‘ATOMIC_REQUESTS’: True, # 自動トランザクション有効 } } |
urlpatterns = [ # 自動トランザクションが有効になっている場合、リクエストに対応する # withdrawMoney関数がトランザクション区間となります path(“withdraw/”, views. withdrawMoney), ] |
def withdrawMoney(account, ammount) # この関数が呼ばれたタイミングで # トランザクションの開始(復元ポイント区間の設定)が行われ、 # この関数を抜けたタイミングでコミットorロールバックが行われる account. balance -= ammount # 口座の残高を減算 account.save() # ①データベースを変更した値で更新 if not lib.withdraaw(ammount): # ②実際の集金処理を呼び出し raise Exception # ③異常時ロールバック |
コミット(変更の確定)
では実際にトランザクションが実行された場合の挙動を、前述したプログラムで見ていきましょう。
デフォルトの状態では①の処理が成功した時点でデータベースへの確定が行われてしまいますが、トランザクションが開始されている場合は暫定保存された状態となります。
この暫定保存の状態では、実行中のプログラムからみたらデータベースに保存されているように見えますが、他のユーザーから見たらデータベースの更新は掛かっておりません。
ではこの暫定保存を、正式保存するにはどうすれば良いかということですが、実は何もしなくて大丈夫です。トランザクション区間であるリクエストにて例外を発生せずに処理を終了させるとDjangoが自動的にコミット処理を実行します。
ロールバック(変更の復元)
ではエラーとなった場合に変更を復元には、トランザクション区間を例外(Exception)で抜けることでロールバックが行われます。
前述するプログラムでは②の実行成否を判定し、エラーであれば③で例外を発生させることでロールバックを実現しています。
ここで注意しなければいけないのは、ロールバック条件はトランザクション区間を例外で抜けることであり、例外が発生することではありません。
例えば以下のようなプログラムであった場合、②で例外が発生したとしても③の処理で例外が消化されているため、トランザクション区間である本関数を正常終了してしまいます。
つまりこの関数はロールバックが行われず、変更が確定してしまい、バグとなってしまいます。
def withdrawMoney(account, ammount) account. balance -= ammount # 口座の残高を減算 account.save() # ①データベースを変更した値で更新 try: lib.withdraaw(ammount) # ②実際の集金処理を呼び出し、異常時に例外発生 except Exception as e: pass # ③例外を消化するためロールバックされない |
まとめ
DjangoのようなWebアプリとデータベースは切っても切れない関係であることから、トランザクションについてはかなり重要な機能となります。ですが今回説明した通り、設定さえしてしまえばあまり実装側での考慮が不要となります。
これは良いようにも思われますが、考慮を怠るとロールバックの項で挙げた悪い例のように、エラー処理でロールバックをし損ねるというバグを生み出すリスクを含んでいます。
特にプログラム初心者などは正常動作を実現することに注力し、異常時の考慮を怠りがちです。そのため、改めてこの機会にてDjangoにおけるトランザクションを覚えていただければと思います。
またデータベースにおけるトランザクションについて、今回AdditionalUserGroupは概要だけの説明となっています。こちらも重要な機能ですので是非とも各自で調べてみてください。
次回、機会があればトランザクションの応用編を説明したいと思います。