[IMP] core: share fields by not duplicating them on model class
Also speed up the basic setup of fields that are always duplicated top-level (on the model's registry class). This saves time and memory, as we also discard field.args and field._base_fields on toplevel fields (those values are no longer useful after setup).
This commit is contained in:
@@ -510,8 +510,8 @@ class TestCustomFields(common.TransactionCase):
|
||||
})
|
||||
query_count = self.cr.sql_log_count - query_count
|
||||
|
||||
# create the related field, and assert it only takes 3 extra queries
|
||||
with self.assertQueryCount(query_count + 3):
|
||||
# create the related field, and assert it only takes 1 extra query
|
||||
with self.assertQueryCount(query_count + 1):
|
||||
self.env['ir.model.fields'].create({
|
||||
'model_id': self.env['ir.model']._get_id('res.partner'),
|
||||
'name': 'x_oh_boy',
|
||||
|
||||
+30
-1
@@ -217,6 +217,8 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
_sequence = None # absolute ordering of the field
|
||||
_base_fields = () # the fields defining self, in override order
|
||||
_extra_keys = () # unknown attributes set on the field
|
||||
_direct = False # whether self may be used directly (shared)
|
||||
_toplevel = False # whether self is on the model's registry class
|
||||
|
||||
automatic = False # whether the field is automatically created ("magic" field)
|
||||
inherited = False # whether the field is inherited (_inherits)
|
||||
@@ -291,6 +293,25 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
# runtime on the registry class of the model. The occurrences of the field
|
||||
# are given to the new field as the parameter '_base_fields'; it is a list
|
||||
# of fields in override order (or reverse MRO).
|
||||
#
|
||||
# In order to save memory, a field should avoid having field.args and/or
|
||||
# many attributes when possible. We call "direct" a field that can be set
|
||||
# up directly from its definition class. Direct fields are non-related
|
||||
# fields defined on models, and can be shared across registries. We call
|
||||
# "toplevel" a field that is put on the model's registry class, and is
|
||||
# therefore specific to the registry.
|
||||
#
|
||||
# Toplevel field are set up once, and are no longer set up from scratch
|
||||
# after that. Those fields can save memory by discarding field.args and
|
||||
# field._base_fields once set up, because those are no longer necessary.
|
||||
#
|
||||
# Non-toplevel non-direct fields are the fields on definition classes that
|
||||
# may not be shared. In other words, those fields are never used directly,
|
||||
# and are always recreated as toplevel fields. On those fields, the base
|
||||
# setup is useless, because only field.args is used for setting up other
|
||||
# fields. We therefore skip the base setup for those fields. The only
|
||||
# attributes of those fields are: '_sequence', 'args', 'model_name', 'name'
|
||||
# and '_module', which makes their __dict__'s size minimal.
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
""" Perform the base setup of a field.
|
||||
@@ -305,7 +326,15 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
# only for fields on definition classes, not registry classes
|
||||
self._module = owner._module
|
||||
owner._field_definitions.append(self)
|
||||
self._setup_attrs(owner, name)
|
||||
|
||||
if not self.args.get('related'):
|
||||
self._direct = True
|
||||
if self._direct or self._toplevel:
|
||||
self._setup_attrs(owner, name)
|
||||
if self._toplevel:
|
||||
# free memory, self.args and self._base_fields are no longer useful
|
||||
self.__dict__.pop('args', None)
|
||||
self.__dict__.pop('_base_fields', None)
|
||||
|
||||
#
|
||||
# Setup field parameter attributes
|
||||
|
||||
+34
-26
@@ -55,7 +55,7 @@ from .exceptions import AccessError, MissingError, ValidationError, UserError
|
||||
from .osv.query import Query
|
||||
from .tools import frozendict, lazy_classproperty, ormcache, \
|
||||
Collector, LastOrderedSet, OrderedSet, IterableGenerator, \
|
||||
groupby
|
||||
groupby, discardattr
|
||||
from .tools.config import config
|
||||
from .tools.func import frame_codeinfo
|
||||
from .tools.misc import CountingStream, clean_context, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT, get_lang
|
||||
@@ -324,11 +324,27 @@ VALID_AGGREGATE_FUNCTIONS = {
|
||||
# depends on the presence or absence of each definition class, which itself
|
||||
# depends on the modules loaded in the registry.
|
||||
#
|
||||
# In order to avoid any ambiguity in a field's definition, all the fields of a
|
||||
# model are recreated on its registry class. When a field is set up, it
|
||||
# retrieves its definition(s) from its model's ancestor classes. It also
|
||||
# collects other information from its model's (registry) class, like compute
|
||||
# dependencies.
|
||||
# By design, the registry class has access to all the fields on the model's
|
||||
# definition classes. When possible, the field is used directly from the
|
||||
# model's registry class. There are a number of cases where the field cannot be
|
||||
# used directly:
|
||||
# - the field is related (and bits may not be shared);
|
||||
# - the field is overridden on definition classes;
|
||||
# - the field is defined for another model (and accessible by mixin).
|
||||
#
|
||||
# The last case prevents sharing the field, because the field object is specific
|
||||
# to a model, and is used as a key in several key dictionaries, like the record
|
||||
# cache and pending computations.
|
||||
#
|
||||
# Setting up a field on its definition class helps saving memory and time.
|
||||
# Indeed, when sharing is possible, the field's setup is almost entirely done
|
||||
# where the field was defined. It is thus done when the definition class was
|
||||
# created, and it may be reused across registries.
|
||||
#
|
||||
# In the example below, the field 'foo' appears once on its model's definition
|
||||
# classes. Assuming that it is not related, that field can be set up directly
|
||||
# on its definition class. If the model appears in several registries, the
|
||||
# field 'foo' is effectively shared across registries.
|
||||
#
|
||||
# class A1(Model): Model
|
||||
# _name = 'a' / \
|
||||
@@ -338,20 +354,12 @@ VALID_AGGREGATE_FUNCTIONS = {
|
||||
# class A2(Model): \ /
|
||||
# _inherit = 'a' \ /
|
||||
# bar = ... a
|
||||
# foo
|
||||
# bar
|
||||
#
|
||||
#
|
||||
# SHARING FIELDS
|
||||
#
|
||||
# The registry class of a model is actually specific to a given registry. In
|
||||
# order to reduce the memory footprint of a registry, the fields on registry
|
||||
# classes can be shared across registries. A field on a registry class may
|
||||
# actually be shared with another registry class, provided it is not related and
|
||||
# the definition classes in the MRO of the registry classes are the same.
|
||||
# Related fields are banned from sharing, because one of their attribute refers
|
||||
# to another field from their registry, which is likely to not be shared across
|
||||
# the given registries.
|
||||
# On the other hand, the field 'bar' is overridden in its model's definition
|
||||
# classes. In that case, the framework recreates the field on the model's
|
||||
# registry class. The field's setup will be based on its definitions, and will
|
||||
# not be shared across registries.
|
||||
|
||||
def is_definition_class(cls):
|
||||
""" Return whether ``cls`` is a model definition class. """
|
||||
@@ -511,6 +519,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
if not isinstance(getattr(cls, name, field), Field):
|
||||
_logger.warning("In model %r, field %r overriding existing value", cls._name, name)
|
||||
setattr(cls, name, field)
|
||||
field._toplevel = True
|
||||
field.__set_name__(cls, name)
|
||||
cls._fields[name] = field
|
||||
|
||||
@@ -521,8 +530,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
"""
|
||||
cls = type(self)
|
||||
field = cls._fields.pop(name, None)
|
||||
if hasattr(cls, name):
|
||||
delattr(cls, name)
|
||||
discardattr(cls, name)
|
||||
if cls._rec_name == name:
|
||||
# fixup _rec_name and display_name's dependencies
|
||||
cls._rec_name = None
|
||||
@@ -2852,10 +2860,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
|
||||
# reset those attributes on the model's class for _setup_fields() below
|
||||
for attr in ('_rec_name', '_active_name'):
|
||||
try:
|
||||
delattr(cls, attr)
|
||||
except AttributeError:
|
||||
pass
|
||||
discardattr(cls, attr)
|
||||
|
||||
@api.model
|
||||
def _setup_base(self):
|
||||
@@ -2870,7 +2875,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
# retrieve fields from parent classes, and duplicate them on cls to
|
||||
# avoid clashes with inheritance between different models
|
||||
for name in cls._fields:
|
||||
delattr(cls, name)
|
||||
discardattr(cls, name)
|
||||
cls._fields.clear()
|
||||
|
||||
# collect the definitions of each field (base definition + overrides)
|
||||
@@ -2880,7 +2885,10 @@ class BaseModel(metaclass=MetaModel):
|
||||
for field in klass._field_definitions:
|
||||
definitions[field.name].append(field)
|
||||
for name, fields_ in definitions.items():
|
||||
self._add_field(name, fields_[-1].new(_base_fields=fields_))
|
||||
if len(fields_) == 1 and fields_[0]._direct and fields_[0].model_name == cls._name:
|
||||
cls._fields[name] = fields_[0]
|
||||
else:
|
||||
self._add_field(name, fields_[-1].new(_base_fields=fields_))
|
||||
|
||||
self._add_magic_fields()
|
||||
|
||||
|
||||
@@ -697,6 +697,13 @@ def attrgetter(*items):
|
||||
return tuple(resolve_attr(obj, attr) for attr in items)
|
||||
return g
|
||||
|
||||
def discardattr(obj, key):
|
||||
""" Perform a ``delattr(obj, key)`` but without crashing if ``key`` is not present. """
|
||||
try:
|
||||
delattr(obj, key)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# ---------------------------------------------
|
||||
# String management
|
||||
# ---------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user