
おそらく誰もが、DjangoがpythonでのWeb開発用の最も人気のあるフレームワークの1つであることを知っています。 また、Webプロジェクトがサードパーティのコードに基づいている場合でも、多くの場合、開発中にORMなどのこのフレームワークの個別の部分を使用します。 この記事では、MySQLデータベースを操作する際にDjango ORMを使用する機能、つまりそれらに関連するトランザクションと落とし穴についてお話したいと思います。 したがって、たとえば、ある時点で、予想されるデータではなく、まったく異なる結果が返されることに気付いた場合、この記事が何を理解するのに役立つでしょう。
次に、InnoDBについて説明します。これはMySQLの一部として実行される唯一のエンジンであり、トランザクションを完全にサポートしているためです(BDBは長い間サポートされていないためカウントされません)。
多くの機能に注目する価値があります。
1. Djangoでは、
MySQLdb拡張
モジュールがMySQLへのインターフェイスとして使用され、次に
インストールされます 。
AUTOCOMMIT=0
つまり、各データ変更操作は、変更をコミットまたはロールバックするためにCOMMIT / ROLLBACKを完了する必要があります。 PHP拡張(PDO、Mysqli)またはRubyを使用してMySQLにアクセスしたことがある場合、接続するとほとんどすべてのデータベースアクセスドライバーで
AUTOCOMMIT値
が変更されないため(MySQLではデフォルトで
AUTOCOMMIT = 1に設定されるため) )
2. MySQLは、たとえば、デフォルトのトランザクション分離レベルが
READ-COMMITTEDであるPosgreSQLやOracleとは異なり、
REPEATABLE-READトランザクション分離レベルを使用します。
これはどういう意味ですか? 特定の例を考えてみましょう:
CREATE TABLE IF NOT EXISTS `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `value` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `test` VALUES (NULL, 'a');
繰り返し読み取り
最初のトランザクション: | 2番目のトランザクション: |
---|
SET AUTOCOMMIT = 0; | SET AUTOCOMMIT = 0; |
SELECT * FROM `test`;
+ ---- + ------- +
| id | 値|
+ ---- + ------- +
| 1 | |
+ ---- + ------- +
| |
| 「テスト」値に挿入(NULL、「b」); SELECT * FROM `test`;
+ ---- + ------- +
| id | 値|
+ ---- + ------- +
| 1 | |
| 2 | b |
+ ---- + ------- + COMMIT; |
「テスト」値に挿入(NULL、「c」); SELECT * FROM `test`;
+ ---- + ------- +
| id | 値|
+ ---- + ------- +
| 1 | |
| 3 | c |
+ ---- + ------- + COMMIT; | |
この例からわかるように、最初のデータ読み取り後の最初のトランザクションでは、他のトランザクションで何が発生しても、
COMMITが発生するまで、後続のすべての読み取りはまったく同じ結果を返します。
読み取りコミット
最初のトランザクション: | 2番目のトランザクション: |
---|
SET SESSION tx_isolation = 'READ-COMMITTED'; SET AUTOCOMMIT = 0; | SET SESSION tx_isolation = 'READ-COMMITTED'; SET AUTOCOMMIT = 0; |
SELECT * FROM `test`;
+ ---- + ------- +
| id | 値|
+ ---- + ------- +
| 1 | |
+ ---- + ------- +
| |
| 「テスト」値に挿入(NULL、「b」); |
SELECT * FROM `test`;
+ ---- + ------- +
| id | 値|
+ ---- + ------- +
| 1 | |
+ ---- + ------- + | |
| SELECT * FROM `test`;
+ ---- + ------- +
| id | 値|
+ ---- + ------- +
| 1 | |
| 2 | b |
+ ---- + ------- + COMMIT; |
「テスト」値に挿入(NULL、「c」); SELECT * FROM `test`;
+ ---- + ------- +
| id | 値|
+ ---- + ------- +
| 1 | |
| 2 | b |
| 3 | c |
+ ---- + ------- + COMMIT; | |
READ-COMMITTEDの場合、SELECTは常にデータの最後にコミットされたバージョンを返します。
Djangoのトピックに戻ると、Django ORMを使用する際の問題は、開発者が注力しているトランザクション分離レベルがREAD-COMMITTEDのみであるように見えることです。 したがって、たとえば、Djangoコード、つまり
QuerySetクラスの
get_or_create()メソッドの実装を見ると:
def get_or_create(self, **kwargs): """ Looks up an object with the given kwargs, creating one if necessary. Returns a tuple of (object, created), where created is a boolean specifying whether an object was created. """ assert kwargs, \ 'get_or_create() must be passed at least one keyword argument' defaults = kwargs.pop('defaults', {}) lookup = kwargs.copy() for f in self.model._meta.fields: if f.attname in lookup: lookup[f.name] = lookup.pop(f.attname) try: self._for_write = True return self.get(**lookup), False except self.model.DoesNotExist: try: params = dict([(k, v) for k, v in kwargs.items() if '__' not in k]) params.update(defaults) obj = self.model(**params) sid = transaction.savepoint(using=self.db) obj.save(force_insert=True, using=self.db) transaction.savepoint_commit(sid, using=self.db) return obj, True except IntegrityError, e: transaction.savepoint_rollback(sid, using=self.db) exc_info = sys.exc_info() try: return self.get(**lookup), False except self.model.DoesNotExist:
次に、オブジェクトを取得する2回目の試行:
return self.get(**lookup), False
常に失敗します。
説明しようとします-たとえば、2つのプロセスが特定のモデルの
get_or_create()メソッドを同時に呼び出します。 最初のプロセスはデータの読み取りを試みます-データが存在しない場合、
DoesNotExist例外が
スローされます。 2番目のプロセスも同様にデータを読み取ろうとし、同様に
DoesNotExist例外をスローします。 さらに、接続はAUTOCOMMIT = 0およびトランザクション分離レベルREPEATABLE-READを使用するため、両方のプロセスが読み取りデータを凍結します。 最初のプロセスがレコードを正常に作成し、作成されたレコードのオブジェクトを返すと仮定します。 ただし、同時に、2番目のプロセスは何も作成できません。これは、一意性制約に違反するためです。 面白いことに、データが再度読み取られると「凍結」結果が返されるため、最初のプロセスで作成されたオブジェクトが表示されません。
もちろん、実験条件ではこのエラーを再現することは非常に問題ですが、多数の競合するリクエストがあると、このコードは不安定になり
、DoesNotExist例外を定期的に
スローします。
これに対処する方法は?
1. get_or_create()メソッドを使用する場合、データを再度読み取る前に強制COMMITを実行する独自のメソッドを記述します。
@transaction.commit_manually() def custom_get_or_create(...): try: obj = SomeModel.objects.create(...) except IntegrityError: transaction.commit() obj = SomeModel.objects.get(...) return obj
2. MySQL設定(/etc/mysql/my.cnf)で、READ-COMMITTEDトランザクション分離レベルを使用します。
transaction-isolation = READ-COMMITTED
3. Django> = 1.2バージョンを使用する場合、データベース接続オプションのsettings.pyで次のコードを使用します。
DATABASE_OPTIONS = { "init_command": "SET storage_engine=INNODB, SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED", }
このバグはDjango
バグトラッカーで長い間
公開されていましたが、今までチケットはクローズされておらず、問題は依然として関連しています。
または、ここに別の例があります-Django ORMは、常にメモリにハングし、MySQLテーブルから新しいデータを定期的に読み取るデーモンの一部として、Webサーバーとは別に使用されます。 この実験は、Djangoの組み込みシェルを使用して実行できます。
python manage.py shell >>> from test_module.models import * >>> len(SomeModel.objects.all()) 10
次に、2番目のターミナルを使用して、いくつかのエントリを追加します。
>>> SomeModel(name='test1').save() >>> SomeModel(name='test2').save() >>> len(SomeModel.objects.all()) 12
また、2番目の端末では変更が明らかですが、最初の端末でこれらの新しく追加されたエントリは、まだ利用できません。 開始されたトランザクションは完了せず、データの最初の読み取り後、COMMITが強制的に呼び出されるまで、後続のすべての読み取りは同じ結果を返します。
それをどうしますか? mysql設定(my.cnf)またはDjangoのsettings.pyのデータベース接続設定でトランザクション分離レベルを変更します。 まあ、または各読み取り後にデータを強制する:
>>> from django.db import connection, transaction >>> len(Param_Type.objects.all()) 10 >>> transaction.commit_unless_managed() >>> len(Param_Type.objects.all()) 12
なぜこれがすべて起こっているのですか? これはおそらく、Djangoが元々PostgreSQLをデータベースとして使用するように設計されていたためであり、上記のようにREAD-COMMITTEDをそのまま使用します。 一般に、これはMySQL InnoDBに関連したDjango ORMの標準的な動作ではありませんが、バグを見つけるのは非常に困難です。 したがって、説明されている問題が議論されているほとんどの場所(さまざまなブログやstackoverflow)で、READ-COMMITTEDをREPEATABLE-READより
「生産的」であると主張して、READ-COMMITTEDをデフォルトのトランザクション分離レベルとして使用することを強くお勧めします。