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.
This commit is contained in:
None 2026-05-02 16:48:37 -05:00
parent ad83d968dc
commit df7e6422df
5 changed files with 15 additions and 12 deletions

View file

@ -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;

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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) => {