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’sDjangoFilterBackend
expects aQuerySet
to apply database-level filtering. Lists lack themodel
attribute, 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 aQuerySet
to 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 yourQ
objects generate efficient SQL. - Use Custom Filtersets:
For complex logic (e.g., date ranges), define aFilterSet
class 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.