From a07a076c45fc4b83caa9ab30ca919b30016015a7 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 2 Jan 2019 13:14:08 +0000 Subject: [PATCH] [FIX] models: avoid prefetch hell when reading fields The method `read()` may be very slow when reading relational fields and computed fields, because the computed fields can be computed on a recordset that is larger than expected. The issue occurs on model 'res.partner' when reading fields 'child_ids' and 'purchase_order_count', for instance. Suppose we read those two fields on a partner with 1000 contacts. First, the one2many field is read from the database and stored to the cache; the latter adds the value ids to the prefetching of 'res.partner'. Then, the fields are fetched from the cache. When 'purchase_order_count' is accessed on the partner, the field is computed on all its children as well... closes odoo/odoo#29867 --- odoo/addons/base/tests/test_api.py | 25 +++++++++++++++++++++++++ odoo/models.py | 22 ++++++++++++---------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py index cbb63daec59..3894629d736 100644 --- a/odoo/addons/base/tests/test_api.py +++ b/odoo/addons/base/tests/test_api.py @@ -358,6 +358,31 @@ class TestAPI(common.TransactionCase): same_prefetch(empty, empty.bank_ids) same_prefetch(empty, empty.category_id) + @mute_logger('odoo.models') + def test_60_prefetch_read(self): + """ Check that reading a field computes it on self only. """ + Partner = self.env['res.partner'] + field = type(Partner).company_type + self.assertTrue(field.compute and not field.store) + + partner1 = Partner.create({'name': 'Foo'}) + partner2 = Partner.create({'name': 'Bar', 'parent_id': partner1.id}) + self.assertEqual(partner1.child_ids, partner2) + + # reading partner1 should not prefetch 'company_type' on partner2 + self.env.clear() + partner1 = partner1.with_prefetch() + partner1.read(['company_type']) + self.assertIn('company_type', partner1._cache) + self.assertNotIn('company_type', partner2._cache) + + # reading partner1 should not prefetch 'company_type' on partner2 + self.env.clear() + partner1 = partner1.with_prefetch() + partner1.read(['child_ids', 'company_type']) + self.assertIn('company_type', partner1._cache) + self.assertNotIn('company_type', partner2._cache) + @mute_logger('odoo.models') def test_70_one(self): """ Check method one(). """ diff --git a/odoo/models.py b/odoo/models.py index db2c6ed2b90..85419013c0e 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -3019,17 +3019,19 @@ class BaseModel(object): # retrieve results from records; this takes values from the cache and # computes remaining fields - result = [] - name_fields = [(name, self._fields[name]) for name in (stored + inherited + computed)] + data = {record: {'id': record.id} for record in self} + missing = set() use_name_get = (load == '_classic_read') - for record in self: - try: - values = {'id': record.id} - for name, field in name_fields: - values[name] = field.convert_to_read(record[name], record, use_name_get) - result.append(values) - except MissingError: - pass + for name in (stored + inherited + computed): + convert = self._fields[name].convert_to_read + # read every field with prefetching limited to self; this avoids + # computing fields on a larger recordset than self + for record in self.with_prefetch(): + try: + data[record][name] = convert(record[name], record, use_name_get) + except MissingError: + missing.add(record) + result = [data[record] for record in self if record not in missing] return result