Як змусити Django ігнорувати кеші та перезавантажувати дані?


77

Я використовую моделі баз даних 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 ? І як мені запобігти цьому?

Відповіді:


95

Маючи цю проблему та знайшовши два остаточні рішення для неї, я вважав, що варто опублікувати ще одну відповідь.

Це проблема із стандартним режимом транзакцій 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 увімкнув автозв'язок у 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, тому я рекомендую вам його видалити.


7
DATABASE_OPTIONS = {"init_command": "ВСТАНОВИТИ storage_engine = INNODB, РЕВІЗ ІЗОЛЯЦІЇ ТРАНЗАКЦІЇ СЕСІЇ ПРОЧИТАНО",}
Гай Гаврілі

Починаючи з django 1.2, синтаксис налаштувань змінився. Додайте "OPTIONS" до свого параметра БАЗИ БД (мабуть, "за замовчуванням") "OPTIONS": {"init_command": "ВСТАНОВИТИ storage_engine = INNODB, РІВЕНЬ ІЗОЛЯЦІЇ ОПЕРАЦІЇ СЕСІЇ, ЧИТАЙТЕ
ВИКОНАНО

2
Ну, я на Django 1.8, і проблема все ще зберігається, чи використовую я MySQL чи SQLite
tgdn

Я спробував це на Django 1.10, і це не працює з sqlite або pg.
Джаред,

8

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

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

Як я вже говорив, це, безумовно, стосується подань, не впевнений, чи стосуватиметься це коду django, який не запускається всередині подання.

детальну інформацію тут:

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


Більшість з цих відповідей є відносно точними, але перше речення вводить в оману. Ця проблема не має нічого спільного з будь-яким "кешем" у Django або "змушуванням django оновлюватись", цілком йдеться про ізоляцію транзакцій на рівні бази даних.
Carl Meyer

7

Я не впевнений, що рекомендував би це ... але ви можете просто вбити кеш самостійно:

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

І ось краща техніка, яка не покладається на возиння з внутрішньою частиною QuerySet: пам’ятайте, що кешування відбувається в QuerySet , але оновлення даних просто вимагає повторного виконання базового запиту . QuerySet - це насправді лише API високого рівня, що обертає об’єкт Query, а також контейнер (з кешуванням!) Для результатів 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()

Досить просто! Звичайно, ви можете реалізувати це як допоміжну функцію та використовувати за потреби.


6

Здається, 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


-1

Ви також можете використовувати MyModel.objects._clone().count(). всі методи у QuerySetвиклику _clone()перед виконанням будь-якої роботи - це гарантує, що будь-які внутрішні кеші будуть анульовані.

Першопричиною є те MyModel.objects, що кожного разу однаковий екземпляр. Клонуючи його, ви створюєте новий екземпляр без кешованого значення. Звичайно, ви завжди можете зв’язати кеш-пам’ять і зробити її недійсною, якщо хочете використовувати той самий екземпляр.


Це виглядає дивним і простим рішенням, але принаймні в моїй версії Django це не працює. Виклик MyModel.objects._clone () призводить до "AttributeError: об'єкт" Менеджер "не має атрибута" _clone '". Я можу зробити MyModel.objects.all () ._ clone (), але це працює так само, як і раніше - не змінюється, доки я не покличу update (). Я використовую Django 1.2.1.
скупий

Моя погана - так і повинно бути MyModel.objects.all()._clone(). Думаючи про це, ви могли б уникнути, роблячи MyModel.objects.all().count()без _clone(). Це створює нову версію базового об’єкта, і ви повинні отримати нову версію без кешованого значення. Тобто хіба що Джанго робить там щось нечесне і несе державу з клоном.
Тревіс Свісгуд,

3
Ця відповідь неправильна. Виклик будь-якого методу (на зразок count()) у менеджера неявно клонує новий набір запитів, не існує неявної поведінки кешування через ідентифікацію менеджера і не потрібно вставляти сторонній виклик до _clone()або all(). Весь цей хід думок - це червоний оселедець, справжньою проблемою OP є ізоляція транзакцій на рівні бази даних, вона взагалі не має нічого спільного з наборами запитів або кешуванням на рівні Django.
Carl Meyer

Вже давно я не грався з цією конкретною проблемою, але, думаю, на момент створення цього файлу count()було якесь кешування, інакше Карл правильний, і ця відповідь далеко.
Тревіс Свісгуд,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.