️(backend) prefetch thread comments tree to fix N+1 queries

Retrieving a thread serialized its nested comments, reactions and
reaction users without prefetching, issuing one query per comment for
its author and reactions and one query per reaction for its users.
    
Signed-off-by: Mohamed El Amine BOUKERFA <boukerfa.ma@gmail.com>
This commit is contained in:
Amine BOUKERFA
2026-06-11 13:25:12 +00:00
committed by GitHub
parent 6ce853312f
commit 5aa1402aa3
3 changed files with 82 additions and 1 deletions
+1
View File
@@ -21,6 +21,7 @@ and this project adheres to
### Fixed
- 🐛(frontend) overlap of block menu dropdown #2406
- ⚡️(backend) fix N+1 queries when serializing thread comments #2415
## [v5.2.1] - 2026-06-05
+10 -1
View File
@@ -3168,7 +3168,16 @@ class ThreadViewSet(
permission_classes = [permissions.CommentPermission]
pagination_class = None
serializer_class = serializers.ThreadSerializer
queryset = models.Thread.objects.select_related("creator", "document")
queryset = models.Thread.objects.select_related(
"creator", "document"
).prefetch_related(
db.Prefetch(
"comments",
queryset=models.Comment.objects.select_related("user").prefetch_related(
"reactions__users"
),
),
)
resource_field_name = "document"
def perform_create(self, serializer):
@@ -512,6 +512,42 @@ def test_api_documents_threads_list_restricted_document_editor(role):
assert len(response.json()) == 3
@pytest.mark.parametrize("nb_threads", [1, 3])
def test_api_documents_threads_list_number_of_queries(
nb_threads, django_assert_num_queries
):
"""
Listing threads should run a constant number of queries whatever the number
of threads, comments, reactions and reaction users.
"""
document = factories.DocumentFactory(
link_reach="public",
link_role=models.LinkRoleChoices.COMMENTER,
)
for _ in range(nb_threads):
thread = factories.ThreadFactory(document=document)
for _ in range(2):
comment = factories.CommentFactory(thread=thread)
for emoji in ["👍", "🎉"]:
factories.ReactionFactory(
comment=comment,
emoji=emoji,
users=factories.UserFactory.create_batch(2),
)
client = APIClient()
# 1 query for the document (permission check), 1 for the threads and 1 per
# prefetched relation: comments, reactions and reaction users.
with django_assert_num_queries(5):
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/",
)
assert response.status_code == 200
assert len(response.json()) == nb_threads
# Retrieve
@@ -857,6 +893,41 @@ def test_api_documents_threads_retrieve_restricted_document_privileged_roles(rol
}
@pytest.mark.parametrize("nb_comments", [1, 3])
def test_api_documents_threads_retrieve_number_of_queries(
nb_comments, django_assert_num_queries
):
"""
Retrieving a thread should run a constant number of queries whatever the
number of comments, reactions and reaction users.
"""
document = factories.DocumentFactory(
link_reach="public",
link_role=models.LinkRoleChoices.COMMENTER,
)
thread = factories.ThreadFactory(document=document)
for _ in range(nb_comments):
comment = factories.CommentFactory(thread=thread)
for emoji in ["👍", "🎉"]:
factories.ReactionFactory(
comment=comment,
emoji=emoji,
users=factories.UserFactory.create_batch(2),
)
client = APIClient()
# 1 query for the thread and 1 per prefetched relation: comments, reactions
# and reaction users.
with django_assert_num_queries(4):
response = client.get(
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/",
)
assert response.status_code == 200
assert len(response.json()["comments"]) == nb_comments
# Destroy