私はDjango HTTP要求から呼び出されないプロセスからのデータベースモデルを使用しています。このプロセスは数秒ごとに新しいデータをポーリングし、その上で何らかの処理を行うことになっています。数秒間スリープし、データベースから未処理のデータをすべて取得するループ。
私が見ているのは、最初のフェッチの後、プロセスが新しいデータをまったく見ることがないということです。いくつかのテストを実行しましたが、毎回新しいQuerySetsを作成しているにもかかわらず、Djangoは結果をキャッシュしています。これを確認するために、Pythonシェル:
>>> MyModel.objects.count()
885
# (Here I added some more data from another process.)
>>> MyModel.objects.count()
885
>>> MyModel.objects.update()
0
>>> MyModel.objects.count()
1025
ご覧のとおり、新しいデータを追加しても結果カウントは変わりません。ただし、マネージャーのupdate()メソッドを呼び出すと、問題が解決するようです。
私はそのupdate()メソッドに関するドキュメントを見つけることができず、それが他にどんな悪いことをするのか分かりません。
私の質問は、なぜこのキャッシュ動作を見ているのですか? Django docs sayと矛盾していますか?そして、どうすればそれを防ぐことができますか?
この問題を抱えており、2つの決定的な解決策を見つけたので、別の答えを投稿する価値があると思いました。
これは、MySQLのデフォルトのトランザクションモードの問題です。 Djangoは、開始時にトランザクションを開きます。つまり、デフォルトでは、データベースに加えられた変更は表示されません。
このようなデモンストレーション
ターミナル1でDjango Shellを実行します
_>>> MyModel.objects.get(id=1).my_field
u'old'
_
ターミナル2の別の
_>>> MyModel.objects.get(id=1).my_field
u'old'
>>> a = MyModel.objects.get(id=1)
>>> a.my_field = "NEW"
>>> a.save()
>>> MyModel.objects.get(id=1).my_field
u'NEW'
>>>
_
ターミナル1に戻って問題を示します-データベースから古い値を読み取ります。
_>>> MyModel.objects.get(id=1).my_field
u'old'
_
ターミナル1でソリューションをデモンストレーションします
_>>> from Django.db import transaction
>>>
>>> @transaction.commit_manually
... def flush_transaction():
... transaction.commit()
...
>>> MyModel.objects.get(id=1).my_field
u'old'
>>> flush_transaction()
>>> MyModel.objects.get(id=1).my_field
u'NEW'
>>>
_
新しいデータが読み取られました
これは、docstringを使用して簡単に貼り付けられるブロック内のコードです
_from Django.db import transaction
@transaction.commit_manually
def flush_transaction():
"""
Flush the current transaction so we don't read stale data
Use in long running processes to make sure fresh data is read from
the database. This is a problem with MySQL and the default
transaction mode. You can fix it by setting
"transaction-isolation = READ-COMMITTED" in my.cnf or by calling
this function at the appropriate moment
"""
transaction.commit()
_
別の解決策は、MySQLのmy.cnfを変更してデフォルトのトランザクションモードを変更することです。
_transaction-isolation = READ-COMMITTED
_
これはMysqlの比較的新しい機能であり、 バイナリロギング/スレーブ化の結果 になります。必要であれば、これをDjango接続プリアンブルに入れることもできます。
3年後に更新
これでDjango 1.6は MySQLで自動コミットをオンにした これは問題ではなくなりました。上記の例はflush_transaction()
コードがなくても正常に動作しますMySQLは_REPEATABLE-READ
_(デフォルト)または_READ-COMMITTED
_トランザクション分離モードです。
非自動コミットモードで実行された以前のバージョンのDjango)で起こっていたことは、最初のselect
ステートメントがトランザクションを開いたことでした。MySQLのデフォルトモードは_REPEATABLE-READ
_データベースへの更新は後続のselect
ステートメントによって読み取られないため、トランザクションを停止して新しいトランザクションを開始する上記のflush_transaction()
コードが必要です。
ただし、_READ-COMMITTED
_トランザクション分離を使用する理由はまだあります。ターミナル1をトランザクションに配置し、ターミナル2からの書き込みを確認する場合は、_READ-COMMITTED
_が必要です。
flush_transaction()
コードは、Django 1.6で非推奨の警告を生成するようになったため、削除することをお勧めします。
「キャッシュ」を更新するためにDjangoを強制することでかなり苦労しました-これは実際にはキャッシュではなく、トランザクションによるアーティファクトです。これは当てはまらないかもしれませんあなたの例ですが、Djangoビューでは、デフォルトで、トランザクションへの暗黙的な呼び出しがあります。mysqlは、開始した他のプロセスから発生する変更から隔離します。
_@transaction.commit_manually
_デコレータを使用し、最新の情報が必要になるたびにtransaction.commit()
を呼び出しました。
私が言うように、これは間違いなくビューに適用され、ビュー内で実行されていないコードDjangoに適用されるかどうかはわかりません。
詳細情報はこちら:
count()
が最初にキャッシュに行くようです。これはDjango QuerySet.countのソース:
def count(self):
"""
Performs a SELECT COUNT() and returns the number of records as an
integer.
If the QuerySet is already fully cached this simply returns the length
of the cached results set to avoid multiple SELECT COUNT(*) calls.
"""
if self._result_cache is not None and not self._iter:
return len(self._result_cache)
return self.query.get_count(using=self.db)
update
は、必要なもの以外にかなりの余分な作業を行っているようです。
しかし、カウントのために独自のSQLを書く以外に、これを行うためのより良い方法は考えられません。
パフォーマンスがそれほど重要でない場合は、update
の前にcount
を呼び出して、あなたがしていることをするだけです。
QuerySet.update:
def update(self, **kwargs):
"""
Updates all elements in the current QuerySet, setting all the given
fields to the appropriate values.
"""
assert self.query.can_filter(), \
"Cannot update a query once a slice has been taken."
self._for_write = True
query = self.query.clone(sql.UpdateQuery)
query.add_update_values(kwargs)
if not transaction.is_managed(using=self.db):
transaction.enter_transaction_management(using=self.db)
forced_managed = True
else:
forced_managed = False
try:
rows = query.get_compiler(self.db).execute_sql(None)
if forced_managed:
transaction.commit(using=self.db)
else:
transaction.commit_unless_managed(using=self.db)
finally:
if forced_managed:
transaction.leave_transaction_management(using=self.db)
self._result_cache = None
return rows
update.alters_data = True
私はそれをお勧めするかどうかわかりません...しかし、あなたは自分でキャッシュを殺すことができます:
>>> qs = MyModel.objects.all()
>>> qs.count()
1
>>> MyModel().save()
>>> qs.count() # cached!
1
>>> qs._result_cache = None
>>> qs.count()
2
そして、QuerySetの内部をいじることに依存しないより良いテクニックを次に示します。キャッシュはQuerySet内で行われますが、データを更新するには基礎となるQuery再実行されます。 QuerySetは、実際にはQueryオブジェクトをラップする高レベルAPIに加えて、クエリ結果用のコンテナ(キャッシングあり)です。したがって、クエリセットを指定して、リフレッシュを強制する汎用的な方法を次に示します。
>>> MyModel().save()
>>> qs = MyModel.objects.all()
>>> qs.count()
1
>>> MyModel().save()
>>> qs.count() # cached!
1
>>> from Django.db.models import QuerySet
>>> qs = QuerySet(model=MyModel, query=qs.query)
>>> qs.count() # refreshed!
2
>>> party_time()
とても簡単!もちろん、これをヘルパー関数として実装し、必要に応じて使用できます。
.all()
をクエリセットに追加すると、DBからの再読み込みが強制されます。 MyModel.objects.all().count()
の代わりにMyModel.objects.count()
を試してください。