summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphael Collet <rco@odoo.com>2017-07-06 17:16:03 +0200
committerRaphael Collet <rco@odoo.com>2017-07-10 16:18:33 +0200
commite25bcf41e2f312536a00ef8531b2f6c6e0334ebc (patch)
treecd4fed7e6ea124b024637c5327f0e73bce376148
parentc47a41b8b4c568ec63692a027c4dc13c82f1f172 (diff)
[REF] models: group recomputation triggers to minimize the number of requests
When several fields are modified and trigger the same recomputations, the same SQL queries were made to determine the records to recompute, because each field was processed independently. In order to avoid duplicated SQL queries, process the triggers of the modified fields altogether. This reduces the number of queries by 15% to 25%, depending on the number of computed fields.
-rw-r--r--odoo/fields.py54
-rw-r--r--odoo/models.py46
2 files changed, 41 insertions, 59 deletions
diff --git a/odoo/fields.py b/odoo/fields.py
index 37c57dcd5c4..84a1d83d7a1 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -1063,51 +1063,6 @@ class Field(MetaField('DummyField', (object,), {})):
# Notification when fields are modified
#
- def modified(self, records):
- """ Notify that field ``self`` has been modified on ``records``: prepare the
- fields/records to recompute, and return a spec indicating what to
- invalidate.
- """
- # invalidate the fields that depend on self, and prepare recomputation
- spec = [(self, records._ids)]
-
- # group triggers by model and path to reduce the number of calls to search()
- bymodel = defaultdict(lambda: defaultdict(list))
- for field, path in records._field_triggers[self]:
- bymodel[field.model_name][path].append(field)
-
- for model_name, bypath in pycompat.items(bymodel):
- for path, fields in pycompat.items(bypath):
- if path and any(field.compute and field.store for field in fields):
- # process stored fields
- stored = set(field for field in fields if field.compute and field.store)
- fields = set(fields) - stored
- if path == 'id':
- target0 = records
- else:
- # don't move this line to function top, see log
- env = records.env(user=SUPERUSER_ID, context={'active_test': False})
- target0 = env[model_name].search([(path, 'in', records.ids)])
- if target0:
- for field in stored:
- # discard records to not recompute for field
- target = target0 - records.env.protected(field)
- if not target:
- continue
- spec.append((field, target._ids))
- # recompute field on target in the environment of
- # records, and as user admin if required
- if field.compute_sudo:
- target = target.with_env(records.env(user=SUPERUSER_ID))
- else:
- target = target.with_env(records.env)
- target._recompute_todo(field)
- # process non-stored fields
- for field in fields:
- spec.append((field, None))
-
- return spec
-
def modified_draft(self, records):
""" Same as :meth:`modified`, but in draft mode. """
env = records.env
@@ -1898,15 +1853,6 @@ class _Relational(Field):
def null(self, record):
return record.env[self.comodel_name]
- def modified(self, records):
- # Invalidate cache for inverse fields, too. Note that the recomputation
- # of fields that depend on inverse fields is already covered by the
- # triggers.
- spec = super(_Relational, self).modified(records)
- for invf in records._field_inverses[self]:
- spec.append((invf, None))
- return spec
-
class Many2one(_Relational):
""" The value of such a field is a recordset of size 0 (no
diff --git a/odoo/models.py b/odoo/models.py
index 2222555fd00..ca40b271748 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -4713,12 +4713,48 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
:param fnames: iterable of field names that have been modified on
records ``self``
"""
- # each field knows what to invalidate and recompute
- spec = []
+ # group triggers by (model, path) to minimize the calls to search()
+ invalids = []
+ triggers = defaultdict(set)
for fname in fnames:
- spec += self._fields[fname].modified(self)
-
- self.env.invalidate(spec)
+ mfield = self._fields[fname]
+ # invalidate mfield on self, and its inverses fields
+ invalids.append((mfield, self._ids))
+ for field in self._field_inverses[mfield]:
+ invalids.append((field, None))
+ # group triggers by model and path to reduce the number of search()
+ for field, path in self._field_triggers[mfield]:
+ triggers[(field.model_name, path)].add(field)
+
+ # process triggers, mark fields to be invalidated/recomputed
+ for model_path, fields in triggers.items():
+ model_name, path = model_path
+ stored = {field for field in fields if field.compute and field.store}
+ # process stored fields
+ if path and stored:
+ # determine records of model_name linked by path to self
+ if path == 'id':
+ target0 = self
+ else:
+ env = self.env(user=SUPERUSER_ID, context={'active_test': False})
+ target0 = env[model_name].search([(path, 'in', self.ids)])
+ target0 = target0.with_env(self.env)
+ # prepare recomputation for each field on linked records
+ for field in stored:
+ # discard records to not recompute for field
+ target = target0 - self.env.protected(field)
+ if not target:
+ continue
+ invalids.append((field, target._ids))
+ # mark field to be recomputed on target
+ if field.compute_sudo:
+ target = target.sudo()
+ target._recompute_todo(field)
+ # process non-stored fields
+ for field in (fields - stored):
+ invalids.append((field, None))
+
+ self.env.invalidate(invalids)
def _recompute_check(self, field):
""" If ``field`` must be recomputed on some record in ``self``, return the