[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:
Raphael Collet
2017-09-08 13:21:00 +02:00
parent 736e850a8e
commit 64e945aa44
4 changed files with 107 additions and 133 deletions
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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