Combining models with Django REST Framework

7/6/2014

Recently I was working on a Django project that called for me to combine two models and expose them over a REST API, in this case by using Django REST Framework. After thinking about the application structure, I determined I had three main options:

  1. Create a base class to query.
  2. Cheat and make two calls to the API, and combine the data on the client side.
  3. Combine two queries, and expose the results over your API.

Option 1 in many cases may be the cleanest option, but in my case the models were barely related, and having a base class would add a layer which could lead to unnecessary join queries in other parts of the app. Option 2 is basically just a hack which may be acceptable if you need to display all of the data to the user, and you prefer to handle filtering on the client side. In my case though, I wanted the ten most recently created objects for each model, so this wasn't an options. The other thing to think about is how you intend on using the REST endpoint; if you need full CRUD functionality, creating a base class makes a lot of sense. In my case I wanted a read-only feed, so the first two options seemed like poor choices. This post will walk through Option 3, implemented with Django REST Framework.

An example, and some code

Let's say you want to create a feed for a social network site where each user has a single feed. You want to display a list of FeedItems, each item containing the category and the content. You want to display all of the Post objects for a user, and also all of the user's unanswered FriendRequests. The first thing you should do is create the serializer for your FeedItem in serializers.py:

  # serializers.py

  class FeedItemSerializer(serializers.Serializer):
      item_type = serializers.CharField(max_length=20)
      data = serializers.CharField(max_length=10000)

Note - data is simply a generic field to hold whatever data is actually exposed by the unrelated models. Next, we are going to use Django REST Framework's ListAPIView. All we need to do is specify our custom serializer, and then override the get_queryset method to return an iterable - our custom combined list. To create this we are going to use the handy itertools library. Here's what our code might look like in our example:

from itertools import chain
from rest_framework.generics import ListAPIView
from .serializers import FeedItemSerializer

class FeedList(ListAPIView):

    permission_classes = (YourPermissionClass,)
    serializer_class = FeedItemSerializer
    paginate_by = 20

    def get_queryset(self):

        queryset_a = Post.objects.all()
        queryset_b = FriendRequest.objects.filter(is_unanswered=True)

        # Create an iterator for the querysets and turn it into a list.
        results_list = list(chain(queryset_a, queryset_b))

        # Optionally filter based on date, score, etc.
        sorted_list = sorted(results_list, key=lambda instance: -instance.date_created)

        # Build the list with items based on the FeedItemSerializer fields
        results = list()
        for entry in sorted_list:
            item_type = entry.__class__.__name__.lower()
            if isinstance(entry, Post):
                serializer = PostSerializer(entry)
            if isinstance(entry, FriendRequest):
                serializer = FriendRequestSerializer(entry)

            results.append({'item_type': item_type, 'data': serializer.data})

        return results

After hooking up the url for this view, you should be able to use the endpoint like any other list view. The obvious downside to this solution is that it has to perform two queries - but if you've read this far, you understand this is a trade-off for keeping your models separated. Overall, I've found this to be a rather elegant solution when inheritance isn't an option. If performance is mission critical, caching the API call with something with memcached or redis makes a lot of sense as it will lessen the database bottleneck greatly. Cheers!