djangoでアトミックトランザクションを使ってみる

djangoでアトミックトランザクションを使ってみる

途中でエラーがあった場合はデータベースの更新をやめるコミットロールバックの機能みたいです。早速使ってみたいと思います。

参考:
  ・アトミックトランザクション
  ・【Django】トランザクションの設定方法

モデル

簡単なモデルになりますが注目するところはPhotoクラスのtitleがunique=trueになっているので、同じ名前の写真名は登録できない事です。


from django.db import models


class Photographer(models.Model):
    name = models.CharField(max_length=128)


class Photo(models.Model):
    photographer = models.ForeignKey(Photographer,
                                     on_delete=models.CASCADE,
                                     related_name='photograpers')
    title = models.CharField(max_length=128, unique=True)

ビュー

まずは下の通りの例です。コミットとロールバックを何もしていない例です。photo_titleにmountainが2件入ってくるので三つ目のphoto_titleをインサートしようとした時にエラーが発生します。実行してみましょう。


from django.http import HttpResponse, HttpResponseBadRequest
from myapp.models import Photographer, Photo


def regist_photo_and_photographer(request):
    regist_datas = ['kyosuke', ['bird', 'mountain', 'mountain']]
    photographer_name = regist_datas[0]
    photo_titles = regist_datas[1]
    photographer = Photographer.objects.create(name=photographer_name)
    for photo_title in photo_titles:
        Photo.objects.create(
            photographer=photographer,
            title=photo_title,
        )
    return HttpResponse("OK")

データベース

データベースが更新されてしまっているのがわかります。


MariaDB [djangodatabase]> select * from myapp_photographer;
+----+---------+
| id | name    |
+----+---------+
|  1 | kyosuke |
+----+---------+
MariaDB [djangodatabase]> select * from myapp_photo;
+----+----------+-----------------+
| id | title    | photographer_id |
+----+----------+-----------------+
|  1 | bird     |               1 |
|  2 | mountain |               1 |
+----+----------+-----------------+

ビュー修正後

下記の通り2か所追記するだけでこの問題を防げます。実行してみましょう。


from django.http import HttpResponse, HttpResponseBadRequest
from myapp.models import Photographer, Photo
from django.db import transaction # 追記


@transaction.atomic # 追記
def regist_photo_and_photographer(request):
    regist_datas = ['kyosuke', ['bird', 'mountain', 'mountain']]
    photographer_name = regist_datas[0]
    photo_titles = regist_datas[1]
    photographer = Photographer.objects.create(name=photographer_name)
    for photo_title in photo_titles:
        Photo.objects.create(
            photographer=photographer,
            title=photo_title,
        )
    return HttpResponse("OK")

データベース

下記の通りデータベースに値が挿入されていないのが確認できました。


MariaDB [djangodatabase]> select * from myapp_photographer;
Empty set (0.00 sec)

MariaDB [djangodatabase]> select * from myapp_photo;
Empty set (0.00 sec)

その他の方法

その他の方法が2つあるみたいだったので載せておきたいと思います。

【コンテキストマネージャを使用する方法】
アノテーションを使わない場合はあ下記の通りに記述する事で同じくデータベースの更新を防げます。


def regist_photo_and_photographer(request):
    with transaction.atomic():
        regist_datas = ['kyosuke', ['bird', 'mountain', 'mountain']]
        photographer_name = regist_datas[0]
        photo_titles = regist_datas[1]
        photographer = Photographer.objects.create(name=photographer_name)
        for photo_title in photo_titles:
            Photo.objects.create(
                photographer=photographer,
                title=photo_title,
            )
        return HttpResponse("OK")

【ATOMIC_REQUESTSを使用する方法】
下記の通りsettings.pyを編集する方法です。しかしこれを記述してしまうとview全体に反映してしまうそうです。詳しくは参考サイトから。


# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
        'ATOMIC_REQUESTS': True,  # 追加
    }
}