Skip to content

Commit

Permalink
perf: fix N+1 in program serializer that is impacting pathways serial…
Browse files Browse the repository at this point in the history
…izer (#4276)

* perf: fix N+1 in program serializer that is impacting pathways serializer
  • Loading branch information
DawoudSheraz authored Feb 23, 2024
1 parent a61c952 commit 1852366
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 20 deletions.
19 changes: 13 additions & 6 deletions course_discovery/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1893,7 +1893,7 @@ class DegreeSerializer(BaseModelSerializer):
micromasters_background_image = StdImageSerializerField()
micromasters_path = serializers.SerializerMethodField()
additional_metadata = DegreeAdditionalMetadataSerializer(required=False)
specializations = serializers.SerializerMethodField()
specializations = serializers.StringRelatedField(many=True)

class Meta:
model = Degree
Expand All @@ -1916,9 +1916,6 @@ def get_micromasters_path(self, degree):
else:
return degree.micromasters_url

def get_specializations(self, degree):
return list(degree.specializations.values_list('value', flat=True))


class ProgramSubscriptionPriceSerializer(BaseModelSerializer):
"""
Expand Down Expand Up @@ -1991,17 +1988,27 @@ def prefetch_queryset(cls, partner, queryset=None):
# Explicitly check if the queryset is None before selecting related
queryset = queryset if queryset is not None else Program.objects.filter(partner=partner)

return queryset.select_related('type', 'partner').prefetch_related(
return queryset.select_related(
'type', 'partner', 'degree', 'language_override', 'level_type_override', 'primary_subject_override',
'degree__additional_metadata'
).prefetch_related(
'excluded_course_runs',
# `type` is serialized by a third-party serializer. Providing this field name allows us to
# prefetch `applicable_seat_types`, a m2m on `ProgramType`, through `type`, a foreign key to
# `ProgramType` on `Program`.
'type__applicable_seat_types',
'type__translations',
'authoring_organizations',
'degree',
'degree__costs',
'degree__deadlines',
'curricula',
'subscription__prices__currency',
'primary_subject_override__translations',
'level_type_override__translations',
'degree__specializations',
'degree__rankings',
'degree__quick_facts',
'labels',
Prefetch('courses', queryset=MinimalProgramCourseSerializer.prefetch_queryset()),
)

Expand Down
28 changes: 14 additions & 14 deletions course_discovery/apps/api/v1/tests/test_views/test_programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def test_list(self):
""" Verify the endpoint returns a list of all programs. """
expected = [self.create_program() for __ in range(3)]

self.assert_list_results(self.list_path, expected, 41)
self.assert_list_results(self.list_path, expected, 26)

def test_extended_query_param_fields(self):
""" Verify that the `extended` query param will result in an extended amount of fields returned. """
Expand Down Expand Up @@ -258,7 +258,7 @@ def test_filter_by_type(self):
program_type_name = 'foo'
program = ProgramFactory(type__name_t=program_type_name, partner=self.partner)
url = self.list_path + '?type=' + program_type_name
self.assert_list_results(url, [program], 21)
self.assert_list_results(url, [program], 17)

url = self.list_path + '?type=bar'
self.assert_list_results(url, [], 5)
Expand All @@ -272,7 +272,7 @@ def test_filter_by_types(self):
# Create a third program, which should be filtered out.
ProgramFactory(partner=self.partner)

self.assert_list_results(url, expected, 27)
self.assert_list_results(url, expected, 18)

def test_filter_by_timestamp(self):
"""
Expand Down Expand Up @@ -311,13 +311,13 @@ def test_filter_by_uuids(self):
# Create a third program, which should be filtered out.
ProgramFactory(partner=self.partner)

self.assert_list_results(url, expected, 27)
self.assert_list_results(url, expected, 18)

@pytest.mark.parametrize(
'status,is_marketable,expected_query_count',
(
(ProgramStatus.Unpublished, False, 5),
(ProgramStatus.Active, True, 34),
(ProgramStatus.Active, True, 19),
)
)
def test_filter_by_marketable(self, status, is_marketable, expected_query_count):
Expand All @@ -336,30 +336,30 @@ def test_filter_by_status(self):
retired = ProgramFactory(status=ProgramStatus.Retired, partner=self.partner)

url = self.list_path + '?status=active'
self.assert_list_results(url, [active], 21)
self.assert_list_results(url, [active], 17)

url = self.list_path + '?status=retired'
self.assert_list_results(url, [retired], 21)
self.assert_list_results(url, [retired], 17)

url = self.list_path + '?status=active&status=retired'
self.assert_list_results(url, [active, retired], 27)
self.assert_list_results(url, [active, retired], 18)

def test_filter_by_hidden(self):
""" Endpoint should filter programs by their hidden attribute value. """
hidden = ProgramFactory(hidden=True, partner=self.partner)
not_hidden = ProgramFactory(hidden=False, partner=self.partner)

url = self.list_path + '?hidden=True'
self.assert_list_results(url, [hidden], 21)
self.assert_list_results(url, [hidden], 17)

url = self.list_path + '?hidden=False'
self.assert_list_results(url, [not_hidden], 21)
self.assert_list_results(url, [not_hidden], 17)

url = self.list_path + '?hidden=1'
self.assert_list_results(url, [hidden], 21)
self.assert_list_results(url, [hidden], 17)

url = self.list_path + '?hidden=0'
self.assert_list_results(url, [not_hidden], 21)
self.assert_list_results(url, [not_hidden], 17)

def test_filter_by_marketing_slug(self):
""" The endpoint should support filtering programs by marketing slug. """
Expand All @@ -375,13 +375,13 @@ def test_filter_by_marketing_slug(self):
program.marketing_slug = SLUG
program.save()

self.assert_list_results(url, [program], 28)
self.assert_list_results(url, [program], 24)

def test_list_exclude_utm(self):
""" Verify the endpoint returns marketing URLs without UTM parameters. """
url = self.list_path + '?exclude_utm=1'
program = self.create_program()
self.assert_list_results(url, [program], 27, extra_context={'exclude_utm': 1})
self.assert_list_results(url, [program], 23, extra_context={'exclude_utm': 1})

def test_minimal_serializer_use(self):
""" Verify that the list view uses the minimal serializer. """
Expand Down

0 comments on commit 1852366

Please sign in to comment.