[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:
@@ -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")
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user