How to prefetch_related() for GenericForeignKeys? - django

I have a List that consists of ListItems. These ListItems then point towards either a ParentItem or a ChildItem model via a GenericForeignKey:
# models.py
class List(models.Model):
title = models.CharField()
class ListItem(models.Model):
list = models.ForeignKey(List, related_name="list_items")
order = models.PositiveSmallIntegerField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey("content_type", "object_id")
class ParentItem(models.Model):
title = models.CharField()
class ChildItem(models.Model):
title = models.CharField()
parent = models.ForeignKey(ParentItem, related_name="child")
I want to display a list of all my Lists with their ListItems and respective ItemA/ItemB data using ListSerializer:
# serializers.py
class ParentItemSerializer(serializers.ModelSerializer):
class Meta:
model = ParentItem
fields = ["title"]
class ChildItemSerializer(serializers.ModelSerializer):
parent = ParentItemSerializer()
class Meta:
model = ChildItem
fields = ["title", "parent"]
class ListItemSerializer(serializers.ModelSerializer):
contents = serializers.SerializerMethodField()
class Meta:
model = ListItem
fields = ["contents"]
def get_contents(self, obj):
item = obj.content_object
type = item.__class__.__name__
if type == "ParentItem":
return ParentItemSerializer(item).data
elif type == "ChildItem":
return ChildItemSerializer(item).data
class ListSerializer(serializers.ModelSerializer):
items = serializers.SerializerMethodField()
class Meta:
model = List
fields = ["title", "items"]
def get_items(self, obj):
return ListItemSerializer(obj.list_items, many=True).data
How can I optimize my List queryset to prefetch these GenericForeignKey relationships?
# views.py
class ListViewSet(viewset.ModelViewSet):
queryset = List.objects.all()
serializer_class = ListSerializer
List.objects.all().prefetch_related("list_items") works but the following does not seem to work:
List.objects.all().prefetch_related(
"list_items",
"list_items__content_object",
"list_items__content_object__parent",
)
I've read the documentation on prefetch_related which suggests it should work:
While prefetch_related supports prefetching GenericForeignKey
relationships, the number of queries will depend on the data. Since a
GenericForeignKey can reference data in multiple tables, one query per
table referenced is needed, rather than one query for all the items.
There could be additional queries on the ContentType table if the
relevant rows have not already been fetched.
but I don't know if that's applicable to DRF.
Edit: Some better success when I move some of the prefetching to the serializer:
class ListSerializer(serializers.ModelSerializer):
def get_items(self, obj):
return ListItemSerializer(obj.list_items.all().prefetch_related("content_object"), many=True).data

As per my edit, prefetching works as intended when I move the relevant fields when they're queried in the serializer instead of cramming it all in the view's queryset.

Related

Got AttributeError when attempting to get a value for field `members` on serializer `HealthQuotationSerializer`

Trying out serialising parent and child model.Here are my models:
class HealthQuotation(models.Model):
quotation_no = models.CharField(max_length=50)
insuredpersons = models.IntegerField()
mobile_no = models.CharField(max_length=10)
def __str__(self):
return self.quotation_no
class HealthQuotationMember(models.Model):
premium = models.FloatField(null=True)
suminsured = models.FloatField()
quotation = models.ForeignKey(HealthQuotation,on_delete=models.CASCADE)
def __str__(self):
return str(self.quotation)
Here are my serializers:
class HealthQuotationMemberSerializer(serializers.ModelSerializer):
class Meta:
model = HealthQuotationMember
fields= "__all__"
class HealthQuotationSerializer(serializers.ModelSerializer):
members = HealthQuotationMemberSerializer(many=True)
class Meta:
model = HealthQuotation
fields = ['id','members']
On Serialising parent model with parent serializer, Django throws error "Got AttributeError when attempting to get a value for field members on serializer HealthQuotationSerializer. The serializer field might be named incorrectly and not match any attribute or key on the HealthQuotation instance. Original exception text was: 'HealthQuotation' object has no attribute".
Because you don't have members field in your model... Try to change your serializer as following and see if it works:
class HealthQuotationSerializer(serializers.ModelSerializer):
quotation = HealthQuotationMemberSerializer()
class Meta:
model = HealthQuotation
fields = ['id','quotation']
Note that I've removed many=True because there will be always one object per this data (ForeignKey). when you have more than one object such as Many2Many you should use many=True.
You have "HealthQuotation" as a parent and "HealthQuotationMember" as a child.
Now, you have decided to retrieve data from parent "HealthQuotation"
and its associated children which will come from "HealthQuotationMember", right?
To achieve that you can use Django SerializerMethodField():
Your serializers.py should look like:
class HealthQuotationMemberSerializer(serializers.ModelSerializer):
class Meta:
model = HealthQuotationMember
fields= '__all__'
class HealthQuotationSerializer(serializers.ModelSerializer):
members = serializers.SerializerMethodField() # I am using SerializerMethodField()
class Meta:
model = HealthQuotation
fields = '__all__'
def get_members(self, quotation):
q = HealthQuotationMember.objects.filter(quotation = quotation)
serializer = HealthQuotationMemberSerializer(q, many=True)
return serializer.data
Your views.py
class GetHealthQuotationList(ListAPIView):
serializer_class = HealthQuotationSerializer
queryset = HealthQuotation.objects.all()
Your url.py should be:
path('get-health-quotation-list', GetHealthQuotationList.as_view()),
NOTE: In case you plan to retrieve data from child table and find its associated parent, then your serializer should be good to go without many=True argument.

Django - How to filter children of a nested queryset?

I have this model called Menu which has a many-to-many relationship with another model called category. Both this models have a field called is_active which indicates that menu or category is available or not.
Alright, then I have an api called RestaurantMenus, which returns all active menus for a restaurant with their categories extended, the response is something like this:
Menu 1
Category 1
Category 2
Menu 2
Category 3
Menu 3
Category 4
Category 5
Category 6
Now what I try to achieve is to only seriliaze those menus and categories which are active (is_active = True). To filter active menus is simple but to filter its children is what I'm struggling with.
My Models:
class Category(models.Model):
menu = models.ForeignKey(Menu, related_name='categories', on_delete=models.CASCADE)
name = models.CharField(max_length=200)
class Menu(models.Model):
name = models.CharField(max_length=200)
My Serializers:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = "__all__"
class MenuSerializer(serializers.ModelSerializer):
categories = CategorySerializer(many=True, read_only=True)
class Meta:
model = Menu
fields = "__all__"
P.S. Category model itself has a many-to-many relationship with another model Called Item, which has an is_active field too. I want the same effect for those too but I cut it from the question cause I think the process should be the same. So actually the api response is something like this:
Menu 1
Category 1
Item 1
Item 2
Item 3
Category 2
Item 4
Using a SerializerMethodField is pretty straightforward.
You can filter inside get_< attribute >, you serialize the queryset and return the data from this serialized queryset.
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = "__all__"
class MenuSerializer(serializers.ModelSerializer):
categories = serializers.SerializerMethodField('get_categories')
def get_categories(self, menu):
qs = Category.objects.filter(menu=menu, is_active=True)
serializer = CategorySerializer(qs, many=True)
return serializer.data
class Meta:
model = Menu
fields = "__all__"
If you have a third nested class, you can just apply the same simple logic to CategorySerializer using a get_items and ItemSerializer.
This approach lets you filter Menu objects to check is_active = True on your views.py since this is business logic.
Here you can use Django Prefetch objects. This lets you define queryset to choose the set objects from.
So, you would write your Menu retrieval query something like this:
from django.db.models import Prefetch
menus = Menu.objects.filter(is_active=True).prefetch_related(Prefetch('categories', queryset=Category.objects.filter(is_active=True)))
To add category items as well to the result set, use the following query:
menus = Menu.objects.filter(is_active=True).prefetch_related(Prefetch('categories', queryset=Category.objects.filter(is_active=True)), Prefetch('categories__items', queryset=Item.objects.filter(is_active=True))))
This should solve your problem.
Note: I have not tested the code so you might need to make some modifications.
I assume you define your models like this
Menu
is_active
categories: ManyToManyField (related_name='menus')
Category
is_active
items: ManyToManyField (related_name='categories')
Item
is_active
E.g.
def get_categories():
active_category_qs = Category.objects.filter(is_active=True)
active_menu_qs = Menu.objects.filter(is_active=True,
categories__in=active_category_qs)
categories = []
for menu in active_menu_qs.iterator():
for category in menu.categories.all():
if category.id not in [
obj_target["id"] for obj_target in categories
]:
category_data = CategorySerializer(category).data # just get the data of category object
categories.append(category_data)
return categories
try this:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = "__all__"
class MenuSerializer(serializers.ModelSerializer):
category = SerializerMethodField()
def get_category(self,instance):
qs = Category.objects.filter(menu__id=instance.id)
data = CategorySerializer(qs,many=True).data
return data
class Meta:
model = Menu
fields = "__all__"
You can try like this using list_serializer_class:
class FilterActiveListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(is_active=True)
return super(FilterActiveListSerializer, self).to_representation(data)
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
list_serializer_class = FilterActiveListSerializer
class CategorySerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=True)
class Meta:
model = Category
list_serializer_class = FilterActiveListSerializer
class MenuSerializer(serializers.ModelSerializer):
categories = CategorySerializer(many=True, read_only=True)
class Meta:
model = Menu
item_instance = Item.objects.filter(is_active = True, category__is_active=True)
you will get all active items that have an active category using this.
fetch all items and create JSON data from them.
upvote if it helps

How do you filter a nested serializer in Django Rest Framework?

In Django Rest Framework, how do you filter a serializer when it's nested in another serializer?
My filters are imposed in the DRF viewsets, but when you call a serializer from inside another serializer, the viewset of the nested serializer never gets called, so the nested results appear unfiltered.
I have tried adding a filter on originating viewset, but it doesn't seem to filter the nested results because the nested results get called as a separate pre-fretched query. (The nested serializer is a reverse lookup, you see.)
Is it possible to add a get_queryset() override in the nested serializer itself (moving it out of the viewset), to add the filter there? I've tried that, too, with no luck.
This is what I tried, but it doesn't even seem to get called:
class QuestionnaireSerializer(serializers.ModelSerializer):
edition = EditionSerializer(read_only=True)
company = serializers.StringRelatedField(read_only=True)
class Meta:
model = Questionnaire
def get_queryset(self):
query = super(QuestionnaireSerializer, self).get_queryset(instance)
if not self.request.user.is_staff:
query = query.filter(user=self.request.user, edition__hide=False)
return query
You can subclass the ListSerializer and overwrite the to_representation method.
By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.
subclass ListSerializer, overwriting to_representation and then calling super
add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer
Here is the relevant code for your sample.
class FilteredListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(user=self.context['request'].user, edition__hide=False)
return super(FilteredListSerializer, self).to_representation(data)
class EditionSerializer(serializers.ModelSerializer):
class Meta:
list_serializer_class = FilteredListSerializer
model = Edition
class QuestionnaireSerializer(serializers.ModelSerializer):
edition = EditionSerializer(read_only=True)
company = serializers.StringRelatedField(read_only=True)
class Meta:
model = Questionnaire
While all the above answers work, I find the use of Django's Prefetch object the easiest way of all.
Say a Restaurant obj has a lot of MenuItems, some of which are is_removed == True, and you only want those that are not removed.
In RestaurantViewSet, do something like
from django.db.models import Prefetch
queryset = Restaurant.objects.prefetch_related(
Prefetch('menu_items', queryset=MenuItem.objects.filter(is_removed=False), to_attr='filtered_menu_items')
)
In RestaurantSerializer, do something like
class RestaurantSerializer(serializers.ModelSerializer):
menu_items = MenuItemSerializer(source='filtered_menu_items', many=True, read_only=True)
Tested many solutions from SO and other places.
Found only one working solution for Django 2.0 + DRF 3.7.7.
Define a method in model which has nested class. Craft a filter that will fit your needs.
class Channel(models.Model):
name = models.CharField(max_length=40)
number = models.IntegerField(unique=True)
active = models.BooleanField(default=True)
def current_epg(self):
return Epg.objects.filter(channel=self, end__gt=datetime.now()).order_by("end")[:6]
class Epg(models.Model):
start = models.DateTimeField()
end = models.DateTimeField(db_index=True)
title = models.CharField(max_length=300)
description = models.CharField(max_length=800)
channel = models.ForeignKey(Channel, related_name='onair', on_delete=models.CASCADE)
.
class EpgSerializer(serializers.ModelSerializer):
class Meta:
model = Epg
fields = ('channel', 'start', 'end', 'title', 'description',)
class ChannelSerializer(serializers.ModelSerializer):
onair = EpgSerializer(many=True, read_only=True, source="current_epg")
class Meta:
model = Channel
fields = ('number', 'name', 'onair',)
Pay attention to source="current_epg" and you'll get the point.
I find it easier, and more straight forward, to use a SerializerMethodField on the serializer field you want to filter.
So you would do something like this.
class CarTypesSerializer(serializers.ModelSerializer):
class Meta:
model = CarType
fields = '__all__'
class CarSerializer(serializers.ModelSerializer):
car_types = serializers.SerializerMethodField()
class Meta:
model = Car
fields = '__all__'
def get_car_types(self, instance):
# Filter using the Car model instance and the CarType's related_name
# (which in this case defaults to car_types_set)
car_types_instances = instance.car_types_set.filter(brand="Toyota")
return CarTypesSerializer(car_types_instances, many=True).data
This saves you from having to create many overrides of the serializers.ListSerializer if you need different filtering criteria for different serializers.
It also has the extra benefit of seeing exactly what the filter does within the serializer instead of diving into a subclass definition.
Of course the downside is if you have a serializer with many nested objects that all need to be filtered in some way. It could cause the serializer code to greatly increase. It's up to you how you would like to filter.
Hope this helps!
When a serializer is instantiated and many=True is passed, a
ListSerializer instance will be created. The serializer class then
becomes a child of the parent ListSerializer
This method takes the target of the field as the value argument, and
should return the representation that should be used to serialize the
target. The value argument will typically be a model instance.
Below is the example of the nested serializer
class UserSerializer(serializers.ModelSerializer):
""" Here many=True is passed, So a ListSerializer instance will be
created"""
system = SystemSerializer(many=True, read_only=True)
class Meta:
model = UserProfile
fields = ('system', 'name')
class FilteredListSerializer(serializers.ListSerializer):
"""Serializer to filter the active system, which is a boolen field in
System Model. The value argument to to_representation() method is
the model instance"""
def to_representation(self, data):
data = data.filter(system_active=True)
return super(FilteredListSerializer, self).to_representation(data)
class SystemSerializer(serializers.ModelSerializer):
mac_id = serializers.CharField(source='id')
system_name = serializers.CharField(source='name')
serial_number = serializers.CharField(source='serial')
class Meta:
model = System
list_serializer_class = FilteredListSerializer
fields = (
'mac_id', 'serial_number', 'system_name', 'system_active',
)
In view:
class SystemView(viewsets.GenericViewSet, viewsets.ViewSet):
def retrieve(self, request, email=None):
data = get_object_or_404(UserProfile.objects.all(), email=email)
serializer = UserSerializer(data)
return Response(serializer.data)
The following worked for me, from self.context['view'], You can get the filter params inside the serializer and use it however you want.
class ShipmentDocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ['id', 'created_date', 'consignment', 'document', 'org', 'title' ]
class ShipmentDocumentTypeSerializer(serializers.ModelSerializer):
documents = serializers.SerializerMethodField()
class Meta:
model = DocumentType
fields = ['id', 'type', 'documents']
def get_documents(self, instance):
consignment_id=self.context['view'].kwargs['consignment_id']
queryset = Document.objects.filter(consignment__id=consignment_id)
return ShipmentDocumentSerializer(queryset, many=True).data

django REST framework - limited queryset for nested ModelSerializer?

I have a ModelSerializer, but by default it serializes all the objects in my model. I would like to limit this queryset to only the most recent 500 (as opposed to all 50 million). How do I do this?
What I have currently is the following:
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
The reason I don't think I can just specify the queryset in my viewset is that this is in fact the nested portion of another serializer.
models.py
class Container(models.Model):
size = models.CharField(max_length=20)
shape = models.CharField(max_length=20)
class Item(models.Model):
container = models.ForeignKey(Container, related_name='items')
name = models.CharField(max_length=20)
color = models.CharField(max_length=20)
views.py
class ContainerViewSet(viewsets.ModelViewSet):
queryset = Container.objects.all() # only a handful of containers
serializer_class = ContainerSerializer
serializers.py
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('name', 'color')
class ContainerSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True) # millions of items per container
class Meta:
model = Container
fields = ('size', 'shape', 'items')
In your View Set you may specify the queryset like follows:
from rest_framework import serializers, viewsets
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()[:500]
serializer_class = MyModelSerializer
I think what you are looking for is the SerializerMethodField.
So your code would look as follows:
class ContainerSerializer(serializers.ModelSerializer):
items = SerializerMethodField('get_items')
class Meta:
model = Container
fields = ('size', 'shape', 'items')
def get_items(self, container):
items = Item.objects.filter(container=container)[:500] # Whatever your query may be
serializer = ItemSerializer(instance=items, many=True)
return serializer.data
The one catch is that the SerializerMethodField is read only.
You may use source parameter
class Container(models.Model):
...
def get_items(self):
return self.items[:500]
and in serializer
items = ItemSerializer(many=True, source='get_items', )

Django REST Framework - query limit on nested serializer?

I have a situation in which one table is related to another via a foreign key as follows:
models.py
class Container(models.Model):
size = models.CharField(max_length=20)
shape = models.CharField(max_length=20)
class Item(models.Model):
container = models.ForeignKey(Container, related_name='items')
name = models.CharField(max_length=20)
color = models.CharField(max_length=20)
serializers.py
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
class ContainerSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True)
class Meta:
model = Container
fields = ('size', 'shape', 'items')
This works fine, but my problem is that all the items in the container get serialized. I only want items with color='green' to be serialized.
class ContainerSerializer(serializers.ModelSerializer):
items = serializers.SerializerMethodField()
def get_items(self, obj):
query = Item.objects.filter(item_set__color='green')
serializer = ItemSerializer(query, many=True)
return serializer.data
class Meta:
model = Container
fields = ('size', 'shape', 'items')
Instead of changing how serializer works, a simplier way, its just filter you Container with green color items and them try to serialize it
You can do something like this:
container_objects = Container.objects.filter(id='your_container_id',item_set__color='green')
serialized_containers = YourContainerSerializer(data=container_objects)