From df7e6422df755e9af2f52ed07909a1014e6d09b1 Mon Sep 17 00:00:00 2001 From: None <190783071+CodeBormen@users.noreply.github.com> Date: Sat, 2 May 2026 16:48:37 -0500 Subject: [PATCH] perf: eliminate three remaining N+1 queries in list endpoints Layered on top of the prior perf commits, removes additional per-row queries Channel list (apps/channels): add auto_created_by to select_related so ChannelSerializer.get_auto_created_by_name does not fire one SELECT per row. Replace instance.streams.all().order_by(channelstream__order) in to_representation with a read off the prefetched channelstream_set. M3U account list (apps/m3u): prefetch_related "filters" on the viewset queryset and sort filters in Python over the prefetch in M3UAccountSerializer.get_filters. Frontend (EditableCell.jsx): align the saveValue useCallback dependency from [row.original.id] to [row.original] in the Number, Group, EPG, and Logo cells, matching the pattern applied to the Text cell. --- apps/channels/api_views.py | 1 + apps/channels/serializers.py | 12 ++++++------ apps/m3u/api_views.py | 2 +- apps/m3u/serializers.py | 4 +++- .../components/tables/ChannelsTable/EditableCell.jsx | 8 ++++---- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index a7dd22b4..9c10e555 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -852,6 +852,7 @@ class ChannelViewSet(viewsets.ModelViewSet): "epg_data", "stream_profile", "override", + "auto_created_by", ).prefetch_related( "streams", # Default-attr prefetch shares the cache with M2M writes; diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index dcbea53e..2c7fee3e 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -532,14 +532,14 @@ class ChannelSerializer(serializers.ModelSerializer): self.fields["streams"] = serializers.SerializerMethodField() return super().to_representation(instance) else: - # Fix: For PATCH/PUT responses, ensure streams are ordered + # Read from the prefetched channelstream_set (ordered by the + # viewset's Prefetch); chaining .order_by() rebuilds the + # queryset and fires one SELECT per row in list responses. representation = super().to_representation(instance) if "streams" in representation: - representation["streams"] = list( - instance.streams.all() - .order_by("channelstream__order") - .values_list("id", flat=True) - ) + representation["streams"] = [ + cs.stream_id for cs in instance.channelstream_set.all() + ] return representation def get_logo(self, obj): diff --git a/apps/m3u/api_views.py b/apps/m3u/api_views.py index b371d551..c09b668f 100644 --- a/apps/m3u/api_views.py +++ b/apps/m3u/api_views.py @@ -44,7 +44,7 @@ class M3UAccountViewSet(viewsets.ModelViewSet): queryset = M3UAccount.objects.select_related( "refresh_task__crontab", "refresh_task__interval" - ).prefetch_related("channel_group", "profiles") + ).prefetch_related("channel_group", "profiles", "filters") serializer_class = M3UAccountSerializer def get_permissions(self): diff --git a/apps/m3u/serializers.py b/apps/m3u/serializers.py index 67134775..485a3687 100644 --- a/apps/m3u/serializers.py +++ b/apps/m3u/serializers.py @@ -370,7 +370,9 @@ class M3UAccountSerializer(serializers.ModelSerializer): return instance def get_filters(self, obj): - filters = obj.filters.order_by("order") + # Sort over the prefetch cache; .order_by() would fire one SELECT + # per account (viewset prefetches "filters"). + filters = sorted(obj.filters.all(), key=lambda f: f.order) return M3UFilterSerializer(filters, many=True).data def get_earliest_expiration(self, obj): diff --git a/frontend/src/components/tables/ChannelsTable/EditableCell.jsx b/frontend/src/components/tables/ChannelsTable/EditableCell.jsx index 4b7b552e..55bd44e3 100644 --- a/frontend/src/components/tables/ChannelsTable/EditableCell.jsx +++ b/frontend/src/components/tables/ChannelsTable/EditableCell.jsx @@ -320,7 +320,7 @@ const EditableNumberCellInner = ({ row, column, getValue, onBlur }) => { setValue(previousValue.current); } }, - [row.original.id, column.id, onBlur] + [row.original, column.id, onBlur] ); useEffect(() => { @@ -434,7 +434,7 @@ const EditableGroupCellInner = ({ notifyInlineSaveError(column.id, error); } }, - [row.original.id] + [row.original] ); const handleChange = (newGroupId) => { @@ -609,7 +609,7 @@ const EditableEPGCellInner = ({ notifyInlineSaveError(column.id, error); } }, - [row.original.id] + [row.original] ); const handleChange = (newEpgDataId) => { @@ -779,7 +779,7 @@ const EditableLogoCellInner = ({ row, logoId, onBlur }) => { notifyInlineSaveError(column.id, error); } }, - [row.original.id] + [row.original] ); const handleChange = (newLogoId) => {