[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.
This commit is contained in:
Raphael Collet
2017-07-10 16:18:33 +02:00
parent c47a41b8b4
commit e25bcf41e2
2 changed files with 40 additions and 58 deletions
-54
View File
@@ -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
+40 -4
View File
@@ -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)
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)
self.env.invalidate(spec)
# 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