Photo by Jandira Sonnendeck on Unsplash
Redis Caching in Django Rest Framework, Complete Guide
The basic purpose of caching is to increase data retrieval performance by reducing the need to access the underlying slower storage layer.
We will implement Redis caching in Django Rest Framework properly this time.
Project Setup
- I'm using Docker to start a Redis database server.
docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
- Start a Django project and install all required libraries.
pip install django djangorestframework django-redis
mkdir django-redis-example && cd django-redis-example
django-admin startproject config .
python manage.py startapp core
- Update
config/settings.py
accordingly for the rest framework and Redis
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework", # For Django REST Framework
"core", # For core application
]
# Connect to Redis server for cache backend
# LINK - https://github.com/jazzband/django-redis#configure-as-cache-backend
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://localhost:6379/1",
}
}
# Set JSON as a default Renderer
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
}
- Create a Model and ModelViewSet to perform CRUD operation on this particular model.
# core/models.py
class City(models.Model):
name = models.CharField(max_length=100)
state_code = models.CharField(max_length=100)
state_name = models.CharField(max_length=100)
country_code = models.CharField(max_length=100)
country_name = models.CharField(max_length=100)
def __str__(self):
return "%s: %s: %s" % (self.name, self.state_code, self.country_code)
# core/serializers.py
from core.models import City
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = City
fields = "__all__"
# core/views.py
from rest_framework.viewsets import ModelViewSet
from core.models import City
from core.serializers import CitySerializer
class CityView(ModelViewSet):
serializer_class = CitySerializer
queryset = City.objects.all()
http_method_names = ["get", "post", "patch", "delete"]
# config/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from core.views import CityView
router = DefaultRouter()
router.register("", CityView, basename="root")
urlpatterns = [
path("", include(router.urls)),
]
Here the basic setup is complete and we can List, Update, Retrieve and Delete from the City model at localhost:8000
.
Adding Caching in the view
Now we will modify ModelViewSet's list()
function to cache the GET request data for 5 minutes.
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
class CityView(ModelViewSet):
...
@method_decorator(cache_page(300, key_prefix="city-view"))
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
When we call the GET request for the first time, the response will be stored in our Redis database with two keys.
:1:views.decorators.cache.cache_header.city-view.cb4fbe6a5f6caec1f1715b85fecd2d7f.en-us.UTC
:1:views.decorators.cache.cache_page.city-view.GET.cb4fbe6a5f6caec1f1715b85fecd2d7f.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC
Here, 1
is the database name, we are using Django's default cache_page therefore the prefix views.decorators.cache
is always going to be the same for Redis, Memcached, or any other caching database. city-view
is the key_prefix we assigned in cache_page
in our views file. We should always use unique keys. In the end .en-us.UTC
is our project's LANGUAGE_CODE
and TIME_ZONE
which are already specified in the settings.py
file.
Problem: Suppose we call the GET request and the response is cached for 5 minutes, now we updated something in the city model. We are not able to retrieve that updated data until the cached data is expired. Therefore we need to make sure whenever the data is updated in our database, the cache related to that model is also updated or deleted.
Solution: we will create a function that will delete the cached data by passing the key_prefix we specified in the view which iscity-view
in our case.
# config/utils.py
from django.core.cache import cache
from django.conf import settings
# LINK - https://github.com/jazzband/django-redis#scan--delete-keys-in-bulk
def delete_cache(key_prefix: str):
"""
Delete all cache keys with the given prefix.
"""
keys_pattern = f"views.decorators.cache.cache_*.{key_prefix}.*.{settings.LANGUAGE_CODE}.{settings.TIME_ZONE}"
cache.delete_pattern(keys_pattern)
to use this function in our view we will override create, destroy, and partial_update functions a little bit.
from config.utils import delete_cache
class CityView(ModelViewSet):
...
CACHE_KEY_PREFIX = "city-view"
@method_decorator(cache_page(300, key_prefix=CACHE_KEY_PREFIX))
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
response = super().create(request, *args, **kwargs)
delete_cache(self.CACHE_KEY_PREFIX)
return response
def destroy(self, request, *args, **kwargs):
response = super().destroy(request, *args, **kwargs)
delete_cache(self.CACHE_KEY_PREFIX)
return response
def partial_update(self, request, *args, **kwargs):
response = super().partial_update(request, *args, **kwargs)
delete_cache(self.CACHE_KEY_PREFIX)
return response
The reason we are using a variable response
to assign the super function instead of directly returning that is that if any validation error or object not found error is thrown. The cache will not be deleted. The proper update needs to be done before deleting it.