Subscribe to Django ORM changes over a WebSocket connection

A common use case of WebSocket connections is delivering live updates to a frontend client. This is very useful in collaborative applications, where multiple users might be interacting with the system at once.

In our last post in this series Expose Django REST-like API over a WebSocket Connection we introduced Django Channels Rest framework as a means of exposing a REST-like API over WebSocket connections. Today we will take this further by investigating how DCRF can be used to observe changes in Django ORM models and thereby seamlessly synchronize both WebSocket and traditional HTTP API clients.

Django Channels groups API provides a simple approach to inform open WebSocket connections (consumer instances) of events. We need to send these events throughout our codebase.

It is important that these events are sent every time an item a client might be subscribed to is changed. Sometimes it is not easy to adjust/inject our existing code with these send events. Such as when models are updated through third-party libs that handle the CRUD operations: DRF and Django-Admin come to mind. In these cases adding the send events can become a tiresome, error-prone activity.

So in DCRF there is a helpful way to allow us to observe any Django ORM model instance without needing to make changes to our existing views, management commands etc.

# Subscribing to model changes with DCRF

In DCRF the mixin ObserverModelInstanceMixin adds an action to our WebSocket consumer that allows us to easily subscribe to any changes coming from all over our existing Django project, including changes made through Django admin and Django management commands, without needing to make any code changes.

To use this we add it as a mixin to our GenericAsyncAPIConsumer.

from django.contrib.auth import get_user_model

from djangochannelsrestframework import permissions
from djangochannelsrestframework.observer.generics import GenericAsyncAPIConsumer
from djangochannelsrestframework.mixins import ObserverModelInstanceMixin

from . import serializers

class UserConsumer(ObserverModelInstanceMixin, GenericAsyncAPIConsumer):
    queryset = get_user_modle().objects.all()
    serializer_class = serializers.UserSerializer
    permission_classes = (permissions.IsAuthenticated,)

We talked about the GenericAsyncAPIConsumer in detail in Expose Django REST-like API over a WebSocket Connectionn article.

In our WebSocket client we send JSON as follows.

{
    "action": "subscribe_instance",
    "request_id": 42, 
    "pk": 97  // the private key of the instance we want to subscribe to.
}

Once subscribed, the server will issue a message back to the subscribed client whenever our model is updated or removed by any user in a WebSocket consumer or a normal Django view, or even Django management command.

For example when the model is updated we get a message.

{
    "action": "update",
    "errors": [],
    "response_status": 200,
    "request_id": 42,
    "data": {
        'email': '42@example.com',
        'id': 42,
        'username': 'thenewname'
    }
}

This will utilize the serializer_class to build the data value.

To unsubscribe from an instance we'll send the following message.

{
    "action": "unsubscribe_instance",
    "request_id": 42,
    "pk": 97 
}

It's important to secure our API so that users can't just observe changes to models they are not permitted to. DCRF lets us override both the get_queryset and filter_queryset methods to limit the items we expose to a user over the WebSocket API.

There are some limitations to this approach: DCRF achieves this by observing Django’s signal system. This means it does not detect bulk operations such as queryset.update(). Also it does not observe changes made to the database from outside of our Django codebase.


Here we have looked into how to observe changes of a single model instance, however sometimes we need to subscribe to an entire query and be informed whenever the results of this query change. In our next post on DjangoChannelsRestFramework we will look into how to achieve this.