Почему Django возвращает устаревшие данные кеша?

У меня есть две модели Django, как показано ниже, MyModel1 и MyModel2:

class MyModel1(CachingMixin, MPTTModel):
    name = models.CharField(null=False, blank=False, max_length=255)
    objects = CachingManager()

    def __str__(self):
        return "; ".join(["ID: %s" % self.pk, "name: %s" % self.name, ] )

class MyModel2(CachingMixin, models.Model):
    name = models.CharField(null=False, blank=False, max_length=255)
    model1 = models.ManyToManyField(MyModel1, related_name="MyModel2_MyModel1")
    objects = CachingManager()

    def __str__(self):
        return "; ".join(["ID: %s" % self.pk, "name: %s" % self.name, ] )

MyModel2 имеет поле ManyToMany для MyModel1 под названием model1

Теперь посмотрите, что происходит, когда я добавляю новую запись в это поле ManyToMany. По словам Джанго, это не имеет никакого эффекта:

>>> m1 = MyModel1.objects.all()[0]
>>> m2 = MyModel2.objects.all()[0]
>>> m2.model1.all()
[]
>>> m2.model1.add(m1)
>>> m2.model1.all()
[]

Почему? Это определенно похоже на проблему с кэшированием, потому что я вижу новую запись в таблице базы данных myapp_mymodel2_mymodel1 для этой связи между m2 и m1. Как мне это исправить??


person Saqib Ali    schedule 16.06.2016    source источник
comment
Именно по этой проблеме есть открытый тикет.   -  person solarissmoke    schedule 16.06.2016
comment
Вы действительно хотите кэшировать такие вещи? если у вас нет огромной пользовательской базы, это действительно преждевременная оптимизация.   -  person e4c5    schedule 16.06.2016
comment
Аннулирование кеша — одна из действительно серьезных проблем. в КС. Если вы не используете большие данные, я рекомендую либо не кэшировать это, либо выполнять полную очистку кеша при каждом обновлении (возможно, с использованием сигналов). Ни одно из этих решений не является отличным решением, но они будут работать и позволят вам продолжать свою жизнь.   -  person Peter Rowell    schedule 16.06.2016
comment
Я думаю, вам следует m2.save() после m2.model1.add(m1). Раньше у меня была эта проблема. Но с тех пор, как я это сделал, у меня его нет.   -  person Rahul    schedule 18.06.2016


Ответы (4)


Действительно ли нужна django-cache-machine?

MyModel1.objects.all()[0]

Примерно переводится как

SELECT * FROM app_mymodel LIMIT 1

Такие запросы всегда выполняются быстро. Не будет существенной разницы в скорости, извлекаете ли вы его из кеша или из базы данных.

Когда вы используете диспетчер кеша, вы на самом деле добавляете здесь немного накладных расходов, которые могут немного замедлить работу. В большинстве случаев эти усилия будут потрачены впустую, потому что попадание в кэш может и не произойти, как описано в следующем разделе.

Как работает django-cache-machine

Всякий раз, когда вы запускаете запрос, CachingQuerySet будет пытаться найти этот запрос в кеше. Запросы обозначаются {prefix}:{sql}. Если он есть, мы возвращаем кешированный набор результатов, и все довольны. Если запрос не находится в кеше, выполняется обычный код для запуска запроса к базе данных. По мере повторения объектов в результирующем наборе они добавляются в список, который будет кэшироваться после завершения итерации.

источник: https://cache-machine.readthedocs.io/en/latest/

Соответственно, если два запроса, выполненные в вашем вопросе, идентичны, диспетчер кеша извлечет второй набор результатов из memcache, если кеш не стал недействительным.

В той же ссылке объясняется, как аннулируются ключи кеша.

Чтобы упростить аннулирование кеша, мы используем «flush lists», чтобы пометить кешированные запросы, которым принадлежит объект. Таким образом, все запросы, в которых был найден объект, будут признаны недействительными при изменении этого объекта. Скрытые списки сопоставляют ключ объекта со списком ключей запроса.

Когда объект сохраняется или удаляется, все ключи запроса в его списке сброса будут удалены. Кроме того, будут очищены списки сброса его отношений внешнего ключа. Чтобы избежать устаревших отношений внешнего ключа, любые кэшированные объекты будут сброшены, когда объект, на который указывает их внешний ключ, становится недействительным.

Понятно, что сохранение или удаление объекта приведет к тому, что многие объекты в кеше будут признаны недействительными. Таким образом, вы замедляете эти операции с помощью диспетчера кеша. Также стоит отметить, что в документации по аннулированию вообще не упоминаются многие ко многим полям. Для этого существует открытая проблема, и из вашего комментария к этой проблеме ясно что вы тоже это обнаружили.

Решение

Кэш-машина Чака. Кэширование всех запросов почти никогда не стоит того. Это приводит ко всем трудным для поиска ошибкам и проблемам. Лучший подход — оптимизировать ваши таблицы и точно настроить запросы. Если вы обнаружите, что конкретный запрос слишком медленный, кэшируйте его вручную.

person e4c5    schedule 19.06.2016

Это было мое обходное решение:

    >>> m1 = MyModel1.objects.all()[0]
    >>> m1
    <MyModel1: ID: 8887972990743179; name: my-name-blahblah>

    >>> m2 = MyModel2.objects.all()[0]
    >>> m2.model1.all()
    []
    >>> m2.model1.add(m1)
    >>> m2.model1.all()
    []

    >>> MyModel1.objects.invalidate(m1)
    >>> MyModel2.objects.invalidate(m2)
    >>> m2.save()
    >>> m2.model1.all()
    [<MyModel1: ID: 8887972990743179; name: my-name-blahblah>]
person Saqib Ali    schedule 16.06.2016
comment
Это действительно ненужно. - person Burhan Khalid; 18.06.2016
comment
@Burhan, каков правильный обходной путь для этой ошибки Django? - person Saqib Ali; 19.06.2016
comment
это не ошибка джанго. Если это вообще ошибка в django-cache-machine, которая является сторонним приложением - person e4c5; 19.06.2016

Рассматривали ли вы подключение к сигналам модели для аннулирования кеша при добавлении объекта? В вашем случае вы должны посмотреть M2M Changed Сигнал

Небольшой пример, который не решает вашу проблему, но связывает обходной путь, который вы дали ранее, с моим подходом к решению сигналов (я не знать django-cache-machine):

def invalidate_m2m(sender, **kwargs):
    instance = kwargs.get('instance', None)
    action = kwargs.get('action', None)

    if action == 'post_add':
        Sender.objects.invalidate(instance)

m2m_changed.connect(invalidate_m2m, sender=MyModel2.model1.through)
person A. J. Parr    schedule 21.06.2016

Ответ AJ Parr почти правильный, но вы забыли post_remove, а также можете привязать его к каждому полю ManytoMany следующим образом:

from django.db.models.signals import m2m_changed
from django.dispatch import receiver

@receiver(m2m_changed, )
def invalidate_cache_m2m(sender, instance, action, reverse, model, pk_set, **kwargs ):
    if action in ['post_add', 'post_remove'] :
        model.objects.invalidate(instance)
person Etienne Pouliot    schedule 17.05.2019