diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-02-25 12:45:11 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-02-25 13:46:10 +0000 |
| commit | 6bb0963b6adb588925be9789e9b380eb437a13d2 (patch) | |
| tree | a9a345ffda88f5752fec9e2d8cdce85183c7cc86 /odoo | |
| parent | 93f10cb6291d3855353e36771e06c08d04b4464f (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.py | 4 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/tests/test_new_fields.py | 47 | ||||
| -rw-r--r-- | odoo/addons/test_performance/tests/test_performance.py | 26 | ||||
| -rw-r--r-- | odoo/models.py | 60 |
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: |
