Подтвердить что ты не робот

Как заставить Django игнорировать любые кеши и перезагружать данные?

Я использую модели базы данных Django из процесса, который не вызывается из HTTP-запроса. Предполагается, что процесс будет обрабатывать новые данные каждые несколько секунд и выполнять некоторую обработку на нем. У меня есть цикл, который спит в течение нескольких секунд, а затем получает все необработанные данные из базы данных.

Что я вижу, так это то, что после первой выборки процесс никогда не видит никаких новых данных. Я провел несколько тестов, и похоже, что Django - это кеширование результатов, хотя я каждый раз создаю новые QuerySets. Чтобы проверить это, я сделал это из оболочки 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? И как я могу предотвратить это?

4b9b3361

Ответ 1

Получив эту проблему и нашел для нее два окончательных решения, я подумал, что стоит опубликовать другой ответ.

Это проблема с режимом транзакций MySQL по умолчанию. Django открывает транзакцию в начале, что означает, что по умолчанию вы не увидите изменений, внесенных в базу данных.

Продемонстрируйте это как

Запустите оболочку django в терминале 1

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

Альтернативным решением является изменение my.cnf для MySQL для изменения режима транзакций по умолчанию

transaction-isolation = READ-COMMITTED

Обратите внимание, что это относительно новая функция для Mysql и некоторые последствия для двоичного ведения журнала/ведомости. Вы также можете поместить это в преамбулу соединения django, если хотите.

Обновление через 3 года

Теперь, когда Django 1.6 включил autocommit в 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, поэтому я рекомендую удалить его.

Ответ 2

Мы изо всех сил пытались заставить django обновить "кеш", который, как оказалось, на самом деле не был кешем, кроме артефакта из-за транзакций. Это может не относиться к вашему примеру, но, конечно же, в представлениях django по умолчанию существует неявный вызов транзакции, который затем отключается от любых изменений, которые происходят из других процессов.

мы использовали декоратор @transaction.commit_manually и звонили на transaction.commit() перед каждым случаем, когда вам нужна обновленная информация.

Как я уже сказал, это определенно относится к представлениям, не уверен, будет ли оно применяться к коду django, который не запускается внутри представления.

подробная информация здесь:

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

Ответ 3

Кажется, что 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

Ответ 4

Я не уверен, что рекомендую... но вы можете просто убить кеш себе:

>>> qs = MyModel.objects.all()
>>> qs.count()
1
>>> MyModel().save()
>>> qs.count()  # cached!
1
>>> qs._result_cache = None
>>> qs.count()
2

И вот лучшая техника, которая не полагается на возиться с внутренностями QuerySet: Помните, что кеширование происходит в QuerySet, но обновление данных просто требует повторного выполнения базового Query. QuerySet - это просто API-интерфейс высокого уровня, который обертывает объект Query, а также контейнер (с кешированием!) Для результатов запроса. Таким образом, с учетом набора запросов, это универсальный способ принудительного обновления:

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

Довольно легко! Вы можете, конечно, реализовать это как вспомогательную функцию и использовать по мере необходимости.

Ответ 5

Если вы добавите .all() в набор запросов, это заставит перечитать данные из БД. Пытаться MyModel.objects.all().count() вместо MyModel.objects.count().

Ответ 6

Вы также можете использовать MyModel.objects._clone().count(). Все методы в вызове QuerySet _clone() до выполнения какой-либо работы - это гарантирует, что любые внутренние тайники будут недействительными.

Основная причина в том, что MyModel.objects - это один и тот же экземпляр каждый раз. Клонируя его, вы создаете новый экземпляр без кешированного значения. Разумеется, вы всегда можете получить доступ к кешу и аннулировать его, если хотите использовать один и тот же экземпляр.