結論
以下のコードでmysqlでもtransaction.atomic()でレコードのロックができた
from django.db import transaction, connections
with transaction.atomic(using="database_name"):
connections["database_name"].cursor().execute("BEGIN") # <-- ここ
target_user = User.objects.select_for_update().get(id=1)
target_user.name = "name"
target_user.save()
やりたかったこと
djangoでデータベースのレコードをロックして悲観的排他制御を実装したかった
環境
Django 3.2.12
Python 3.6.15
Mysql 5.7.41
内容
まずデータベースでレコードをロックするために sql では、
$ BEGIN <-- トランザクション開始
$ SELECT **** FOR UPDATE <-- テーブル、レコードのロック
$ UPDATE **** <-- 更新
$ COMMIT <-- トランザクション終了
のように BEGIN
で始まりCOMMIT
で終わるトランザクション内で、FOR UPDATE
を実行する必要がある
そして、Djangoでレコードをロックするには以下のようにすれば良いといろんな記事に書いてあった。
from django.db import transaction
with transaction.atomic(using="database_name"): # <--トランザクション開始
target_user = User.objects.select_for_update().get(id=1) # <--ロック
target_user.name = "name"
target_user.save() # <--更新
# <-- トランザクション終了
しかし、このコードを実行してもレコードはロックされず、データベースのsqlのログを確認すると、以下のようにBEGIN
が実行されていなかった。
$ SELECT **** FOR UPDATE
$ UPDATE ****
$ COMMIT
つまりwith transaction.atomic(using="database_name"):
でBEGIN
が実行されていなかった。
ライブラリ(django.db.transaction)のコードを読むと、transaction.atomic()ではsettings.pyで指定したデータベースエンジンごとにトランザクションを開始しようとしていることがわかった。
django.db.backends.base.base の BaseDatabaseWrapper の set_auto_commit() メソッドでそれぞれのデータベースエンジンごとのトランザクション開始方法に従って処理を実行するようだった。
django.db.backends.*** (***はmysql, sqlite3など)をみてみると BaseDatabaseWrapper を継承しており、mysqlのクラスにはBEGIN
を実行するコードがなかった(sqlite3にはあった)
なので、transaction.atomic()で使われているconnection.cursor()を使ってBEGIN
を実行すれば問題なさそう。
connections["database_name"].cursor().execute("BEGIN")
を追加し、解決。
思ったこと
そもそも、mysqlではBEGIN
が利用できるはずなのに、なぜ実行されていなかったのかわからない。
何か他に正しい解決方法がありそう。。。。