[ADD] models: add _read_group method to avoid sorting on the names of many2one

The public read_group method is used by back-end code as well as RPC calls to receive grouped data.
 Most of the time, if the data isn't shown to the user, it doesn't need to be sorted on the names of
 the many2one, and can just be sorted on their ID instead, avoiding extra joins in the resulting queries

 This is a first commit that will reduce the features of _read_group to the strict minimum necessary
 to obtain correct grouped data that is never shown to a user

Task-id: 2479334
Part-of: odoo/odoo#84908
This commit is contained in:
Vincent Schippefilt
2022-03-02 17:10:47 +00:00
committed by Raphael Collet
parent a5e21abdf5
commit 4ef0c00b4b
2 changed files with 137 additions and 6 deletions
+109 -2
View File
@@ -196,7 +196,7 @@ class TestBase(TransactionCase):
self.assertEqual(p1.street, p1street, 'Address fields must not be synced after turning sync off')
self.assertNotEqual(ghoststep.street, p1street, 'Parent address must never be touched')
# turn on sync again
# turn on sync again
p1.write({'type': 'contact'})
self.assertEqual(p1.street, ghoststep.street, 'Address fields must be synced again')
self.assertEqual(p1.phone, p1phone, 'Phone should be preserved after address sync')
@@ -411,7 +411,7 @@ class TestBase(TransactionCase):
title_lady = self.env['res.partner.title'].create({'name': 'Lady...'})
user_vals_list = [
{'name': 'Alice', 'login': 'alice', 'color': 1, 'function': 'Friend', 'date': '2015-03-28', 'title': title_lady.id},
{'name': 'Alice', 'login': 'alice2', 'color': 0, 'function': 'Friend', 'date': '2015-01-28', 'title': title_lady.id},
{'name': 'Alice', 'login': 'alice2', 'color': 0, 'function': 'Friend', 'date': '2015-01-28', 'title': title_lady.id},
{'name': 'Bob', 'login': 'bob', 'color': 2, 'function': 'Friend', 'date': '2015-03-02', 'title': title_sir.id},
{'name': 'Eve', 'login': 'eve', 'color': 3, 'function': 'Eavesdropper', 'date': '2015-03-20', 'title': title_lady.id},
{'name': 'Nab', 'login': 'nab', 'color': -3, 'function': '5$ Wrench', 'date': '2014-09-10', 'title': title_sir.id},
@@ -507,6 +507,113 @@ class TestBase(TransactionCase):
self.assertEqual([g['title_count'] for g in groups_data], [2, 4], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [-1, 10], 'Incorrect aggregation of int column')
def test_61_private_read_group(self):
"""
the _read_group should behave exactly like read_group (public method) except for sorting the one2many on ID
instead of name, so avoiding the join on the "to many" table to get the name
"""
title_sir = self.env['res.partner.title'].create({'name': 'Sir...'})
title_lady = self.env['res.partner.title'].create({'name': 'Lady...'})
user_vals_list = [
{'name': 'Alice', 'login': 'alice', 'color': 1, 'function': 'Friend', 'date': '2015-03-28', 'title': title_lady.id},
{'name': 'Alice', 'login': 'alice2', 'color': 0, 'function': 'Friend', 'date': '2015-01-28', 'title': title_lady.id},
{'name': 'Bob', 'login': 'bob', 'color': 2, 'function': 'Friend', 'date': '2015-03-02', 'title': title_sir.id},
{'name': 'Eve', 'login': 'eve', 'color': 3, 'function': 'Eavesdropper', 'date': '2015-03-20', 'title': title_lady.id},
{'name': 'Nab', 'login': 'nab', 'color': -3, 'function': '5$ Wrench', 'date': '2014-09-10', 'title': title_sir.id},
{'name': 'Nab', 'login': 'nab-she', 'color': 6, 'function': '5$ Wrench', 'date': '2014-01-02', 'title': title_lady.id},
]
res_users = self.env['res.users']
users = res_users.create(user_vals_list)
domain = [('id', 'in', users.ids)]
# group on local char field without domain and without active_test (-> empty WHERE clause)
groups_data = res_users.with_context(active_test=False)._read_group([], fields=['login'], groupby=['login'], orderby='login DESC')
self.assertGreater(len(groups_data), 6, "Incorrect number of results when grouping on a field")
# group on local char field with limit
groups_data = res_users._read_group(domain, fields=['login'], groupby=['login'], orderby='login DESC', limit=3, offset=3)
self.assertEqual(len(groups_data), 3, "Incorrect number of results when grouping on a field with limit")
self.assertEqual(['bob', 'alice2', 'alice'], [g['login'] for g in groups_data], 'Result mismatch')
# group on inherited char field, aggregate on int field (second groupby ignored on purpose)
groups_data = res_users._read_group(domain, fields=['name', 'color', 'function'], groupby=['function', 'login'])
self.assertEqual(len(groups_data), 3, "Incorrect number of results when grouping on a field")
self.assertEqual([g['function'] for g in groups_data], ['5$ Wrench', 'Eavesdropper', 'Friend'], 'incorrect _read_group order')
for group_data in groups_data:
self.assertIn('color', group_data, "Aggregated data for the column 'color' is not present in _read_group return values")
self.assertEqual(group_data['color'], 3, "Incorrect sum for aggregated data for the column 'color'")
# group on inherited char field, reverse order
groups_data = res_users._read_group(domain, fields=['name', 'color'], groupby='name', orderby='name DESC')
self.assertEqual([g['name'] for g in groups_data], ['Nab', 'Eve', 'Bob', 'Alice'], 'Incorrect ordering of the list')
# group on int field, default ordering
groups_data = res_users._read_group(domain, fields=['color'], groupby='color')
self.assertEqual([g['color'] for g in groups_data], [-3, 0, 1, 2, 3, 6], 'Incorrect ordering of the list')
# multi group, second level is int field, should still be summed in first level grouping
groups_data = res_users._read_group(domain, fields=['name', 'color'], groupby=['name', 'color'], orderby='name DESC')
self.assertEqual([g['name'] for g in groups_data], ['Nab', 'Eve', 'Bob', 'Alice'], 'Incorrect ordering of the list')
self.assertEqual([g['color'] for g in groups_data], [3, 3, 2, 1], 'Incorrect ordering of the list')
# group on inherited char field, multiple orders with directions
groups_data = res_users._read_group(domain, fields=['name', 'color'], groupby='name', orderby='color DESC, name')
self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field")
self.assertEqual([g['name'] for g in groups_data], ['Eve', 'Nab', 'Bob', 'Alice'], 'Incorrect ordering of the list')
self.assertEqual([g['name_count'] for g in groups_data], [1, 2, 1, 2], 'Incorrect number of results')
# group on inherited date column (res_partner.date) -> Year-Month, default ordering
groups_data = res_users._read_group(domain, fields=['function', 'color', 'date'], groupby=['date'])
self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field")
self.assertEqual([g['date'] for g in groups_data], ['January 2014', 'September 2014', 'January 2015', 'March 2015'], 'Incorrect ordering of the list')
self.assertEqual([g['date_count'] for g in groups_data], [1, 1, 1, 3], 'Incorrect number of results')
# group on inherited date column (res_partner.date) specifying the :year -> Year default ordering
groups_data = res_users._read_group(domain, fields=['function', 'color', 'date'], groupby=['date:year'])
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
self.assertEqual([g['date:year'] for g in groups_data], ['2014', '2015'], 'Incorrect ordering of the list')
self.assertEqual([g['date_count'] for g in groups_data], [2, 4], 'Incorrect number of results')
# group on inherited date column (res_partner.date) -> Year-Month, custom order
groups_data = res_users._read_group(domain, fields=['function', 'color', 'date'], groupby=['date'], orderby='date DESC')
self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field")
self.assertEqual([g['date'] for g in groups_data], ['March 2015', 'January 2015', 'September 2014', 'January 2014'], 'Incorrect ordering of the list')
self.assertEqual([g['date_count'] for g in groups_data], [3, 1, 1, 1], 'Incorrect number of results')
# group on inherited many2one (res_partner.title), default order
groups_data = res_users._read_group(domain, fields=['function', 'color', 'title'], groupby=['title'])
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
# m2o is returned as a (id, label) pair
# here the order of the titles is by ID
self.assertEqual([g['title'] for g in groups_data], [(title_sir.id, 'Sir...'), (title_lady.id, 'Lady...')], 'Incorrect ordering of the list')
self.assertEqual([g['title_count'] for g in groups_data], [2, 4], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [-1, 10], 'Incorrect aggregation of int column')
# group on inherited many2one (res_partner.title), reversed natural order
groups_data = res_users._read_group(domain, fields=['function', 'color', 'title'], groupby=['title'], orderby="title desc")
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
# m2o is returned as a (id, label) pair
# here the order of the titles is by ID DESC
self.assertEqual([g['title'] for g in groups_data], [(title_sir.id, 'Sir...'), (title_lady.id, 'Lady...')], 'Incorrect ordering of the list')
self.assertEqual([g['title_count'] for g in groups_data], [2, 4], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [-1, 10], 'Incorrect aggregation of int column')
# group on inherited many2one (res_partner.title), multiple orders with m2o in second position
groups_data = res_users._read_group(domain, fields=['function', 'color', 'title'], groupby=['title'], orderby="color desc, title desc")
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
# m2o is returned as a (id, label) pair
self.assertEqual([g['title'] for g in groups_data], [(title_lady.id, 'Lady...'), (title_sir.id, 'Sir...')], 'Incorrect ordering of the list')
self.assertEqual([g['title_count'] for g in groups_data], [4, 2], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [10, -1], 'Incorrect aggregation of int column')
# group on inherited many2one (res_partner.title), ordered by other inherited field (color)
groups_data = res_users._read_group(domain, fields=['function', 'color', 'title'], groupby=['title'], orderby='color')
self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field")
# m2o is returned as a (id, label) pair
self.assertEqual([g['title'] for g in groups_data], [(title_sir.id, 'Sir...'), (title_lady.id, 'Lady...')], 'Incorrect ordering of the list')
self.assertEqual([g['title_count'] for g in groups_data], [2, 4], 'Incorrect number of results')
self.assertEqual([g['color'] for g in groups_data], [-1, 10], 'Incorrect aggregation of int column')
def test_70_archive_internal_partners(self):
test_partner = self.env['res.partner'].create({'name':'test partner'})
test_user = self.env['res.users'].create({
+28 -4
View File
@@ -68,7 +68,7 @@ from .tools.lru import LRU
_logger = logging.getLogger(__name__)
_unlink = logging.getLogger(__name__ + '.unlink')
regex_order = re.compile(r'^(\s*([a-z0-9:_]+|"[a-z0-9:_]+")(\s+(desc|asc))?\s*(,|$))+(?<!,)$', re.I)
regex_order = re.compile(r'^(\s*([a-z0-9:_]+|"[a-z0-9:_]+")(\.id)?(\s+(desc|asc))?\s*(,|$))+(?<!,)$', re.I)
regex_object_name = re.compile(r'^[a-z0-9_.]+$')
regex_pg_name = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I)
regex_field_agg = re.compile(r'(\w+)(?::(\w+)(?:\((\w+)\))?)?')
@@ -2246,11 +2246,16 @@ class BaseModel(metaclass=MetaModel):
for key in ('field', 'groupby')
}
for order_part in orderby.split(','):
order_split = order_part.split()
order_split = order_part.split() # potentially ["field:group_func", "desc"]
order_field = order_split[0]
is_many2one_id = order_field.endswith(".id")
if is_many2one_id:
order_field = order_field[:-3]
if order_field == 'id' or order_field in groupby_fields:
if self._fields[order_field.split(':')[0]].type == 'many2one':
order_clause = self._generate_order_by(order_part, query).replace('ORDER BY ', '')
order_field_name = order_field.split(':')[0]
if self._fields[order_field_name].type == 'many2one' and not is_many2one_id:
order_clause = self._generate_order_by(order_part, query)
order_clause = order_clause.replace('ORDER BY ', '')
if order_clause:
orderby_terms.append(order_clause)
groupby_terms += [order_term.split()[0] for order_term in order_clause.split(',')]
@@ -2413,6 +2418,25 @@ class BaseModel(metaclass=MetaModel):
del data['id']
return data
@api.model
def _read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
"""
Executes exactly what the public read_group() does, except it doesn't
order many2one fields on their comodel's order but on their ID instead.
"""
if not orderby:
if isinstance(groupby, str):
groupby = [groupby]
groupby_list = groupby[:1] if lazy else groupby
order_list = []
for order_spec in groupby_list:
field_name = order_spec.split(":")[0] # field name could be formatted like "field:group_func"
if self._fields[field_name].type == 'many2one':
order_spec = f"{field_name}.id" # do not order by comodel's order
order_list.append(order_spec)
orderby = ','.join(order_list)
return self.read_group(domain, fields, groupby, offset, limit, orderby, lazy)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
"""Get the list of records in list view grouped by the given ``groupby`` fields.