0

I have a Like model whose code looks like this:

class Like(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    liked_on = models.DateTimeField(auto_now_add=True)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey()

    class Meta:
        unique_together = ('customer', 'content_type', 'object_id')
        indexes = [
            models.Index(fields=["content_type", "object_id"]),
        ]

    def __str__(self):
        return f'{self.customer.user} likes {self.content_object}'

This generic relation enables me to reuse the likes model for the CampusAD and PropertyAD models which both have a GenericRelation to the Likes model.

In the PropertyADSerializer which feeds the endpoints, /api/properties and /api/properties/{property_id}, I have a SerializerMethodField which provides a boolean telling the front-end which properties have been liked by the current authenticated user (useful for showing a like icon on the page for users). The SerializerMethodField is defined as thus:

    def get_liked_by_authenticated_user(self, obj):
        request = self.context.get('request')
        if request and request.user.is_authenticated:
            customer = Customer.objects.get(user_id=request.user.id)
            content_type = ContentType.objects.get_for_model(PropertyAD)
            likes = Like.objects.filter(
                customer=customer, content_type=content_type,
                object_id=obj.id)
            return likes.exists()
        return False

Now, the issue with this approach is that it makes calls to the Customer and ContentType tables to fetch the required Customer and PropertyAD objects before using them for filtering, and this happens for each PropertyAD on the page resulting in hundreds of calls to the database as shown in the debug toolbar output below:

default 121.59 ms (147 queries including 141 similar and 71 duplicates )

    
SELECT ••• FROM `core_auth_customer` WHERE `core_auth_customer`.`user_id` = 1 LIMIT 21
71 similar queries. Duplicated 71 times.    
    1.29    
    
SELECT ••• FROM `properties_propertyad` WHERE `properties_propertyad`.`owner_id` = 1
    
    9.75    
    
SELECT ••• FROM `properties_propertyimage` WHERE `properties_propertyimage`.`ad_id` IN (9, 33, 58, 114, 118, 145, 149, 177, 185, 244, 248, 266, 271, 275, 281, 318, 331, 345, 349, 353, 354, 358, 381, 391, 395, 396, 406, 431, 435, 444, 453, 458, 491, 493, 518, 544, 548, 567, 594, 601, 652, 656, 671, 675, 704, 732, 746, 747, 777, 782, 808, 813, 821, 845, 893, 894, 896, 902, 908, 937, 950, 951, 957, 962, 969, 971, 973, 975, 1001, 1002)
    
    1.47    
    
SELECT ••• FROM `core_auth_customer` INNER JOIN `core_auth_user` ON (`core_auth_customer`.`user_id` = `core_auth_user`.`id`) WHERE `core_auth_customer`.`id` IN (1) ORDER BY `core_auth_user`.`first_name` ASC, `core_auth_user`.`last_name` ASC
    
    1.10    
    
SELECT ••• FROM `core_auth_customer` WHERE `core_auth_customer`.`user_id` = 1 LIMIT 21
71 similar queries. Duplicated 71 times.    
    1.05    
    
SELECT ••• FROM `likes_like` WHERE (`likes_like`.`content_type_id` = 6 AND `likes_like`.`customer_id` = 1 AND `likes_like`.`object_id` = 9) LIMIT 1
70 similar queries.     
    0.94    
    
SELECT ••• FROM `core_auth_customer` WHERE `core_auth_customer`.`user_id` = 1 LIMIT 21
71 similar queries. Duplicated 71 times.    
    0.93    
    
SELECT ••• FROM `likes_like` WHERE (`likes_like`.`content_type_id` = 6 AND `likes_like`.`customer_id` = 1 AND `likes_like`.`object_id` = 33) LIMIT 1
70 similar queries.     
    0.54    
    
SELECT ••• FROM `core_auth_customer` WHERE `core_auth_customer`.`user_id` = 1 LIMIT 21
71 similar queries. Duplicated 71 times. 


...

I have tried prefetching the Customer and PropertyAD objects as thus :

    def get_liked_by_authenticated_user(self, obj):
        request = self.context.get('request')
        if request and request.user.is_authenticated:
            customer = Customer.objects.get(user_id=request.user.id)
            content_type = ContentType.objects.get_for_model(PropertyAD)
            likes = Like.objects.prefetch_related('customer', 'content_type').filter(
                customer=customer, content_type=content_type,
                object_id=obj.id)
            return likes.exists()
        return False    def get_liked_by_authenticated_user(self, obj):
        request = self.context.get('request')
        if request and request.user.is_authenticated:
            customer = Customer.objects.get(user_id=request.user.id)
            content_type = ContentType.objects.get_for_model(PropertyAD)
            likes = Like.objects.prefetch_related('customer', 'content_type').filter(
                customer=customer, content_type=content_type,
                object_id=obj.id)
            return likes.exists()
        return False

...but that did not seem to make any difference to the queries. I also thought of caching the returned objects, but that does not seem like an ideal solution to the problem as they may change relatively often.

Are there any optimization techniques for filtering GenericRelations I may be unaware of, or is there a totally different way I can filter the likes without having to incur all those extra database calls? I've tried many iterations and they aren't working. Or... is there another way I should be supplying the information about what property has been liked by the authenticated user, to the front-end?

Sunderam Dubey
  • 1
  • 11
  • 20
  • 40

0 Answers0