web-dev-qa-db-ja.com

キャッシュを無視してデータをリロードするには、Djangoを強制するにはどうすればよいですか?

私は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と矛盾していますか?そして、どうすればそれを防ぐことができますか?

73
scippy

この問題を抱えており、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で非推奨の警告を生成するようになったため、削除することをお勧めします。

93
Nick Craig-Wood

「キャッシュ」を更新するためにDjangoを強制することでかなり苦労しました-これは実際にはキャッシュではなく、トランザクションによるアーティファクトです。これは当てはまらないかもしれませんあなたの例ですが、Djangoビューでは、デフォルトで、トランザクションへの暗黙的な呼び出しがあります。mysqlは、開始した他のプロセスから発生する変更から隔離します。

_@transaction.commit_manually_デコレータを使用し、最新の情報が必要になるたびにtransaction.commit()を呼び出しました。

私が言うように、これは間違いなくビューに適用され、ビュー内で実行されていないコードDjangoに適用されるかどうかはわかりません。

詳細情報はこちら:

http://devblog.resolversystems.com/?p=439

8
hwjp

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
6
adamJLev

私はそれをお勧めするかどうかわかりません...しかし、あなたは自分でキャッシュを殺すことができます:

>>> 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()

とても簡単!もちろん、これをヘルパー関数として実装し、必要に応じて使用できます。

6
Chris Clark

.all()をクエリセットに追加すると、DBからの再読み込みが強制されます。 MyModel.objects.all().count()の代わりにMyModel.objects.count()を試してください。

3
Sarah Messer