To speed up the site, in addition to optimizing database queries, you can use caching.
Django allows you to cache:
- individual view , both Class Based View , and ordinary functions view
- whole templates or parts of these templates
- QuerySet
- as well as properties of model objects using cached_property
I was interested in the ability to cache individual properties of model objects for heavy computing or heavy database queries.
The
cached_property
decorator has such a functional, but the drawback for me was that caching occurred only for the lifetime of the object.
Whereas I need caching for a longer period of time than the existence of an object when requesting a page. And also I needed to cache properties depending on the input arguments. This decorator on the site caches the number of likes and dislikes, as well as information about whether the current user liked a particular content object.
Thus the decorator model_cached_property was written
model_cached_property
This decorator uses redis as a caching backend, because invalidating the cache may require deleting the group of keys that belong to this property. Since the property can be cached for different users in different ways.
Intall EVILEG-CORE
pip install evileg-core
Also evileg_core will pull up all the dependencies necessary for this package. Including the django-redis library, which is used to work with redis .
If you are not using redis yet, you will need to install it.
sudo apt install redis-server
settings.py
Add evileg_core to installed applications
INSTALLED_APPS = [ ... 'evileg_core', ]
Configuring the caching backend
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } }
Using model_cached_property
Normal caching
from evileg_core.cache.decorators import model_cached_property class Article(models.Model): @model_cached_property def comments_count(self): return self.comments.count()
This decorator will cache the number of comments on the article for 60 seconds. At the next request for the article page, the request will be executed first to the cache for a specific article object, and only if the cache has expired, the request will again be executed to the database.
Set Caching Timeout
If you want to set a specific caching time, you can use the timeout argument with the decorator.
class Article(models.Model): @model_cached_property(timeout=3000) def comments_count(self): return self.comments.count()
Global setting of cache duration
You can also set the global caching time for all decorators in settings.py in seconds
MODEL_CACHED_PROPERTY_TIMEOUT = 300000
Using properties with arguments
And now for the fun part. Caching properties with arguments, thanks to which you can cache the result using information about the input arguments of the property of the data model.
This is both an advantage of this decorator and its disadvantage. The thing is that caching is correct, it is necessary that the input arguments are unique. For example, if the input arguments are temporary non-unique objects, like AnonymousUser , then caching will not work.
However, the decorator can be used for caching depending on the user. It might look like this.
class Article(models.Model): @model_cached_property def __user_in_bookmarks(self, user): return self.bookmarks.filter(user=user).exists() def user_in_bookmarks(self, user): return self.__user_in_bookmarks(user) if user.is_authenticated else False
Please note that there is a check for user.is_authenticated , because caching of an unauthenticated user will not work correctly, but it will work correctly for an authenticated user, since the authenticated user is a unique object.
Cache invalidation
In the event that the cache has become irrelevant before the expiration of its life, then you can use the function invalidate_model_cached_property
from evileg_core.cache.utils import invalidate_model_cached_property class Article(models.Model): def invalidate_cache(self): invalidate_model_cached_property(self, self.comments_count)
For correct invalidation of the cache, you must pass the object whose cache you want to clear, as well as the method of this object.
It may look the same way.
article = get_object_or_404(Article, pk=12) invalidate_model_cached_property(article, article.comments_count)
Conclusion
Thus model_cached_property can be used to
- caching properties of model objects for a long time, more than the lifetime of the object when requesting a page
- caching properties of model objects depending on input arguments
There are limitations
- this decorator can only be used in models, that is, classes inherited from models.Model
- caching of model properties depending on the arguments should be performed only for unique input arguments, otherwise caching will not be correct
а functools.lru_cache в данном случае не поможет?
Добрый день.
Думаю, что не совсем. Технически здесь решается задача кэширования не последних вызовов функции, как в lru_cache, а кэширование свойств для ряда объектов модели данных. К тому же cache_clear() будет полностью удалять весь кэш, тогда как у меня предусмотрен более специфический функционал для инвалидации части ключей, которые перестали быть актуальны.
Давайте поясню на примере.
На сайте есть модель данных ForumTopic , у которой есть лайки и дислайки, а также они подсвечиваются, если пользователь выбрал лайк или дислайк. Вся эта информация у меня кэшируется, поскольку для лайков и дислайков используются GenericForeignKey и запросы к базе данных посылаются отдельно, что получается несколько накладно для страниц со списком тем на форуме. Поэтому я предпочитаю кэшировать эти данные и при срабатывание некоторых событий делать инвалидацию кэша.
Так вот, если использовать lru_cache, то он будет кэшировать лишь n-последних вызовов метода, то есть придётся отслеживать количество контента и вручную поправлять количество последних кэширований, если решать эту задачу в лоб. А в случае очистки кэша, при вызове страниц будут заново кэшироваться все свойства для всех объектов ForumTopic. Получается ситуация, если пользователь лайкнул один единственный пост, то кэш будет очищаться для всех объектов, чего я не хочу.
model_cached_property же работает немного иначе. Он кэширует информацию о модели данных, а именно о таблице в базе данных, а также об id объекта, а потом информацию об имени свойства и входных аргументах. В результате происходит уникальное кжширование для каждого отдельного объекта.
Когда вы вызываете функцию invalidate_model_cached_property, то кэш очищается только для конкретного объекта базы данных.
Таким образом, когда пользователь лайкнет один какой-то объект ForumTopic , то кэш будет очищен только для этого ForumTopic . И при следующем запросе список объектов ForumTopic будет выполняться запрос о лайках и дислайках только для одного единственного объекта. Плюс я не беспокоюсь о том, что мне нужно думать, какой максимальный размер кэша нужно делать.
Конечно, вопрос расхода памяти на кэширование является довольно важным и если появится бутылочное горлышко, то я буду его решать. Но на данный момент я поставил у себя длительность кэширования подобных активностей, как лайки, на целый месяц и на данный момент ещё не заметил проблем.
Но если я что-то не учёл и у вас есть, что дополнить, то с удовольствием выслушаю вашу точку зрения, возможно, что это поможет доработать функциональность model_cached_property
Всё конечно супер классно. Интересует лишь один вопрос.
Здравствуйте. В общем меня интересует такой вопрос. Я пробовал это на Like , Dislike. Как я понимаю если не перевалидировать кеш то ничего не изменится на странице. Вернётся значение из кэша? Отсюда второй вопрос как и где прописть эту перевалидацию? Ибо если я пишу её через модель как сказано в статье. То ничего не происходит. По этому интересует то как и где перевалидировать кеш.
Да, если не вызывать invalidate_cache , то ничего не произойдёт.
Место вызова инвалидации обычно индивидуально. Но я стараюсь вешать его на сигналы сохранения и удаления объектов.
В случае с Like Dislike , которые используют GenericForeignKey удобнее всего поступить следующим образом.
Добавить метод invalidate_cache , который будет вызывать метод инвалидации content_object
А потом навешать инвалидацию на сигналы
В таком случае инвалидация будет действовать на лету, главное, чтобы во всех моделях были, к которым привязываются лайки и дизлайки, были определены соответсвующие методы инвалидации.
Спасибо за пояснения. Я ток щас догнал как это всё работает. Просто с сигналами не работал никогда. Но вот теперь стало понятно.