Почему 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
, Как мне это исправить??
3 ответа
Действительно ли нужен 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, если кэш не был аннулирован.
Эта же ссылка объясняет, как ключи кеша становятся недействительными.
Для поддержки легкого аннулирования кэша мы используем "очищенные списки", чтобы пометить кэшированные запросы, к которым принадлежит объект. Таким образом, все запросы, в которых был найден объект, будут отменены при изменении этого объекта. Сброс списков сопоставляет ключ объекта списку ключей запроса.
При сохранении или удалении объекта все ключи запроса в его списке очистки будут удалены. Кроме того, очищенные списки его отношений с ключевыми ключами будут очищены. Чтобы избежать устаревших отношений с внешним ключом, любые кэшированные объекты будут сброшены, когда объект, на который указывает их внешний ключ, признан недействительным.
Понятно, что сохранение или удаление объекта приведет к тому, что многие объекты в кэше будут признаны недействительными. Таким образом, вы замедляете эти операции с помощью диспетчера кэша. Также стоит отметить, что в документации по аннулированию вообще не упоминается много полей. Существует открытая проблема для этого, и из вашего комментария по этой проблеме ясно, что вы обнаружили это тоже.
Решение
Чак кеш машинка. Кэширование всех запросов почти никогда не стоит. Это приводит ко всем видам трудно найти ошибки и проблемы. Лучший подход - оптимизировать ваши таблицы и точно настроить ваши запросы. Если вы нашли слишком медленный запрос, кешируйте его вручную.
Это было мое обходное решение:
>>> 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>]
Рассматривали ли вы использование сигналов модели для аннулирования кэша при добавлении объекта? Для вашего случая вы должны посмотреть на M2M измененный сигнал
Небольшой пример, который не решает вашу проблему, но связывает обходной путь, который вы дали ранее, с моим подходом к решению сигналов (я не знаю 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)
Ответ 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)