Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django serialize multiple objects in one call

I was wondering how I can decrease the number of calls to my database when serializing:

I have the following 2 models:

 class House(models.Model):
     name = models.CharField(max_length = 100, null = True, blank = True)
     address = models.CharField(max_length = 500, null = True, blank = True)

 class Room(models.Model):
      house = models.ForeignKey(House)
      name  = models.CharField(max_length = 100)

There is 1 house, it can have multiple Room.

I am using django-rest-framework and trying to serialize all 3 things together at the house level.

 class HouseSerializer(serializers.ModelSerializer)
    rooms = serializers.SerializerMethodField('room_serializer')

    def room_serializer(self):
         rooms = Room.objects.filter(house_id = self.id) # we are in House serializer, so self is a house
         return RoomSerializer(rooms).data

    class Meta:
        model = House
        fields = ('id', 'name', 'address')

So now, for every house I want to serialize, I need to make a separate call for its Rooms. It works, but that's an extra call. (imagine me trying to package a lot of stuff together!)

Now, if I had 100 houses, to serialize everything, I would need to make 100 Database hits, O(n) time

I know I can decrease this to 2 hits, if I can get all the information together. O(1) time

my_houses = Houses.objects.filter(name = "mine")
my_rooms = Rooms.objects.filter(house_id__in = [house.id for house in my_houses])

My question is how can I do this? and get the serializers to be happy?

Can I somehow do a loop after doing my two calls, to "attach" a Room to a House, then serialize it? (am I allowed to add an attribute like that?) If I can, how do i get my serializer to read it?

Please note that I do not need django-rest-serializer to allow me to change the attributes in the Rooms, this way. This is for GET only.

like image 668
user1639926 Avatar asked Nov 30 '14 09:11

user1639926


1 Answers

As it is currently writen, using a SerializerMethodField, you are making N+1 queries. I have covered this a few times on Stack Overflow for optimizing the database queries and in general, it's similar to how you would improve the performance in Django. You are dealing with a one-to-many relationship, which can be optimized the same way as many-to-many relationships with prefetch_related.

class HouseSerializer(serializers.ModelSerializer)
    rooms = RoomSerializer(read_only=True, source="room_set", many=True)

    class Meta:
        model = House
        fields = ('id', 'name', 'address', )

The change I made uses nested serializers instead of manually generating the serializer within a SerializerMethodField. I had restricted it to be read_only, as you mentioned you only need it for GET requests and writable serializers have problems in Django REST Framework 2.4.

As your reverse relationship for the Room -> House relationship has not been set, it is the default room_set. You can (and should) override this by setting the related_name on the ForeignKey field, and you would need to adjust the source accordingly.

In order to prevent the N+1 query issue, you will need to override the queryset on your view. In the case of a generic view, this would be done on the queryset attribute or within the get_queryset method like queyset = House.objects.prefetch_related('room_set'). This will request all of the related rooms alongisde the request for the House object, so instead of N+1 requests you will only have two requests.

like image 136
Kevin Brown-Silva Avatar answered Sep 26 '22 23:09

Kevin Brown-Silva