From 71620d323f42c99367efe4a192c70819107868a7 Mon Sep 17 00:00:00 2001 From: d0cd <23022326+d0cd@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:46:53 +0000 Subject: [PATCH] perf: eliminate N+1 queries in IssueSerializer and dedupe cycle conflict check - IssueSerializer.to_representation now reads assignees/labels from the existing prefetched M2M cache instead of issuing fresh IssueAssignee / IssueLabel filter queries per instance. The view's get_queryset already prefetches both relations, so every issue serialized in a list endpoint was wasting up to 4 extra queries. - CycleListCreate.post collapses the duplicate "exists check then refetch" pair into a single .first() lookup. Run-on: Niteshift Staging Co-Authored-By: Claude Opus 4.7 --- apps/api/plane/api/serializers/issue.py | 25 ++++++------------------ apps/api/plane/api/views/cycle.py | 26 +++++++++---------------- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/apps/api/plane/api/serializers/issue.py b/apps/api/plane/api/serializers/issue.py index ba9ddae301d..430706f8cf8 100644 --- a/apps/api/plane/api/serializers/issue.py +++ b/apps/api/plane/api/serializers/issue.py @@ -289,32 +289,19 @@ def update(self, instance, validated_data): def to_representation(self, instance): data = super().to_representation(instance) if "assignees" in self.fields: + assignees = instance.assignees.all() if "assignees" in self.expand: from .user import UserLiteSerializer - data["assignees"] = UserLiteSerializer( - User.objects.filter( - pk__in=IssueAssignee.objects.filter(issue=instance).values_list("assignee_id", flat=True) - ), - many=True, - ).data + data["assignees"] = UserLiteSerializer(assignees, many=True).data else: - data["assignees"] = [ - str(assignee) - for assignee in IssueAssignee.objects.filter(issue=instance).values_list("assignee_id", flat=True) - ] + data["assignees"] = [str(assignee.id) for assignee in assignees] if "labels" in self.fields: + labels = instance.labels.all() if "labels" in self.expand: - data["labels"] = LabelSerializer( - Label.objects.filter( - pk__in=IssueLabel.objects.filter(issue=instance).values_list("label_id", flat=True) - ), - many=True, - ).data + data["labels"] = LabelSerializer(labels, many=True).data else: - data["labels"] = [ - str(label) for label in IssueLabel.objects.filter(issue=instance).values_list("label_id", flat=True) - ] + data["labels"] = [str(label.id) for label in labels] return data diff --git a/apps/api/plane/api/views/cycle.py b/apps/api/plane/api/views/cycle.py index 30b04ed46dd..06f102acd62 100644 --- a/apps/api/plane/api/views/cycle.py +++ b/apps/api/plane/api/views/cycle.py @@ -307,29 +307,21 @@ def post(self, request, slug, project_id): ): serializer = CycleCreateSerializer(data=request.data, context={"request": request}) if serializer.is_valid(): - if ( - request.data.get("external_id") - and request.data.get("external_source") - and Cycle.objects.filter( - project_id=project_id, - workspace__slug=slug, - external_source=request.data.get("external_source"), - external_id=request.data.get("external_id"), - ).exists() - ): + if request.data.get("external_id") and request.data.get("external_source"): cycle = Cycle.objects.filter( workspace__slug=slug, project_id=project_id, external_source=request.data.get("external_source"), external_id=request.data.get("external_id"), ).first() - return Response( - { - "error": "Cycle with the same external id and external source already exists", - "id": str(cycle.id), - }, - status=status.HTTP_409_CONFLICT, - ) + if cycle: + return Response( + { + "error": "Cycle with the same external id and external source already exists", + "id": str(cycle.id), + }, + status=status.HTTP_409_CONFLICT, + ) serializer.save(project_id=project_id) # Send the model activity model_activity.delay(