Caching in Django REST Framework: Memcached
Tags: django, memcached, caching, python
Published: 2020-07-04 16:48:18

Caching something is to save the result of an expensive calculation so that we don’t have to perform the calculation next time. We will discuss the caching in Django REST framework step by step:

* [Caching Concept](#chapter-1)
* [Setting up the Memcached](#chapter-2)
* [Adding Cache Config in `settings.py`](#chapter-3)
* [Caching API Response](#chapter-4)
* [Invalidating Cached Data](#chapter-5)

##**Caching Concept** <a name="chapter-1"></a>
This is the pseudocode of the general caching concept:
```python
get data from the cache using a key
if the key is in the cache:
    return the cached data
else:
    generate data
    save the generated data in the cache with a key(for next time)
    return the generated data
```

##**Setting up the Memcached** <a name="chapter-2"></a>:
  * [Install Memcached](#2-chapter-1)
  * [Install a Python Client](#2-chapter-2)

###**Install Memcached** <a name="2-chapter-1"></a>:
We will install [Memcached](https://memcached.org/) using docker [image](https://hub.docker.com/_/memcached).
```bash
docker run -d memcached -p 11211:11211 -m 64
```
This would set the Memcached server to use 64 megabytes for storage running at *11211* port.

###**Install a Python Client** <a name="2-chapter-2"></a>:
We will install [pylibmc](https://sendapatch.se/projects/pylibmc/), a python client to communicate with the Memcached server. But at first, we need to install the required dependencies of `pylibmc`,

For `ubuntu` or ubuntu-based docker image,
```bash
sudo apt-get install libmemcached-dev zlib1g-dev
```
For `MacOS`,
```bash
brew install libmemcached
```
Then install the PyPI package,
```bash
pip install pylibmc
```
##**Adding Cache Config in settings.py** <a name="chapter-3"></a>:
```python
CACHE_HOST='127.0.0.1'
CACHE_PORT=11211
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
        'LOCATION': f'{CACHE_HOST}:{CACHE_PORT}',
    }
}
```
##**Caching API Response** <a name="chapter-4"></a>:
Suppose, We have a `user` app. We will have the following functionalities:
 * We will implement caching at user details API `/api/user/{id}/`. 
 * Any request at this API will try to get from the cache. If no cached data exists for a user_id, user details data will be fetched from Database.
 * Any change or deletion on the User object will invalidate the cache.
The following is the `user details` API,
```python
from rest_framework.viewsets import ModelViewSet
from rest_framework.response import Response
from django.core.cache import cache

from apps.user.models import User
from apps.user.serializers import UserSerializer

class UserViewSet(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    def retrieve(self, request, pk=None):
        cache_key = f"user_details_{pk}"

        data = cache.get(cache_key)
        if data:
            return Response(data)

        response = super().retrieve(request, pk=pk)
        cache.set(cache_key, response.data)
        return response
```
## **Invalidating Cached Data** <a name="chapter-5"></a>:
We will invalidate the cached data using [django signal](https://docs.djangoproject.com/en/dev/topics/signals/).
* [Creating Signal Receivers](#chapter-5-1)
* [Connecting Receivers with Signals](#chapter-5-2)
* [Changing `default_app_config`](#chapter-5-3)

### **Creating Signal Receivers** <a name="chapter-5-1"></a>:
We will add the following lines in `user/signals.py`,
```python
def _invalidate_cached_data(user_id):
    cache_key = f"user_details_{pk}"
    cache.delete(cache_key)


def user_post_save_handler(sender, instance, created, **kwargs):
    if not created:
        _invalidate_cached_data(instance.id)


def user_post_delete_handler(sender, instance, **kwargs):
    _invalidate_cached_data(instance.id)
```
### **Connecting Receivers with Signals** <a name="chapter-5-2">:
We will add the following line at `user/apps.py` inside user app,
```python
from django.apps import AppConfig
from django.db.models.signals import post_save, post_delete


class UserAppConfig(AppConfig):
    name = "apps.user"

    def ready(self):
        from apps.user.models import User
        from apps.user.signals import (
            user_post_delete_handler,
            user_post_save_handler,
        )

        post_save.connect(user_post_save_handler, sender=User)
        post_delete.connect(user_post_delete_handler, sender=User)
```
### **Changing `default_app_config`** <a name="chapter-5-3">:
We will add the following line at `user/__init__.py` in `user` app.
```python
default_app_config = "apps.user.apps.UserAppConfig"
```

Feel free to leave a comment.