How to Filter Combined Querysets in Django Rest Framework

If you’ve ever tried to combine querysets from multiple models in Django Rest Framework (DRF) and then apply filters, you’ve likely encountered cryptic errors like AttributeError: 'list' object has no attribute 'model' or NotSupportedError when using union(). Let’s break down why this happens and how to implement a robust solution while keeping DRF’s default filtering intact.

Why Combining Querysets Breaks Filters

Your goal is to merge querysets from models that inherit from a common parent (e.g., Transaction) and apply filters using django-filters. Here’s why your initial approach fails:

  • Returning a List:
    Using list(chain(...)) converts the querysets into a Python list. DRF’s DjangoFilterBackend expects a QuerySet to apply database-level filtering. Lists lack the model attribute, causing the AttributeError.
  • Using union():
    While queryset1.union(queryset2) returns a QuerySet, Django’s ORM restricts filtering after union(), as noted in the NotSupportedError.

Proxy Models and Custom Filtering

To combine querysets while preserving DRF’s filtering capabilities, follow these steps:

Define a Proxy Model

Create a proxy model that mirrors the parent Transaction model. This allows querying all child models as if they were a single model:

# models.py  
class Transaction(models.Model):  
    # Common fields (e.g., company, description, amount, created_at)  
    company = models.ForeignKey(Company, on_delete=models.CASCADE)  
    description = models.CharField(max_length=200)  
    amount = models.DecimalField(max_digits=10, decimal_places=2)  
    created_at = models.DateTimeField(auto_now_add=True)  

    class Meta:  
        abstract = True  # Parent model is abstract  

# Proxy model to unify child models  
class UnifiedTransaction(Transaction):  
    class Meta:  
        proxy = True  

Override get_queryset with Q Objects

Use Django’s Q objects to query all child models through the proxy:

# views.py  
from django.db.models import Q  

class TransactionViewSet(viewsets.ReadOnlyModelViewSet):  
    serializer_class = TransactionSerializer  
    filter_backends = [DjangoFilterBackend, filters.SearchFilter]  
    filterset_fields = {"company": ["exact"], "amount": ["gte", "lte"]}  
    search_fields = ["id", "description"]  

    def get_queryset(self):  
        # Query all child models using Q objects  
        return UnifiedTransaction.objects.filter(  
            Q(paymentcollection__isnull=False) |  
            Q(upfrontsale__isnull=False)  
        ).select_related("company")  

Handle Reverse Relations

If child models like PaymentCollection and UpFrontSale have a OneToOneField to Transaction, use isnull checks to include them in the proxy queryset.

Adding Custom Filter Functionality

Let’s enhance the viewset with date filtering and ordering:

Custom Date Filter

Extend filterset_fields and add a date range filter:

# filters.py  
import django_filters  
from .models import UnifiedTransaction  

class UnifiedTransactionFilter(django_filters.FilterSet):  
    created_after = django_filters.DateTimeFilter(  
        field_name="created_at", lookup_expr="gte"  
    )  
    created_before = django_filters.DateTimeFilter(  
        field_name="created_at", lookup_expr="lte"  
    )  

    class Meta:  
        model = UnifiedTransaction  
        fields = ["company", "amount", "created_after", "created_before"]  

# Update the viewset  
class TransactionViewSet(viewsets.ReadOnlyModelViewSet):  
    filterset_class = UnifiedTransactionFilter  

Enable Ordering

Add OrderingFilter to sort results:

filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]  
ordering_fields = ["created_at", "amount"]  

Final Thoughts

  1. Avoid Combining Querysets as Lists:
    Always return a QuerySet to retain DRF’s filtering, ordering, and pagination.
  2. Proxy Models Are Your Friend:
    Use proxy models to unify querysets from related models without duplicating data.
  3. Test SQL Queries:
    Run print(queryset.query) to ensure your Q objects generate efficient SQL.
  4. Use Custom Filtersets:
    For complex logic (e.g., date ranges), define a FilterSet class instead of relying on filterset_fields.

By structuring your querysets around proxy models and Django’s Q objects, you maintain compatibility with DRF’s built-in filtering while keeping your code clean and scalable.

Related blog posts