Django + MySQL: 当使用 transaction.atomic() 和 select_for_update() 锁定记录时的笔记

結果

在以下的代码中,即使在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

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

换句话说,在使用”database_name”参数的transaction.atomic()函数中,并没有执行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,但我不知道为什么它没有被执行。
似乎还有其他正确的解决办法……