From 460677aeea84f6c8c9b25ea4f30c09cbeca16d0d Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 18 Jun 2026 15:24:23 -0500 Subject: [PATCH] refactor(channels): optimize stream retrieval in ChannelSerializer and add tests for query efficiency - Updated `get_streams` method in `ChannelSerializer` to use prefetched `channelstream_set`, improving performance by reducing database queries. - Added `ChannelListIncludeStreamsQueryTests` to ensure that the number of queries remains stable as channels grow, verifying efficient stream inclusion in API responses. --- apps/channels/serializers.py | 11 ++-- apps/channels/tests/test_channel_api.py | 69 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index 2e80d840..61bbef8d 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -551,10 +551,13 @@ class ChannelSerializer(serializers.ModelSerializer): return LogoSerializer(obj.logo).data def get_streams(self, obj): - """Retrieve ordered stream IDs for GET requests.""" - return StreamSerializer( - obj.streams.all().order_by("channelstream__order"), many=True - ).data + """Retrieve ordered streams for GET requests using prefetched channelstream_set.""" + ordered_streams = [ + cs.stream + for cs in obj.channelstream_set.all() + if cs.stream_id is not None + ] + return StreamSerializer(ordered_streams, many=True).data def create(self, validated_data): streams = validated_data.pop("streams", []) diff --git a/apps/channels/tests/test_channel_api.py b/apps/channels/tests/test_channel_api.py index 363f5925..5d60a8da 100644 --- a/apps/channels/tests/test_channel_api.py +++ b/apps/channels/tests/test_channel_api.py @@ -504,3 +504,72 @@ class SeriesRuleAPITests(TestCase): def test_bulk_remove_requires_tvg_id_or_title(self): resp = self.client.post(self.bulk_remove_url, {}, format="json") self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + + +class ChannelListIncludeStreamsQueryTests(TestCase): + """include_streams=true must not issue one stream query per channel.""" + + def setUp(self): + from apps.channels.models import ChannelStream, Stream + from apps.m3u.models import M3UAccount + + self.user = User.objects.create_user(username="list_admin", password="x") + self.user.user_level = 10 + self.user.save() + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + self.account = M3UAccount.objects.create( + name="list-test-account", + account_type="XC", + username="user", + password="pass", + ) + self.group = ChannelGroup.objects.create(name=f"List Group {self.id}") + + def _add_channel_with_stream(self, number): + from apps.channels.models import ChannelStream, Stream + + channel = Channel.objects.create( + channel_number=float(number), + name=f"Channel {number}", + channel_group=self.group, + ) + stream = Stream.objects.create( + name=f"Stream {number}", + url=f"http://example.com/{number}.ts", + m3u_account=self.account, + ) + ChannelStream.objects.create(channel=channel, stream=stream, order=0) + return channel + + def _query_count_for_list(self): + from django.db import connection + from django.test.utils import CaptureQueriesContext + + with CaptureQueriesContext(connection) as ctx: + response = self.client.get( + "/api/channels/channels/", + {"page": 1, "page_size": 50, "include_streams": "true"}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + return len(ctx.captured_queries) + + def test_include_streams_query_count_stable_as_channels_grow(self): + self._add_channel_with_stream(1) + self._add_channel_with_stream(2) + self._add_channel_with_stream(3) + q_small = self._query_count_for_list() + + self._add_channel_with_stream(4) + self._add_channel_with_stream(5) + self._add_channel_with_stream(6) + self._add_channel_with_stream(7) + q_large = self._query_count_for_list() + + self.assertEqual( + q_small, + q_large, + "include_streams list should use prefetched channelstream_set, " + "not one streams M2M query per channel", + )