Skip to content

Commit 84681f4

Browse files
committed
refactor(be): filter and paginate entities list in a subquery
Joins and selecting multiple fields for both counting and paginating caused MySQL to count rows one by one or ignore indexes. This commit changes the paginated query to filter, order and paginate in a subquery that selects only the entity ID; and joins the subquery with the original query.
1 parent c9fb4ed commit 84681f4

2 files changed

Lines changed: 56 additions & 17 deletions

File tree

common/entity_services/helpers/list_rules.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
from marshmallow import EXCLUDE, Schema
1010
from marshmallow.fields import Enum, Int
11-
from peewee import Field, Ordering, Select
11+
from peewee import JOIN, Field, Ordering, Select
1212
from werkzeug.datastructures import MultiDict
1313

14+
from common.entities import BaseEntity
15+
1416
DEFAULT_PAGE = 1
1517
DEFAULT_COUNT = 10
1618
T = TypeVar("T")
@@ -50,9 +52,25 @@ def __len__(self) -> int:
5052

5153
@classmethod
5254
def get_paginated_results(cls, query: Select, order_by: Field, list_rules: ListRules) -> Page[T]:
55+
model: BaseEntity = query.model
5356
ordering = list_rules.order_by_field(order_by)
54-
paginated_query = query.order_by(ordering).paginate(list_rules.page, list_rules.count)
55-
return cls(results=list(paginated_query), total=query.count())
57+
58+
paginated_subquery: Select = (
59+
model.select(model.id)
60+
.where(query._where)
61+
.order_by(ordering)
62+
.paginate(list_rules.page, list_rules.count)
63+
.alias("results")
64+
)
65+
66+
results_query = query.clone()
67+
results_query._where = None
68+
results_query = (
69+
results_query.join(paginated_subquery, join_type=JOIN.INNER, on=(model.id == paginated_subquery.c.id))
70+
.order_by(ordering)
71+
)
72+
73+
return cls(results=list(results_query), total=paginated_subquery.count(clear_limit=True))
5674

5775

5876
@dataclass

common/tests/unit/entity_services/helpers/test_list_rules.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest.mock import Mock
1+
from unittest.mock import ANY, Mock, MagicMock
22

33
import pytest
44
from werkzeug.datastructures import MultiDict
@@ -20,21 +20,29 @@ def mocked_entity_class():
2020

2121
@pytest.fixture()
2222
def mocked_query():
23-
# .paginate() (and other query methods in general) doesn't return a list until iterated on, but this is good enough for the test
24-
paginate = Mock(return_value=list())
2523
query = Mock()
26-
query.paginate = paginate
27-
query.order_by = Mock(return_value=query)
24+
subquery = Mock()
25+
results_query = MagicMock()
26+
27+
query.model.select.return_value = subquery
28+
query.clone = Mock(return_value=results_query)
29+
30+
subquery.where.return_value = subquery
31+
subquery.order_by.return_value = subquery
32+
subquery.paginate.return_value = subquery
33+
subquery.alias.return_value = subquery
34+
35+
results_query.join.return_value = results_query
36+
results_query.order_by.return_value = results_query
37+
2838
yield query
2939

3040

3141
@pytest.fixture()
32-
def mocked_query_with_data():
33-
# .paginate() (and other query methods in general) doesn't return a list until iterated on, but this is good enough for the test
34-
paginate = Mock(return_value=[Mock(), Mock()])
35-
query = Mock()
36-
query.paginate = paginate
37-
query.order_by = Mock(return_value=query)
42+
def mocked_query_with_data(mocked_query):
43+
query = mocked_query
44+
results_query = query.clone.return_value
45+
results_query.__iter__.return_value = [Mock(), Mock()]
3846
yield query
3947

4048

@@ -80,11 +88,24 @@ def test_from_params_extras():
8088

8189
@pytest.mark.unit
8290
def test_page_get_paginated_results(mocked_field, mocked_entity_class, mocked_query):
91+
mocked_subquery = mocked_query.model.select.return_value
92+
mocked_results_query = mocked_query.clone.return_value
8393
rules = ListRules.from_params(MultiDict())
94+
8495
result = Page[mocked_entity_class].get_paginated_results(mocked_query, mocked_field, rules)
96+
8597
mocked_field.asc.assert_called_once()
86-
mocked_query.paginate.assert_called_once()
87-
mocked_query.count.assert_called_once()
98+
99+
mocked_subquery.where.assert_called_once_with(mocked_query._where)
100+
mocked_subquery.order_by.assert_called_once_with(rules.order_by_field(mocked_field))
101+
mocked_subquery.paginate.assert_called_once_with(rules.page, rules.count)
102+
mocked_subquery.alias.assert_called_once()
103+
mocked_subquery.count.assert_called_once_with(clear_limit=True)
104+
105+
assert mocked_results_query._where is None
106+
mocked_results_query.join.assert_called_once_with(mocked_subquery, join_type=ANY, on=ANY)
107+
mocked_results_query.order_by.assert_called_once_with(rules.order_by_field(mocked_field))
108+
88109
assert type(result) == Page
89110

90111

@@ -102,6 +123,6 @@ def test_page_len(mocked_field, mocked_entity_class, mocked_query_with_data):
102123
def test_page_iter(mocked_field, mocked_entity_class, mocked_query_with_data):
103124
rules = ListRules.from_params(MultiDict())
104125
result = Page[mocked_entity_class].get_paginated_results(mocked_query_with_data, mocked_field, rules)
105-
count = len(mocked_query_with_data.paginate())
126+
count = len(list(mocked_query_with_data.clone.return_value))
106127
r = [r for r in result]
107128
assert len(r) == count

0 commit comments

Comments
 (0)