'Does using/calling method having calculating queryset in another method hits the database multiple times

I'm working on a DRF project to learn about ContentType models. I created a post model and comment model(ContentType) and then added comments to the post. Everything was working fine until I added django-debug-tool and duplicated queries.

I have the following questions:

  1. I've defined a method(children) and property(total_replies) on the comment model. Since total_replies just calling children method and count the size of queryset. Will it result in hitting the database two or more times in case I use the children method in some other methods or property?
  2. If the database is hitting multiple times, what solution two improve performance?
  3. After adding select_related the num of queries has been reduced drastically. Before using select_related enter image description here

After using select_related enter image description here Is it good to use select_related at all places where Foreignkey has been used?

Blog app

models.py

class Post(models.Model):
    title = models.CharField(verbose_name=_("Post Title"), max_length=50)
    content = models.TextField()
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='blog_posts')
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

    def __str__(self):
        return self.title

    @property
    def comments(self):
        instance = self
        #qs = Comment.objects.filter_by_instance(instance) #before
        qs = Comment.objects.select_related('user').filter_by_instance(instance)
        return qs

    @property
    def get_content_type(self):
        instance = self
        content_type = ContentType.objects.get_for_model(instance.__class__)
        return content_type


serializers.py
class PostSerializer(serializers.ModelSerializer):
    author = UserPublicSerializer(read_only=True)
    status_description = serializers.ReadOnlyField(source='get_status_display')

    class Meta:
        model = Post
        fields = (
            'url', 'id', 'title', 'author',
            'content', 'category', 'total_likes',
        )


class PostDetailSerializer(serializers.ModelSerializer):
    author = UserPublicSerializer(read_only=True)
    status_description = serializers.ReadOnlyField(source='get_status_display')
    comments = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = (
            'url', 'id', 'title', 'author', 'content',
            'category', 'comments', 'total_likes'
        )


    def get_comments(self, obj):
        request = self.context.get('request')
        comments_qs = Comment.objects.filter_by_instance(obj)
        comments = CommentSerializer(comments_qs, many=True, context={'request':request}).data
        return comments


class PostListCreateAPIView(generics.ListCreateAPIView):
    serializer_class = serializers.PostSerializer
    # queryset = Post.objects.all().order_by('-id') # before
    queryset = Post.objects.select_related('author').order_by('-id')
    name = 'post-list'
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

class PostRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
    serializer_class = serializers.PostDetailSerializer
    # queryset = Post.objects.all().order_by('-id') # before
    queryset = Post.objects.select_related('author').order_by('-id')
    name = 'post-detail'
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, account_permissions.IsStaffOrAuthorOrReadOnly]

    def perform_update(self, serializer):
        serializer.save(author=self.request.user)

Comment app

models.py

class CommentManager(models.Manager):
    def all(self):
        qs = super().filter(parent=None)
        return qs

    def filter_by_instance(self, instance):
        content_type = ContentType.objects.get_for_model(instance.__class__)
        object_id = instance.id
        qs = super().filter(content_type=content_type, object_id=object_id).select_related('user').filter(parent=None)
        return qs


class Comment(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='comments')
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey(ct_field='content_type', fk_field='object_id')
    parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)
    content = models.TextField()
   
    objects = CommentManager()

    def __str__(self):
        if self.is_parent:
            return f"comment {self.id} by {self.user}"
        return f"reply {self.id} to comment {self.parent.id} by {self.user}"

    def children(self):
        return Comment.objects.select_related('user').filter(parent=self)

    @property
    def is_parent(self):
        if self.parent is not None:
            return False
        return True

    @property
    def total_replies(self):
        return self.children().count()


serializers.py

class CommentSerializer(serializers.ModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
    user = UserPublicSerializer(read_only=True)

    class Meta:
        model = Comment
        fields = ('url', 'user', 'id', 'content_type', 'object_id', 'parent', 'content',  'total_replies',)


class CommentChildSerializer(serializers.ModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
    user = UserPublicSerializer(read_only=True)

    class Meta:
        model = Comment
        fields = ('url', 'user',  'id', 'content',)


class CommentDetailSerializer(serializers.ModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
    replies = serializers.SerializerMethodField()

    class Meta:
        model = Comment
        fields = ('url', 'id', 'content_type', 'object_id', 'content', 'replies', 'total_replies',)

    def get_replies(self, obj):
        request = self.context.get('request')
        if obj.is_parent:
            return CommentChildSerializer(obj.children(), many=True, context={'request':request}).data
        return None


views.py

class CommentListAPIView(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    queryset = Comment.objects.select_related('user').order_by('-id')
    name = 'comment-list'
    serializer_class = serializers.CommentSerializer

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

class CommentDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    queryset = Comment.objects.select_related('user').all()
    name = 'comment-detail'
    serializer_class = serializers.CommentDetailSerializer

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

Thanks in advance.



Solution 1:[1]

That's exactly what django docs says about select_related :

"Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query. This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries."

They describe select_related as something complex but good in term of transactional db cost.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jonatrios