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:
Usinglist(chain(...))converts the querysets into a Python list. DRF’sDjangoFilterBackendexpects aQuerySetto apply database-level filtering. Lists lack themodelattribute, causing theAttributeError. - Using
union():
Whilequeryset1.union(queryset2)returns aQuerySet, Django’s ORM restricts filtering afterunion(), as noted in theNotSupportedError.
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
- Avoid Combining Querysets as Lists:
Always return aQuerySetto retain DRF’s filtering, ordering, and pagination. - Proxy Models Are Your Friend:
Use proxy models to unify querysets from related models without duplicating data. - Test SQL Queries:
Runprint(queryset.query)to ensure yourQobjects generate efficient SQL. - Use Custom Filtersets:
For complex logic (e.g., date ranges), define aFilterSetclass instead of relying onfilterset_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.