[FIX] fields: combine special values during onchange

Consider a many2one field `foo_id` on model `bar`, with an inverse one2many
field `bar_ids` on model `foo`.  During an onchange, the statement

    bar.foo_id = foo

puts a special value in cache to add `bar` to the value of `foo.bar_ids`
without explicitly reading `foo.bar_ids`.

Executing the above statement a second time, the cache of `foo.bar_ids` is no
longer empty.  This causes the actual value of `foo.bar_ids` to be read and
updated.  The issue is that this can be slow for large values of `foo.bar_ids`.

Avoid reading the value of the one2many field by handling the case where the
cache contains the special value: simply update the special value to take into
account the second assignment.

closes odoo/odoo#28982
This commit is contained in:
Raphael Collet
2018-11-23 11:18:19 +00:00
parent 264f89f63f
commit 499cdd5708
3 changed files with 66 additions and 12 deletions
@@ -1,4 +1,8 @@
# -*- coding: utf-8 -*-
try:
from unittest.mock import patch
except ImportError:
from mock import patch
from odoo.tests import common
@@ -465,3 +469,32 @@ class TestOnChange(common.TransactionCase):
result = Message.onchange(value, ['message', 'message_name', 'message_currency'], field_onchange)
self.assertEqual(result['value'], onchange_result)
def test_onchange_many2one_one2many(self):
""" Setting a many2one field should not read the inverse one2many. """
discussion = self.env.ref('test_new_api.discussion_0')
field_onchange = self.Message._onchange_spec()
self.assertEqual(field_onchange.get('discussion'), '1')
values = {
'discussion': discussion.id,
'name': "[%s] %s" % ('', self.env.user.name),
'body': False,
'author': self.env.uid,
'size': 0,
}
called = [False]
orig_read = type(discussion).read
def mock_read(self, fields=None, load='_classic_read'):
if discussion in self and 'messages' in (fields or ()):
called[0] = True
return orig_read(self, fields, load)
# changing 'discussion' on message should not read 'messages' on discussion
with patch.object(type(discussion), 'read', mock_read, create=True):
self.env.cache.invalidate()
self.Message.onchange(values, 'discussion', field_onchange)
self.assertFalse(called[0], "discussion.messages has been read")
+6
View File
@@ -982,6 +982,12 @@ class Cache(object):
value = self._data[field][record.id].get(key, SpecialValue(None))
return default if isinstance(value, SpecialValue) else value
def get_special(self, record, field, default=None):
""" Return the special value of ``field`` for ``record``. """
key = field.cache_key(record)
value = self._data[field][record.id].get(key)
return value.get if isinstance(value, SpecialValue) else default
def set_special(self, record, field, getter):
""" Set the value of ``field`` for ``record`` to return ``getter()``. """
key = field.cache_key(record)
+27 -12
View File
@@ -2019,6 +2019,26 @@ class Many2one(_Relational):
return super(Many2one, self).convert_to_onchange(value, record, names)
class _RelationalMultiUpdate(object):
""" A getter to update the value of an x2many field, without reading its
value until necessary.
"""
__slots__ = ['record', 'field', 'value']
def __init__(self, record, field, value):
self.record = record
self.field = field
self.value = value
def __call__(self):
# determine the current field's value, and update it in cache only
record, field, value = self.record, self.field, self.value
cache = record.env.cache
cache.remove(record, field)
val = field.convert_to_cache(record[field.name] | value, record, validate=False)
cache.set(record, field, val)
return val
class _RelationalMulti(_Relational):
""" Abstract class for relational fields *2many. """
@@ -2030,21 +2050,16 @@ class _RelationalMulti(_Relational):
""" Update the cached value of ``self`` for ``records`` with ``value``. """
cache = records.env.cache
for record in records:
if cache.contains(record, self):
special = cache.get_special(record, self)
if isinstance(special, _RelationalMultiUpdate):
# include 'value' in the existing _RelationalMultiUpdate; this
# avoids reading the field's value (which may be large)
special.value |= value
elif cache.contains(record, self):
val = self.convert_to_cache(record[self.name] | value, record, validate=False)
cache.set(record, self, val)
else:
cache.set_special(record, self, self._update_getter(record, value))
def _update_getter(self, record, value):
def getter():
# determine the current field's value, and update it in cache only
cache = record.env.cache
cache.remove(record, self)
val = self.convert_to_cache(record[self.name] | value, record, validate=False)
cache.set(record, self, val)
return val
return getter
cache.set_special(record, self, _RelationalMultiUpdate(record, self, value))
def convert_to_cache(self, value, record, validate=True):
# cache format: tuple(ids)