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.

