summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2020-02-25 12:45:11 +0000
committerSébastien Theys <seb@odoo.com>2020-02-25 13:46:10 +0000
commit6bb0963b6adb588925be9789e9b380eb437a13d2 (patch)
treea9a345ffda88f5752fec9e2d8cdce85183c7cc86 /odoo
parent93f10cb6291d3855353e36771e06c08d04b4464f (diff)
[IMP] core: optimize read() by avoiding useless query
When the `select` returns nothing more than ids that are already known, there is no need to make it at all. This removes one query from `_read` every time it has to fetch only fields that are stored in a different table (o2m, ...), which happens all the time when reading a stored field first (triggering prefetch) and then reading a o2m. The query that is now removed was used to check access rules, but the trick is to use `check_access_rule` to verify the rules in python instead. Part of task-2061122 closes odoo/odoo#36263 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Diffstat (limited to 'odoo')
-rw-r--r--odoo/addons/base/tests/test_orm.py4
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py47
-rw-r--r--odoo/addons/test_performance/tests/test_performance.py26
-rw-r--r--odoo/models.py60
4 files changed, 92 insertions, 45 deletions
diff --git a/odoo/addons/base/tests/test_orm.py b/odoo/addons/base/tests/test_orm.py
index d4bf0ab8631..0cc2d4a1524 100644
--- a/odoo/addons/base/tests/test_orm.py
+++ b/odoo/addons/base/tests/test_orm.py
@@ -35,10 +35,6 @@ class TestORM(TransactionCase):
# Deleting an already deleted record should be simply ignored
self.assertTrue(p1.unlink(), "Re-deleting should be a no-op")
- # Updating an already deleted record should raise, even as admin
- with self.assertRaises(MissingError):
- p1.write({'name': 'foo'})
-
@mute_logger('odoo.models', 'odoo.addons.base.models.ir_rule')
def test_access_filtered_records(self):
""" Verify that accessing filtered records works as expected for non-admin user """
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index b98b1a34c0f..94b1dd9c04e 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -1714,6 +1714,53 @@ class TestFields(TransactionCaseWithUserDemo):
self.assertEqual(record_no_bin_size.binary_computed, expected_value)
self.assertEqual(record_bin_size.binary_computed, expected_value)
+ def test_96_ir_rule_m2m_field(self):
+ """Ensures m2m fields can't be read if the left records can't be read.
+ Also makes sure reading m2m doesn't take more queries than necessary."""
+ tag = self.env['test_new_api.multi.tag'].create({})
+ record = self.env['test_new_api.multi.line'].create({
+ 'name': 'image',
+ 'tags': [(4, tag.id)],
+ })
+
+ # only one query as admin: reading pivot table
+ with self.assertQueryCount(1):
+ record.read(['tags'])
+
+ user = self.env['res.users'].create({'name': "user", 'login': "user"})
+ record_user = record.with_user(user)
+
+ # prep the following query count by caching access check related data
+ record_user.read(['tags'])
+
+ # only one query as user: reading pivot table
+ with self.assertQueryCount(1):
+ record_user.read(['tags'])
+
+ # create a passing ir.rule
+ self.env['ir.rule'].create({
+ 'model_id': self.env['ir.model']._get(record._name).id,
+ 'domain_force': "[('id', '=', %d)]" % record.id,
+ })
+
+ # prep the following query count by caching access check related data
+ record_user.read(['tags'])
+
+ # still only 1 query: reading pivot table
+ # access rules are checked in python in this case
+ with self.assertQueryCount(1):
+ record_user.read(['tags'])
+
+ # create a blocking ir.rule
+ self.env['ir.rule'].create({
+ 'model_id': self.env['ir.model']._get(record._name).id,
+ 'domain_force': "[('id', '!=', %d)]" % record.id,
+ })
+
+ # ensure ir.rule is applied even when reading m2m
+ with self.assertRaises(AccessError):
+ record_user.read(['tags'])
+
class TestX2many(common.TransactionCase):
def test_definition_many2many(self):
diff --git a/odoo/addons/test_performance/tests/test_performance.py b/odoo/addons/test_performance/tests/test_performance.py
index 1dcc7c98837..014cfa60aca 100644
--- a/odoo/addons/test_performance/tests/test_performance.py
+++ b/odoo/addons/test_performance/tests/test_performance.py
@@ -99,7 +99,7 @@ class TestPerformance(SavepointCaseWithUserDemo):
self.assertEqual(len(rec1.line_ids), 1)
rec1.invalidate_cache()
- with self.assertQueryCount(16):
+ with self.assertQueryCount(15):
rec1.write({'line_ids': [(0, 0, {'value': val}) for val in range(1, 12)]})
self.assertEqual(len(rec1.line_ids), 12)
@@ -107,23 +107,23 @@ class TestPerformance(SavepointCaseWithUserDemo):
# update N lines: O(N) queries
rec1.invalidate_cache()
- with self.assertQueryCount(7):
+ with self.assertQueryCount(6):
rec1.write({'line_ids': [(1, line.id, {'value': 42}) for line in lines[0]]})
self.assertEqual(rec1.line_ids, lines)
rec1.invalidate_cache()
- with self.assertQueryCount(27):
+ with self.assertQueryCount(26):
rec1.write({'line_ids': [(1, line.id, {'value': 42 + line.id}) for line in lines[1:]]})
self.assertEqual(rec1.line_ids, lines)
# delete N lines: O(1) queries
rec1.invalidate_cache()
- with self.assertQueryCount(__system__=18, demo=18):
+ with self.assertQueryCount(__system__=16, demo=16):
rec1.write({'line_ids': [(2, line.id) for line in lines[0]]})
self.assertEqual(rec1.line_ids, lines[1:])
rec1.invalidate_cache()
- with self.assertQueryCount(__system__=16, demo=16):
+ with self.assertQueryCount(__system__=14, demo=14):
rec1.write({'line_ids': [(2, line.id) for line in lines[1:]]})
self.assertFalse(rec1.line_ids)
self.assertFalse(lines.exists())
@@ -133,12 +133,12 @@ class TestPerformance(SavepointCaseWithUserDemo):
# unlink N lines: O(1) queries
rec1.invalidate_cache()
- with self.assertQueryCount(__system__=14, demo=14):
+ with self.assertQueryCount(__system__=13, demo=13):
rec1.write({'line_ids': [(3, line.id) for line in lines[0]]})
self.assertEqual(rec1.line_ids, lines[1:])
rec1.invalidate_cache()
- with self.assertQueryCount(__system__=16, demo=16):
+ with self.assertQueryCount(__system__=14, demo=14):
rec1.write({'line_ids': [(3, line.id) for line in lines[1:]]})
self.assertFalse(rec1.line_ids)
self.assertFalse(lines.exists())
@@ -155,29 +155,29 @@ class TestPerformance(SavepointCaseWithUserDemo):
self.assertEqual(rec2.line_ids, lines[0])
rec1.invalidate_cache()
- with self.assertQueryCount(8):
+ with self.assertQueryCount(7):
rec2.write({'line_ids': [(4, line.id) for line in lines[1:]]})
self.assertFalse(rec1.line_ids)
self.assertEqual(rec2.line_ids, lines)
rec1.invalidate_cache()
- with self.assertQueryCount(6):
+ with self.assertQueryCount(5):
rec2.write({'line_ids': [(4, line.id) for line in lines[0]]})
self.assertEqual(rec2.line_ids, lines)
rec1.invalidate_cache()
- with self.assertQueryCount(6):
+ with self.assertQueryCount(5):
rec2.write({'line_ids': [(4, line.id) for line in lines[1:]]})
self.assertEqual(rec2.line_ids, lines)
# empty N lines in rec2: O(1) queries
rec1.invalidate_cache()
- with self.assertQueryCount(__system__=17, demo=17):
+ with self.assertQueryCount(__system__=15, demo=15):
rec2.write({'line_ids': [(5,)]})
self.assertFalse(rec2.line_ids)
rec1.invalidate_cache()
- with self.assertQueryCount(4):
+ with self.assertQueryCount(3):
rec2.write({'line_ids': [(5,)]})
self.assertFalse(rec2.line_ids)
@@ -186,7 +186,7 @@ class TestPerformance(SavepointCaseWithUserDemo):
# set N lines in rec2: O(1) queries
rec1.invalidate_cache()
- with self.assertQueryCount(5):
+ with self.assertQueryCount(4):
rec2.write({'line_ids': [(6, 0, lines[0].ids)]})
self.assertEqual(rec1.line_ids, lines[1:])
self.assertEqual(rec2.line_ids, lines[0])
diff --git a/odoo/models.py b/odoo/models.py
index 33c966bb1be..1461f9cef65 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -3004,14 +3004,6 @@ Fields:
else:
_logger.warning("%s.read() with unknown field '%s'", self._name, name)
- env = self.env
- cr, user, context, su = env.args
-
- # make a query object for selecting ids, and apply security rules to it
- param_ids = object()
- query = Query(['"%s"' % self._table], ['"%s".id IN %%s' % self._table], [param_ids])
- self._apply_ir_rules(query, 'read')
-
# determine the fields that are stored as columns in tables; ignore 'id'
fields_pre = [
field
@@ -3021,30 +3013,42 @@ Fields:
if not (field.inherited and callable(field.base_field.translate))
]
- # the query may involve several tables: we need fully-qualified names
- def qualify(field):
- col = field.name
- res = self._inherits_join_calc(self._table, field.name, query)
- if field.type == 'binary' and (context.get('bin_size') or context.get('bin_size_' + col)):
- # PG 9.2 introduces conflicting pg_size_pretty(numeric) -> need ::cast
- res = 'pg_size_pretty(length(%s)::bigint)' % res
- return '%s as "%s"' % (res, col)
+ if fields_pre:
+ env = self.env
+ cr, user, context, su = env.args
- # selected fields are: 'id' followed by fields_pre
- qual_names = [qualify(name) for name in [self._fields['id']] + fields_pre]
+ # make a query object for selecting ids, and apply security rules to it
+ param_ids = object()
+ query = Query(['"%s"' % self._table], ['"%s".id IN %%s' % self._table], [param_ids])
+ self._apply_ir_rules(query, 'read')
- # determine the actual query to execute
- from_clause, where_clause, params = query.get_sql()
- query_str = "SELECT %s FROM %s WHERE %s" % (",".join(qual_names), from_clause, where_clause)
+ # the query may involve several tables: we need fully-qualified names
+ def qualify(field):
+ col = field.name
+ res = self._inherits_join_calc(self._table, field.name, query)
+ if field.type == 'binary' and (context.get('bin_size') or context.get('bin_size_' + col)):
+ # PG 9.2 introduces conflicting pg_size_pretty(numeric) -> need ::cast
+ res = 'pg_size_pretty(length(%s)::bigint)' % res
+ return '%s as "%s"' % (res, col)
- # fetch one list of record values per field
- param_pos = params.index(param_ids)
+ # selected fields are: 'id' followed by fields_pre
+ qual_names = [qualify(name) for name in [self._fields['id']] + fields_pre]
- result = []
- for sub_ids in cr.split_for_in_conditions(self.ids):
- params[param_pos] = tuple(sub_ids)
- cr.execute(query_str, params)
- result += cr.fetchall()
+ # determine the actual query to execute
+ from_clause, where_clause, params = query.get_sql()
+ query_str = "SELECT %s FROM %s WHERE %s" % (",".join(qual_names), from_clause, where_clause)
+
+ # fetch one list of record values per field
+ param_pos = params.index(param_ids)
+
+ result = []
+ for sub_ids in cr.split_for_in_conditions(self.ids):
+ params[param_pos] = tuple(sub_ids)
+ cr.execute(query_str, params)
+ result += cr.fetchall()
+ else:
+ self.check_access_rule('read')
+ result = [(id_,) for id_ in self.ids]
fetched = self.browse()
if result: