[REF] models: make the API of RecordCache more consistent
Make `RecordCache` work on a single record only, and implement a mapping from
field names (only) to values.
Also add explicit methods on `RecordCache` to wrap special behavior in cache,
and simplify all special values as a wrapper for a getter function.
The record cache may contain regular and special values. Modify `RecordCache`
so that only methods ending with `_value` check for regular values. The
dictionary methods considers both regular and special values as equivalent.
name in record._cache # test if cache has a value
iter(record._cache) # iterate on values
record._cache[name] # get value
record._cache.get(name) # get value or default
record._cache.has_value(name) # test if cache has a regular value
record._cache.get_value(name) # get regular value or default
This commit is contained in:
@@ -228,7 +228,8 @@ class IrActionsActWindow(models.Model):
|
||||
if len(existing) < len(self):
|
||||
# mark missing records in cache with a failed value
|
||||
exc = MissingError(_("Record does not exist or has been deleted."))
|
||||
(self - existing)._cache.update(fields.FailedValue(exc))
|
||||
for record in (self - existing):
|
||||
record._cache.set_failed(self._fields, exc)
|
||||
return existing
|
||||
|
||||
@api.model
|
||||
|
||||
+1
-1
@@ -941,7 +941,7 @@ class Environment(Mapping):
|
||||
|
||||
def check_cache(self):
|
||||
""" Check the cache consistency. """
|
||||
from odoo.fields import SpecialValue
|
||||
from odoo.models import SpecialValue
|
||||
|
||||
# make a full copy of the cache, and invalidate it
|
||||
cache_dump = dict(
|
||||
|
||||
+29
-57
@@ -37,24 +37,6 @@ _schema = logging.getLogger(__name__[:-7] + '.schema')
|
||||
|
||||
Default = object() # default value for __init__() methods
|
||||
|
||||
class SpecialValue(object):
|
||||
""" Encapsulates a value in the cache in place of a normal value. """
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
class FailedValue(SpecialValue):
|
||||
""" Special value that encapsulates an exception instead of a value. """
|
||||
def __init__(self, exception):
|
||||
self.exception = exception
|
||||
def get(self):
|
||||
raise self.exception
|
||||
|
||||
def _check_value(value):
|
||||
""" Return ``value``, or call its getter if ``value`` is a :class:`SpecialValue`. """
|
||||
return value.get() if isinstance(value, SpecialValue) else value
|
||||
|
||||
def copy_cache(records, env):
|
||||
""" Recursively copy the cache of ``records`` to the environment ``env``. """
|
||||
todo, done = set(records), set()
|
||||
@@ -911,14 +893,14 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
# only a single record may be accessed
|
||||
record.ensure_one()
|
||||
try:
|
||||
value = record._cache[self]
|
||||
value = record._cache[self.name]
|
||||
except KeyError:
|
||||
# cache miss, determine value and retrieve it
|
||||
if record.id:
|
||||
self.determine_value(record)
|
||||
else:
|
||||
self.determine_draft_value(record)
|
||||
value = record._cache[self]
|
||||
value = record._cache[self.name]
|
||||
else:
|
||||
# null record -> return the null value for this field
|
||||
value = self.convert_to_cache(False, record, validate=False)
|
||||
@@ -940,7 +922,7 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
spec = self.modified_draft(record)
|
||||
|
||||
# set value in cache, inverse field, and mark record as dirty
|
||||
record._cache[self] = value
|
||||
record._cache[self.name] = value
|
||||
if env.in_onchange:
|
||||
for invf in record._field_inverses[self]:
|
||||
invf._update(record[self.name], record)
|
||||
@@ -957,7 +939,7 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
record.write({self.name: write_value})
|
||||
# Update the cache unless value contains a new record
|
||||
if not (self.relational and not all(value)):
|
||||
record._cache[self] = value
|
||||
record._cache[self.name] = value
|
||||
|
||||
############################################################################
|
||||
#
|
||||
@@ -970,7 +952,7 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
fields = records._field_computed[self]
|
||||
for field in fields:
|
||||
for record in records:
|
||||
record._cache[field] = field.convert_to_cache(False, record, validate=False)
|
||||
record._cache[field.name] = field.convert_to_cache(False, record, validate=False)
|
||||
if isinstance(self.compute, pycompat.string_types):
|
||||
getattr(records, self.compute)()
|
||||
else:
|
||||
@@ -988,7 +970,7 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
try:
|
||||
self._compute_value(record)
|
||||
except Exception as exc:
|
||||
record._cache[self.name] = FailedValue(exc)
|
||||
record._cache.set_failed([self.name], exc)
|
||||
|
||||
def determine_value(self, record):
|
||||
""" Determine the value of ``self`` for ``record``. """
|
||||
@@ -1007,12 +989,10 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
computed = record._field_computed[self]
|
||||
for source, target in pycompat.izip(recs, recs.with_env(env)):
|
||||
try:
|
||||
values = target._convert_to_cache({
|
||||
f.name: source[f.name] for f in computed
|
||||
}, validate=False)
|
||||
except MissingError as e:
|
||||
values = FailedValue(e)
|
||||
target._cache.update(values)
|
||||
values = {f.name: source[f.name] for f in computed}
|
||||
target._cache.update(target._convert_to_cache(values, validate=False))
|
||||
except MissingError as exc:
|
||||
target._cache.set_failed(target._fields, exc)
|
||||
# the result is saved to database by BaseModel.recompute()
|
||||
return
|
||||
|
||||
@@ -1031,7 +1011,7 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
|
||||
else:
|
||||
# this is a non-stored non-computed field
|
||||
record._cache[self] = self.convert_to_cache(False, record, validate=False)
|
||||
record._cache[self.name] = self.convert_to_cache(False, record, validate=False)
|
||||
|
||||
def determine_draft_value(self, record):
|
||||
""" Determine the value of ``self`` for the given draft ``record``. """
|
||||
@@ -1041,7 +1021,7 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
self._compute_value(record)
|
||||
else:
|
||||
null = self.convert_to_cache(False, record, validate=False)
|
||||
record._cache[self] = SpecialValue(null)
|
||||
record._cache.set_special(self.name, lambda: null)
|
||||
|
||||
def determine_inverse(self, records):
|
||||
""" Given the value of ``self`` on ``records``, inverse the computation. """
|
||||
@@ -1137,7 +1117,8 @@ class Integer(Field):
|
||||
|
||||
def _update(self, records, value):
|
||||
# special case, when an integer field is used as inverse for a one2many
|
||||
records._cache[self] = value.id or 0
|
||||
for record in records:
|
||||
record._cache[self.name] = value.id or 0
|
||||
|
||||
def convert_to_export(self, value, record):
|
||||
if value or value == 0:
|
||||
@@ -1923,11 +1904,9 @@ class Many2one(_Relational):
|
||||
model.env['ir.model.constraint']._reflect_constraint(model, conname, 'f', None, self._module)
|
||||
|
||||
def _update(self, records, value):
|
||||
""" Update the cached value of ``self`` for ``records`` with ``value``.
|
||||
This is used to reflect the assignment ``value[name] = records``, where
|
||||
``name`` is the inverse field of ``self``.
|
||||
"""
|
||||
records._cache[self] = self.convert_to_cache(value, records, validate=False)
|
||||
""" Update the cached value of ``self`` for ``records`` with ``value``. """
|
||||
for record in records:
|
||||
record._cache[self.name] = self.convert_to_cache(value, record, validate=False)
|
||||
|
||||
def convert_to_column(self, value, record, values=None):
|
||||
return value or None
|
||||
@@ -1982,22 +1961,6 @@ class Many2one(_Relational):
|
||||
return False
|
||||
return super(Many2one, self).convert_to_onchange(value, record, fnames)
|
||||
|
||||
class UnionUpdate(SpecialValue):
|
||||
""" Placeholder for a value update; when this value is taken from the cache,
|
||||
it returns ``record[field.name] | value`` and stores it in the cache.
|
||||
"""
|
||||
def __init__(self, field, record, value):
|
||||
self.args = (field, record, value)
|
||||
|
||||
def get(self):
|
||||
field, record, value = self.args
|
||||
# in order to read the current field's value, remove self from cache
|
||||
del record._cache[field]
|
||||
# read the current field's value, and update it in cache only
|
||||
value = field.convert_to_cache(record[field.name] | value, record, validate=False)
|
||||
record._cache[field] = value
|
||||
return value
|
||||
|
||||
|
||||
class _RelationalMulti(_Relational):
|
||||
""" Abstract class for relational fields *2many. """
|
||||
@@ -2005,11 +1968,20 @@ class _RelationalMulti(_Relational):
|
||||
def _update(self, records, value):
|
||||
""" Update the cached value of ``self`` for ``records`` with ``value``. """
|
||||
for record in records:
|
||||
if self in record._cache:
|
||||
if self.name in record._cache:
|
||||
val = self.convert_to_cache(record[self.name] | value, record, validate=False)
|
||||
record._cache[self.name] = val
|
||||
else:
|
||||
val = UnionUpdate(self, record, value)
|
||||
record._cache[self] = val
|
||||
record._cache.set_special(self.name, 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
|
||||
del record._cache[self.name]
|
||||
val = self.convert_to_cache(record[self.name] | value, record, validate=False)
|
||||
record._cache[self.name] = val
|
||||
return val
|
||||
return getter
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
# cache format: tuple(ids)
|
||||
|
||||
+75
-74
@@ -2571,13 +2571,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
result = self.read([f.name for f in fs], load='_classic_write')
|
||||
|
||||
# check the cache, and update it if necessary
|
||||
if field not in self._cache:
|
||||
if not self._cache.has_value(field.name):
|
||||
for values in result:
|
||||
record = self.browse(values.pop('id'), self._prefetch)
|
||||
record._cache.update(record._convert_to_cache(values, validate=False))
|
||||
if not self._cache.contains(field):
|
||||
e = AccessError("No value found for %s.%s" % (self, field.name))
|
||||
self._cache[field] = FailedValue(e)
|
||||
if field.name not in self._cache:
|
||||
exc = AccessError("No value found for %s.%s" % (self, field.name))
|
||||
self._cache.set_failed([field.name], exc)
|
||||
|
||||
@api.multi
|
||||
def _read_from_database(self, field_names, inherited_field_names=[]):
|
||||
@@ -2674,15 +2674,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
# mark non-existing records in missing
|
||||
forbidden = missing.exists()
|
||||
if forbidden:
|
||||
_logger.info(
|
||||
_('The requested operation cannot be completed due to record rules: Document type: %s, Operation: %s, Records: %s, User: %s') % \
|
||||
(self._name, 'read', ','.join([str(r.id) for r in self][:6]), self._uid))
|
||||
# store an access error exception in existing records
|
||||
exc = AccessError(
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
|
||||
(self._name, 'read')
|
||||
)
|
||||
_logger.info(
|
||||
_('The requested operation cannot be completed due to record rules: Document type: %s, Operation: %s, Records: %s, User: %s') % \
|
||||
(self._name, 'read', ','.join([str(r.id) for r in self][:6]), self._uid))
|
||||
forbidden._cache.update(FailedValue(exc))
|
||||
for record in forbidden:
|
||||
record._cache.set_failed(self._fields, exc)
|
||||
|
||||
@api.multi
|
||||
def get_metadata(self):
|
||||
@@ -3869,7 +3870,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
if len(existing) < len(self):
|
||||
# mark missing records in cache with a failed value
|
||||
exc = MissingError(_("Record does not exist or has been deleted."))
|
||||
(self - existing)._cache.update(FailedValue(exc))
|
||||
for record in (self - existing):
|
||||
record._cache.set_failed(self._fields, exc)
|
||||
return existing
|
||||
|
||||
@api.multi
|
||||
@@ -4419,7 +4421,10 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
for name in name_seq.split('.'):
|
||||
field = recs._fields[name]
|
||||
null = field.convert_to_cache(False, self, validate=False)
|
||||
recs = recs.mapped(lambda rec: field.convert_to_record(rec._cache.get(field, null), rec))
|
||||
if recs:
|
||||
recs = recs.mapped(lambda rec: field.convert_to_record(rec._cache.get_value(name, null), rec))
|
||||
else:
|
||||
recs = field.convert_to_record(null, recs)
|
||||
return recs
|
||||
|
||||
def filtered(self, func):
|
||||
@@ -5011,81 +5016,77 @@ collections.Set.register(BaseModel)
|
||||
collections.Sequence.register(BaseModel)
|
||||
|
||||
class RecordCache(MutableMapping):
|
||||
""" Implements a proxy dictionary to read/update the cache of a record.
|
||||
Upon iteration, it looks like a dictionary mapping field names to
|
||||
values. However, fields may be used as keys as well.
|
||||
"""
|
||||
def __init__(self, records):
|
||||
self._recs = records
|
||||
""" A mapping from field names to values, to read and update the cache of a record. """
|
||||
def __init__(self, record):
|
||||
assert len(record) == 1, "Unexpected RecordCache(%s)" % record
|
||||
self._record = record
|
||||
|
||||
def contains(self, field):
|
||||
""" Return whether `records[0]` has a value for ``field`` in cache. """
|
||||
if isinstance(field, pycompat.string_types):
|
||||
field = self._recs._fields[field]
|
||||
return self._recs.id in self._recs.env.cache[field]
|
||||
def __contains__(self, name):
|
||||
""" Return whether `record` has a cached value for field ``name``. """
|
||||
field = self._record._fields[name]
|
||||
return self._record.id in self._record.env.cache[field]
|
||||
|
||||
def __contains__(self, field):
|
||||
""" Return whether `records[0]` has a regular value for ``field`` in cache. """
|
||||
if isinstance(field, pycompat.string_types):
|
||||
field = self._recs._fields[field]
|
||||
dummy = SpecialValue(None)
|
||||
value = self._recs.env.cache[field].get(self._recs.id, dummy)
|
||||
return not isinstance(value, SpecialValue)
|
||||
|
||||
def get(self, field, default=None):
|
||||
""" Return the cached, regular value of ``field`` for `records[0]`, or ``default``. """
|
||||
if isinstance(field, pycompat.string_types):
|
||||
field = self._recs._fields[field]
|
||||
dummy = SpecialValue(None)
|
||||
value = self._recs.env.cache[field].get(self._recs.id, dummy)
|
||||
return default if isinstance(value, SpecialValue) else value
|
||||
|
||||
def __getitem__(self, field):
|
||||
""" Return the cached value of ``field`` for `records[0]`. """
|
||||
if isinstance(field, pycompat.string_types):
|
||||
field = self._recs._fields[field]
|
||||
value = self._recs.env.cache[field][self._recs.id]
|
||||
def __getitem__(self, name):
|
||||
""" Return the cached value of field ``name`` for `record`. """
|
||||
field = self._record._fields[name]
|
||||
value = self._record.env.cache[field][self._record.id]
|
||||
return value.get() if isinstance(value, SpecialValue) else value
|
||||
|
||||
def __setitem__(self, field, value):
|
||||
""" Assign the cached value of ``field`` for all records in ``records``. """
|
||||
if isinstance(field, pycompat.string_types):
|
||||
field = self._recs._fields[field]
|
||||
values = dict.fromkeys(self._recs._ids, value)
|
||||
self._recs.env.cache[field].update(values)
|
||||
def __setitem__(self, name, value):
|
||||
""" Assign the cached value of field ``name`` for ``record``. """
|
||||
field = self._record._fields[name]
|
||||
self._record.env.cache[field][self._record.id] = value
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
""" Update the cache of all records in ``records``. If the argument is a
|
||||
``SpecialValue``, update all fields (except "magic" columns).
|
||||
"""
|
||||
if args and isinstance(args[0], SpecialValue):
|
||||
values = dict.fromkeys(self._recs._ids, args[0])
|
||||
for name, field in self._recs._fields.items():
|
||||
if name != 'id':
|
||||
self._recs.env.cache[field].update(values)
|
||||
else:
|
||||
return super(RecordCache, self).update(*args, **kwargs)
|
||||
|
||||
def __delitem__(self, field):
|
||||
""" Remove the cached value of ``field`` for all ``records``. """
|
||||
if isinstance(field, pycompat.string_types):
|
||||
field = self._recs._fields[field]
|
||||
field_cache = self._recs.env.cache[field]
|
||||
for id in self._recs._ids:
|
||||
field_cache.pop(id, None)
|
||||
def __delitem__(self, name):
|
||||
""" Remove the cached value of field ``name`` for ``record``. """
|
||||
field = self._record._fields[name]
|
||||
del self._record.env.cache[field][self._record.id]
|
||||
|
||||
def __iter__(self):
|
||||
""" Iterate over the field names with a regular value in cache. """
|
||||
cache, id = self._recs.env.cache, self._recs.id
|
||||
dummy = SpecialValue(None)
|
||||
for name, field in self._recs._fields.items():
|
||||
if name != 'id' and not isinstance(cache[field].get(id, dummy), SpecialValue):
|
||||
""" Iterate over the field names with a cached value. """
|
||||
cache, record_id = self._record.env.cache, self._record.id
|
||||
for name, field in self._record._fields.items():
|
||||
if name != 'id' and record_id in cache[field]:
|
||||
yield name
|
||||
|
||||
def __len__(self):
|
||||
""" Return the number of fields with a regular value in cache. """
|
||||
""" Return the number of fields with a cached value. """
|
||||
return sum(1 for name in self)
|
||||
|
||||
def has_value(self, name):
|
||||
""" Return whether `record` has a cached, regular value for field ``name``. """
|
||||
field = self._record._fields[name]
|
||||
dummy = SpecialValue(None)
|
||||
value = self._record.env.cache[field].get(self._record.id, dummy)
|
||||
return not isinstance(value, SpecialValue)
|
||||
|
||||
def get_value(self, name, default=None):
|
||||
""" Return the cached, regular value of field ``name`` for `record`, or ``default``. """
|
||||
field = self._record._fields[name]
|
||||
dummy = SpecialValue(None)
|
||||
value = self._record.env.cache[field].get(self._record.id, dummy)
|
||||
return default if isinstance(value, SpecialValue) else value
|
||||
|
||||
def set_special(self, name, getter):
|
||||
""" Use the given getter to get the cached value of field ``name``. """
|
||||
field = self._record._fields[name]
|
||||
self._record.env.cache[field][self._record.id] = SpecialValue(getter)
|
||||
|
||||
def set_failed(self, names, exception):
|
||||
""" Mark the given fields with the given exception. """
|
||||
def getter():
|
||||
raise exception
|
||||
for name in names:
|
||||
self.set_special(name, getter)
|
||||
|
||||
|
||||
class SpecialValue(object):
|
||||
""" Wrapper for a function to get the cached value of a field. """
|
||||
__slots__ = ['get']
|
||||
|
||||
def __init__(self, getter):
|
||||
self.get = getter
|
||||
|
||||
|
||||
AbstractModel = BaseModel
|
||||
|
||||
@@ -5214,4 +5215,4 @@ def _normalize_ids(arg, atoms=set(IdType)):
|
||||
|
||||
# keep those imports here to avoid dependency cycle errors
|
||||
from .osv import expression
|
||||
from .fields import Field, SpecialValue, FailedValue
|
||||
from .fields import Field
|
||||
|
||||
Reference in New Issue
Block a user