Usage¶
drf-cached-instances is designed to work with Django REST Framework (DRF), using a cache for read-only operations such as getting an instance or list of instances. You may also want Celery for asynchronously updating the cache.
There are a few steps needed to integrate drf-cached-instances into your
project. See the sample app sample_poll_app
for a small example, or
browsercompat for a fuller example.
Create an app-specific cache strategy¶
drf-cached-instances requires that you specify how a model is cached, by adding methods to the Cache class. Each model requires three functions:
- A serializer, which turns a Django instance into a JSON-serializable dictionary,
- A loader, which loads a Django instance and related objects from the database, and
- An invalidator, which specifies which instance caches are possibly invalid when an instance is updated.
The naming convention for these functions are {model}_{version}_{function}
.
For example, the serializer for the User model for the ‘v1’ API would be
user_v1_serializer
. API/cache versioning is option, and the default
version name is ‘default’.
Here’s an example of a customized Cache:
from django.contrib.auth.models import User
from drf_cached_instances.cache import BaseCache
class MyCache(BaseCache):
"""Cache for my application."""
def user_default_serializer(self, obj):
"""Convert a User to a cached instance representation."""
if not obj:
return None
self.user_default_add_related_pks(obj)
return dict((
('id', obj.id),
('username', obj.username),
self.field_to_json('DateTime', 'date_joined', obj.date_joined),
))
def user_default_loader(self, pk):
"""Load a User from the database."""
try:
obj = User.objects.get(pk=pk)
except User.DoesNotExist:
return None
else:
self.user_default_add_related_pks(obj)
return obj
def user_default_add_related_pks(self, obj):
"""Add related primary keys to a User instance."""
if not hasattr(obj, '_votes_pks'):
obj._votes_pks = list(obj.votes.values_list('pk', flat=True))
def user_default_invalidator(self, obj):
"""Invalidate cached items when the User changes."""
return []
Use the cache in views¶
If you are using viewsets, add the CachedViewMixin to your viewset declarations:
from django.contrib.auth.models import User
from drf_cached_instances.mixins import CachedViewMixin
from rest_framework.viewsets import ModelViewSet
from rest_framework.serializers import DateField, ModelSerializer
class UserSerializer(ModelSerializer):
"""DRF serializer for Users."""
created = DateField(source='date_joined', read_only=True)
class Meta:
model = User
fields = ('id', 'username', 'created')
class UserViewSet(CachedViewMixin, ModelViewSet):
"""API endpoint that allows users to be viewed or edited."""
queryset = User.objects.all()
serializer_class = UserSerializer
Add signal hooks to update the cache¶
When an instance is updated, the cache is invalid and needs to be updated. This can be done by adding signal hooks for model modifications in models.py:
from django.contrib.auth.models import User
from django.db.models.signals import post_delete, post_save, m2m_changed
from django.dispatch import receiver
from .cache import MyCache
def update_cache_for_instance(model_name, instance_pk, instance):
cache = MyCache()
version = cache.default_version
to_update = cache.update_instance(
model_name, instance_pk, instance, version)
for related_name, related_pk, related_version in to_update:
update_cache_for_instance(
related_name, related_pk, version=related_version)
@receiver(post_delete, sender=User, dispatch_uid='post_delete_update_cache')
def post_delete_user_update_cache(sender, instance, **kwargs):
update_cache_for_instance('User', instance.pk, instance)
@receiver(post_save, sender=User, dispatch_uid='post_save_update_cache')
def post_save_user_update_cache(sender, instance, created, raw, **kwargs):
if raw:
return
update_cache_for_instance('User', instance.pk, instance)
This will follow the invalidation logic in the Cache class, to ensure that the cache is consistant across related instances.
Handling cascading cache updates¶
The update_cache_for_instance
method uses recursion to ensure the cache is
consistant. By default, this populates missing cache entries as well. For
highly related instances, this would result in loading a lot of the database
into a cold cache, making the first update very slow.
There are a few ways to handle the cold cache problem. The first is to use a asynchronous task system like Celery for updates. This way, updates can return quickly while backend processes warm the cache.
Another method is to use update_only=True
when calling
cache.update_instance
. This will stop the invalidation chain on cache
misses, which may result in an inconsistent cache for cached instances that are
a few steps away from the updates instance. Eventual consistency can be
maintained by automatically expiring cache entries.
You may want to configure update_only=True
in development for speed, and
use the default update_only=False
in production.