Skip to content

Commit d037e75

Browse files
authored
perf: select related objects in container queries (#333)
1 parent b7ec7d9 commit d037e75

File tree

8 files changed

+126
-8
lines changed

8 files changed

+126
-8
lines changed

openedx_learning/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Open edX Learning ("Learning Core").
33
"""
44

5-
__version__ = "0.27.0"
5+
__version__ = "0.27.1"

openedx_learning/apps/authoring/publishing/api.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,7 @@ def get_entities_in_container(
12831283
container: Container,
12841284
*,
12851285
published: bool,
1286+
select_related_version: str | None = None,
12861287
) -> list[ContainerEntityListEntry]:
12871288
"""
12881289
[ 🛑 UNSTABLE ]
@@ -1293,14 +1294,35 @@ def get_entities_in_container(
12931294
container: The Container, e.g. returned by `get_container()`
12941295
published: `True` if we want the published version of the container, or
12951296
`False` for the draft version.
1297+
select_related_version: An optional optimization; specify a relationship
1298+
on ContainerVersion, like `componentversion` or `containerversion__x`
1299+
to preload via select_related.
12961300
"""
12971301
assert isinstance(container, Container)
1298-
container_version = container.versioning.published if published else container.versioning.draft
1302+
if published:
1303+
# Very minor optimization: reload the container with related 1:1 entities
1304+
container = Container.objects.select_related(
1305+
"publishable_entity__published__version__containerversion__entity_list").get(pk=container.pk)
1306+
container_version = container.versioning.published
1307+
select_related = ["entity__published__version"]
1308+
if select_related_version:
1309+
select_related.append(f"entity__published__version__{select_related_version}")
1310+
else:
1311+
# Very minor optimization: reload the container with related 1:1 entities
1312+
container = Container.objects.select_related(
1313+
"publishable_entity__draft__version__containerversion__entity_list").get(pk=container.pk)
1314+
container_version = container.versioning.draft
1315+
select_related = ["entity__draft__version"]
1316+
if select_related_version:
1317+
select_related.append(f"entity__draft__version__{select_related_version}")
12991318
if container_version is None:
13001319
raise ContainerVersion.DoesNotExist # This container has not been published yet, or has been deleted.
13011320
assert isinstance(container_version, ContainerVersion)
1302-
entity_list = []
1303-
for row in container_version.entity_list.entitylistrow_set.order_by("order_num"):
1321+
entity_list: list[ContainerEntityListEntry] = []
1322+
for row in container_version.entity_list.entitylistrow_set.select_related(
1323+
"entity_version",
1324+
*select_related,
1325+
).order_by("order_num"):
13041326
entity_version = row.entity_version # This will be set if pinned
13051327
if not entity_version: # If this entity is "unpinned", use the latest published/draft version:
13061328
entity_version = row.entity.published.version if published else row.entity.draft.version
@@ -1393,7 +1415,10 @@ def get_containers_with_entity(
13931415
qs = Container.objects.filter(
13941416
publishable_entity__draft__version__containerversion__entity_list__entitylistrow__entity_id=publishable_entity_pk, # pylint: disable=line-too-long # noqa: E501
13951417
)
1396-
return qs.order_by("pk").distinct() # Ordering is mostly for consistent test cases.
1418+
return qs.select_related(
1419+
"publishable_entity__draft__version__containerversion",
1420+
"publishable_entity__published__version__containerversion",
1421+
).order_by("pk").distinct() # Ordering is mostly for consistent test cases.
13971422

13981423

13991424
def get_container_children_count(

openedx_learning/apps/authoring/sections/api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,12 @@ def get_subsections_in_section(
257257
"""
258258
assert isinstance(section, Section)
259259
subsections = []
260-
for entry in publishing_api.get_entities_in_container(section, published=published):
260+
entries = publishing_api.get_entities_in_container(
261+
section,
262+
published=published,
263+
select_related_version="containerversion__subsectionversion",
264+
)
265+
for entry in entries:
261266
# Convert from generic PublishableEntityVersion to SubsectionVersion:
262267
subsection_version = entry.entity_version.containerversion.subsectionversion
263268
assert isinstance(subsection_version, SubsectionVersion)

openedx_learning/apps/authoring/subsections/api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,12 @@ def get_units_in_subsection(
256256
"""
257257
assert isinstance(subsection, Subsection)
258258
units = []
259-
for entry in publishing_api.get_entities_in_container(subsection, published=published):
259+
entries = publishing_api.get_entities_in_container(
260+
subsection,
261+
published=published,
262+
select_related_version="containerversion__unitversion",
263+
)
264+
for entry in entries:
260265
# Convert from generic PublishableEntityVersion to UnitVersion:
261266
unit_version = entry.entity_version.containerversion.unitversion
262267
assert isinstance(unit_version, UnitVersion)

openedx_learning/apps/authoring/units/api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,12 @@ def get_components_in_unit(
257257
"""
258258
assert isinstance(unit, Unit)
259259
components = []
260-
for entry in publishing_api.get_entities_in_container(unit, published=published):
260+
entries = publishing_api.get_entities_in_container(
261+
unit,
262+
published=published,
263+
select_related_version="componentversion",
264+
)
265+
for entry in entries:
261266
# Convert from generic PublishableEntityVersion to ComponentVersion:
262267
component_version = entry.entity_version.componentversion
263268
assert isinstance(component_version, ComponentVersion)

tests/openedx_learning/apps/authoring/sections/test_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,32 @@ def test_sections_containing(self):
993993
]
994994
assert result2 == [section4_unpinned]
995995

996+
def test_get_subsections_in_section_queries(self):
997+
"""
998+
Test the query count of get_subsections_in_section()
999+
This also tests the generic method get_entities_in_container()
1000+
"""
1001+
section = self.create_section_with_subsections([
1002+
self.subsection_1,
1003+
self.subsection_2,
1004+
self.subsection_2_v1,
1005+
])
1006+
with self.assertNumQueries(4):
1007+
result = authoring_api.get_subsections_in_section(section, published=False)
1008+
assert result == [
1009+
Entry(self.subsection_1.versioning.draft),
1010+
Entry(self.subsection_2.versioning.draft),
1011+
Entry(self.subsection_2.versioning.draft, pinned=True),
1012+
]
1013+
authoring_api.publish_all_drafts(self.learning_package.id)
1014+
with self.assertNumQueries(4):
1015+
result = authoring_api.get_subsections_in_section(section, published=True)
1016+
assert result == [
1017+
Entry(self.subsection_1.versioning.draft),
1018+
Entry(self.subsection_2.versioning.draft),
1019+
Entry(self.subsection_2.versioning.draft, pinned=True),
1020+
]
1021+
9961022
def test_add_remove_container_children(self):
9971023
"""
9981024
Test adding and removing children subsections from sections.

tests/openedx_learning/apps/authoring/subsections/test_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,32 @@ def test_subsections_containing(self):
10241024
]
10251025
assert result2 == [subsection4_unpinned]
10261026

1027+
def test_get_units_in_subsection_queries(self):
1028+
"""
1029+
Test the query count of get_units_in_subsection()
1030+
This also tests the generic method get_entities_in_container()
1031+
"""
1032+
subsection = self.create_subsection_with_units([
1033+
self.unit_1,
1034+
self.unit_2,
1035+
self.unit_2_v1,
1036+
])
1037+
with self.assertNumQueries(4):
1038+
result = authoring_api.get_units_in_subsection(subsection, published=False)
1039+
assert result == [
1040+
Entry(self.unit_1.versioning.draft),
1041+
Entry(self.unit_2.versioning.draft),
1042+
Entry(self.unit_2.versioning.draft, pinned=True),
1043+
]
1044+
authoring_api.publish_all_drafts(self.learning_package.id)
1045+
with self.assertNumQueries(4):
1046+
result = authoring_api.get_units_in_subsection(subsection, published=True)
1047+
assert result == [
1048+
Entry(self.unit_1.versioning.draft),
1049+
Entry(self.unit_2.versioning.draft),
1050+
Entry(self.unit_2.versioning.draft, pinned=True),
1051+
]
1052+
10271053
def test_add_remove_container_children(self):
10281054
"""
10291055
Test adding and removing children units from subsections.

tests/openedx_learning/apps/authoring/units/test_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,32 @@ def test_units_containing(self):
10121012
]
10131013
assert result2 == [unit4_unpinned, unit7_several]
10141014

1015+
def test_get_components_in_unit_queries(self):
1016+
"""
1017+
Test the query count of get_components_in_unit()
1018+
This also tests the generic method get_entities_in_container()
1019+
"""
1020+
unit = self.create_unit_with_components([
1021+
self.component_1,
1022+
self.component_2,
1023+
self.component_2_v1,
1024+
])
1025+
with self.assertNumQueries(3):
1026+
result = authoring_api.get_components_in_unit(unit, published=False)
1027+
assert result == [
1028+
Entry(self.component_1.versioning.draft),
1029+
Entry(self.component_2.versioning.draft),
1030+
Entry(self.component_2.versioning.draft, pinned=True),
1031+
]
1032+
authoring_api.publish_all_drafts(self.learning_package.id)
1033+
with self.assertNumQueries(3):
1034+
result = authoring_api.get_components_in_unit(unit, published=True)
1035+
assert result == [
1036+
Entry(self.component_1.versioning.draft),
1037+
Entry(self.component_2.versioning.draft),
1038+
Entry(self.component_2.versioning.draft, pinned=True),
1039+
]
1040+
10151041
def test_add_remove_container_children(self):
10161042
"""
10171043
Test adding and removing children components from containers.

0 commit comments

Comments
 (0)