[MERGE] orm: implement new model for the prefetching of records

The new model is now the following:
 - each recordset is attached to a prefetch object: records._prefetch
 - the prefetch object associates model names to record ids to prefetch:
   ids = records._prefetch[model_name]
 - a call to model.browse(ids) returns a recordset with a new prefetch object
 - the prefetch object is passed and populated by
    - iteration
    - access of relational field
    - with_env(), sudo(), with_context()
This commit is contained in:
Raphael Collet
2016-04-25 13:38:27 +02:00
13 changed files with 360 additions and 209 deletions
@@ -287,7 +287,7 @@ class BaseActionRule(models.Model):
if res:
if 'value' in res:
res['value'].pop('id', None)
self.update(self._convert_to_cache(res['value'], validate=False))
self.update({key: val for key, val in res['value'].iteritems() if key in self._fields})
if 'domain' in res:
result.setdefault('domain', {}).update(res['domain'])
if 'warning' in res:
@@ -41,7 +41,6 @@ class SaleOrderEventRegistration(models.TransientModel):
'sale_order_line_id': so_line.id,
}])
res['event_registration_ids'] = attendee_list
res = self._convert_to_cache(res, validate=False)
res = self._convert_to_write(res)
return res
+1 -1
View File
@@ -52,7 +52,7 @@ class TestMailTemplate(TestMail):
values = composer.onchange_template_id(self.email_template.id, 'comment', 'mail.channel', self.group_pigs.id)['value']
# use _convert_to_cache to return a browse record list from command list or id list for x2many fields
values = self.env['mail.compose.message']._convert_to_cache(values)
values = composer._convert_to_record(composer._convert_to_cache(values))
recipients = values['partner_ids']
attachments = values['attachment_ids']
+1 -1
View File
@@ -398,7 +398,7 @@ class MailComposer(models.TransientModel):
# ORM handle the assignation of command list on new onchange (api.v8),
# this force the complete replacement of x2many field with
# command and is compatible with onchange api.v7
values = self._convert_to_write(self._convert_to_cache(values))
values = self._convert_to_write(values)
return {'value': values}
+1 -1
View File
@@ -363,7 +363,7 @@ class Partner(osv.osv):
# create a record in cache, apply onchange then revert back to a dictionnary
invoice_line = invoice_line_obj.new(cr, uid, line_values, context=context)
invoice_line._onchange_product_id()
line_values = invoice_line._convert_to_write(invoice_line._cache)
line_values = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache})
line_values['price_unit'] = amount
invoice_obj.write(cr, uid, [invoice_id], {'invoice_line_ids': [(0, 0, line_values)]}, context=context)
invoice_list.append(invoice_id)
+2 -2
View File
@@ -156,7 +156,7 @@ class PosOrder(models.Model):
invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.ids
# We convert a new id object back to a dictionary to write to
# bridge between old and new api
inv_line = invoice_line._convert_to_write(invoice_line._cache)
inv_line = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache})
inv_line.update(price_unit=line.price_unit, discount=line.discount)
return InvoiceLine.sudo().create(inv_line)
@@ -410,7 +410,7 @@ class PosOrder(models.Model):
invoice._onchange_partner_id()
invoice.fiscal_position_id = order.fiscal_position_id
inv = invoice._convert_to_write(invoice._cache)
inv = invoice._convert_to_write({name: invoice[name] for name in invoice._cache})
new_invoice = Invoice.with_context(local_context).sudo().create(inv)
order.write({'invoice_id': new_invoice.id, 'state': 'invoiced'})
Invoice += new_invoice
+39
View File
@@ -79,6 +79,45 @@ Accessing a relational field (:class:`~openerp.fields.Many2one`,
# 1 database update
records.write({'a': 1, 'b': 2, 'c': 3})
Record cache and prefetching
----------------------------
Odoo maintains a cache for the fields of the records, so that not every field
access issues a database request, which would be terrible for performance. The
following example queries the database only for the first statement::
record.name # first access reads value from database
record.name # second access gets value from cache
To avoid reading one field on one record at a time, Odoo *prefetches* records
and fields following some heuristics to get good performance. Once a field must
be read on a given record, the ORM actually reads that field on a larger
recordset, and stores the returned values in cache for later use. The prefetched
recordset is usually the recordset from which the record comes by iteration.
Moreover, all simple stored fields (boolean, integer, float, char, text, date,
datetime, selection, many2one) are fetched altogether; they correspond to the
columns of the model's table, and are fetched efficiently in the same query.
Consider the following example, where ``partners`` is a recordset of 1000
records. Without prefetching, the loop would make 2000 queries to the database.
With prefetching, only one query is made::
for partner in partners:
print partner.name # first pass prefetches 'name' and 'lang'
# (and other fields) on all 'partners'
print partner.lang
The prefetching also works on *secondary records*: when relational fields are
read, their values (which are records) are subscribed for future prefetching.
Accessing one of those secondary records prefetches all secondary records from
the same model. This makes the following example generate only two queries, one
for partners and one for countries::
countries = set()
for partner in partners:
country = partner.country_id # first pass prefetches all partners
countries.add(country.name) # first pass prefetches all countries
Set operations
--------------
+1 -1
View File
@@ -402,7 +402,7 @@ class IrValues(models.Model):
# FIXME: needs cleanup
try:
action_def = {
field: action._fields[field].convert_to_read(action[field])
field: action._fields[field].convert_to_read(action[field], action)
for field in fields
}
if action._name in ('ir.actions.report.xml', 'ir.actions.act_window'):
+71 -18
View File
@@ -324,29 +324,82 @@ class TestAPI(common.TransactionCase):
self.env.check_cache()
@mute_logger('openerp.models')
def test_60_cache_prefetching(self):
def test_60_prefetch(self):
""" Check the record cache prefetching """
self.env.invalidate_all()
partners = self.env['res.partner'].search([], limit=models.PREFETCH_MAX)
self.assertTrue(len(partners) > 1)
# all the records of an instance already have an entry in cache
partners = self.env['res.partner'].search([])
partner_ids = self.env.prefetch['res.partner']
self.assertEqual(set(partners.ids), set(partner_ids))
# countries have not been fetched yet; their cache must be empty
countries = self.env['res.country'].browse()
self.assertFalse(self.env.prefetch['res.country'])
# all the records in partners are ready for prefetching
self.assertItemsEqual(partners.ids, partners._prefetch['res.partner'])
# reading ONE partner should fetch them ALL
countries |= partners[0].country_id
country_cache = self.env.cache[partners._fields['country_id']]
self.assertLessEqual(set(partners._ids), set(country_cache))
partner = next(p for p in partners)
partner.country_id
country_id_cache = self.env.cache[type(partners).country_id]
self.assertItemsEqual(partners.ids, country_id_cache)
# read all partners, and check that the cache already contained them
country_ids = list(self.env.prefetch['res.country'])
for p in partners:
countries |= p.country_id
self.assertLessEqual(set(countries.ids), set(country_ids))
# partners' countries are ready for prefetching
country_ids = set(cid for cids in country_id_cache.itervalues() for cid in cids)
self.assertTrue(len(country_ids) > 1)
self.assertItemsEqual(country_ids, partners._prefetch['res.country'])
# reading ONE partner country should fetch ALL partners' countries
country = next(p.country_id for p in partners if p.country_id)
country.name
name_cache = self.env.cache[type(country).name]
self.assertItemsEqual(country_ids, name_cache)
@mute_logger('openerp.models')
def test_60_prefetch_object(self):
""" Check the prefetching model. """
partners = self.env['res.partner'].search([], limit=models.PREFETCH_MAX)
self.assertTrue(partners)
def same_prefetch(a, b):
self.assertIs(a._prefetch, b._prefetch)
def diff_prefetch(a, b):
self.assertIsNot(a._prefetch, b._prefetch)
# the recordset operations below should create new prefetch objects
diff_prefetch(partners, partners.browse())
diff_prefetch(partners, partners.browse(partners.ids))
diff_prefetch(partners, partners[0])
diff_prefetch(partners, partners[:10])
# the recordset operations below should pass the prefetch object
same_prefetch(partners, partners.sudo(self.env.ref('base.user_demo')))
same_prefetch(partners, partners.with_context(active_test=False))
same_prefetch(partners, partners[:10].with_prefetch(partners._prefetch))
# iterating and reading relational fields should pass the prefetch object
self.assertEqual(type(partners).country_id.type, 'many2one')
self.assertEqual(type(partners).bank_ids.type, 'one2many')
self.assertEqual(type(partners).category_id.type, 'many2many')
vals0 = {
'name': 'Empty relational fields',
'country_id': False,
'bank_ids': [],
'category_id': [],
}
vals1 = {
'name': 'Non-empty relational fields',
'country_id': self.ref('base.be'),
'bank_ids': [(0, 0, {'acc_number': 'FOO42'})],
'category_id': [(4, self.ref('base.res_partner_category_0'))],
}
partners = partners.create(vals0) + partners.create(vals1)
for partner in partners:
same_prefetch(partners, partner)
same_prefetch(partners, partner.country_id)
same_prefetch(partners, partner.bank_ids)
same_prefetch(partners, partner.category_id)
# same with empty recordsets
empty = partners.browse()
same_prefetch(empty, empty.country_id)
same_prefetch(empty, empty.bank_ids)
same_prefetch(empty, empty.category_id)
@mute_logger('openerp.models')
def test_70_one(self):
@@ -385,20 +385,21 @@ class TestNewFields(common.TransactionCase):
def test_31_prefetch(self):
""" test prefetch of records handle AccessError """
Category = self.env['test_new_api.category']
cat_1 = Category.create({'name': 'NOACCESS'}).id
cat_2 = Category.create({'name': 'ACCESS', 'parent': cat_1}).id
cat1 = Category.create({'name': 'NOACCESS'})
cat2 = Category.create({'name': 'ACCESS', 'parent': cat1.id})
cats = cat1 + cat2
self.env.clear()
cat = Category.browse(cat_2)
self.assertEqual(cat.name, 'ACCESS')
# both categories should be in prefetch ids
self.assertSetEqual(self.env.prefetch[Category._name], set([cat_1, cat_2]))
cat1, cat2 = cats
self.assertEqual(cat2.name, 'ACCESS')
# both categories should be ready for prefetching
self.assertItemsEqual(cat2._prefetch[Category._name], cats.ids)
# but due to our (lame) overwrite of `read`, it should not forbid us to read records we have access to
self.assertFalse(len(cat.discussions))
self.assertEqual(cat.parent.id, cat_1)
self.assertFalse(cat2.discussions)
self.assertEqual(cat2.parent, cat1)
with self.assertRaises(AccessError):
Category.browse(cat_1).name
cat1.name
def test_40_new(self):
""" test new records. """
+7 -5
View File
@@ -865,7 +865,6 @@ class Environment(object):
self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
self.registry = RegistryManager.get(cr.dbname)
self.cache = defaultdict(dict) # {field: {id: value, ...}, ...}
self.prefetch = defaultdict(set) # {model_name: set(id), ...}
self.computed = defaultdict(set) # {field: set(id), ...}
self.dirty = defaultdict(set) # {record: set(field_name), ...}
self.all = envs
@@ -878,7 +877,7 @@ class Environment(object):
def __getitem__(self, model_name):
""" Return an empty recordset from the given model. """
return self.registry[model_name]._browse(self, ())
return self.registry[model_name]._browse((), self)
def __iter__(self):
""" Return an iterator on model names. """
@@ -973,7 +972,6 @@ class Environment(object):
""" Clear the cache of all environments. """
for env in list(self.all):
env.cache.clear()
env.prefetch.clear()
env.computed.clear()
env.dirty.clear()
@@ -1038,6 +1036,8 @@ class Environment(object):
def check_cache(self):
""" Check the cache consistency. """
from openerp.fields import SpecialValue
# make a full copy of the cache, and invalidate it
cache_dump = dict(
(field, dict(field_cache))
@@ -1053,9 +1053,11 @@ class Environment(object):
for record in records:
try:
cached = field_dump[record.id]
cached = cached.get() if isinstance(cached, SpecialValue) else cached
value = field.convert_to_record(cached, record)
fetched = record[field.name]
if fetched != cached:
info = {'cached': cached, 'fetched': fetched}
if fetched != value:
info = {'cached': value, 'fetched': fetched}
invalids.append((field, record, info))
except (AccessError, MissingError):
pass
+162 -135
View File
@@ -52,11 +52,12 @@ def copy_cache(records, env):
if record not in done:
done.add(record)
target = record.with_env(env)
for name, value in record._cache.iteritems():
for name in record._cache:
field = record._fields[name]
value = record[name]
if isinstance(value, BaseModel):
todo.update(value)
value = value.with_env(env)
target._cache[name] = value
target._cache[name] = field.convert_to_cache(value, target, validate=False)
def resolve_mro(model, name, predicate):
@@ -81,19 +82,16 @@ def default_new_to_old(field, value):
""" Convert the new-API default ``value`` to the old API. """
if callable(value):
from openerp import api
return api.model(lambda model: field.convert_to_write(value(model)))
return api.model(lambda model: field.convert_to_write(value(model), model))
else:
return value
def default_old_to_new(field, value):
""" Convert the old-API default ``value`` to the new API. """
if callable(value):
return lambda model: field.convert_to_cache(
value(model._model, model._cr, model._uid, model._context),
model, validate=False,
)
return lambda model: value(model._model, model._cr, model._uid, model._context)
else:
return lambda model: field.convert_to_cache(value, model, validate=False)
return lambda model: value
def default_old_to_old(field, value):
""" Convert the old-API default ``value`` to the old API. """
@@ -758,57 +756,60 @@ class Field(object):
# Conversion of values
#
def null(self, env):
""" return the null value for this field in the given environment """
def null(self, record):
""" Return the null value for this field in the record format. """
return False
def convert_to_cache(self, value, record, validate=True):
""" convert ``value`` to the cache level in ``env``; ``value`` may come from
an assignment, or have the format of methods :meth:`BaseModel.read`
or :meth:`BaseModel.write`
""" Convert ``value`` to the cache format; ``value`` may come from an
assignment, or have the format of methods :meth:`BaseModel.read` or
:meth:`BaseModel.write`. If the value represents a recordset, it should
be added for prefetching on ``record``.
:param record: the target record for the assignment, or an empty recordset
:param bool validate: when True, field-specific validation of
``value`` will be performed
:param bool validate: when True, field-specific validation of ``value``
will be performed
"""
return value
def convert_to_read(self, value, use_name_get=True):
""" convert ``value`` from the cache to a value as returned by method
:meth:`BaseModel.read`
def convert_to_record(self, value, record):
""" Convert ``value`` from the cache format to the record format.
If the value represents a recordset, it should share the prefetching of
``record``.
"""
return value
:param bool use_name_get: when True, value's diplay name will
be computed using :meth:`BaseModel.name_get`, if relevant
for the field
def convert_to_read(self, value, record, use_name_get=True):
""" Convert ``value`` from the record format to the format returned by
method :meth:`BaseModel.read`.
:param bool use_name_get: when True, the value's display name will be
computed using :meth:`BaseModel.name_get`, if relevant for the field
"""
return False if value is None else value
def convert_to_write(self, value):
""" convert ``value`` from the cache to a valid value for method
:meth:`BaseModel.write`.
def convert_to_write(self, value, record):
""" Convert ``value`` from the record format to the format of method
:meth:`BaseModel.write`.
"""
return self.convert_to_read(value)
return self.convert_to_read(value, record)
def convert_to_onchange(self, value, fnames=None):
""" convert ``value`` from the cache to a value as returned by method
:meth:`BaseModel.onchange`.
def convert_to_onchange(self, value, record, fnames=()):
""" Convert ``value`` from the record format to the format returned by
method :meth:`BaseModel.onchange`.
:param fnames: an optional collection of field names to convert
(for relational fields only)
:param fnames: an optional collection of field names to convert
(for relational fields only)
"""
return self.convert_to_read(value)
return self.convert_to_read(value, record)
def convert_to_export(self, value, env):
""" convert ``value`` from the cache to a valid value for export. The
parameter ``env`` is given for managing translations.
"""
def convert_to_export(self, value, record):
""" Convert ``value`` from the record format to the export format. """
if not value:
return ''
return value if env.context.get('export_raw_data') else ustr(value)
return value if record._context.get('export_raw_data') else ustr(value)
def convert_to_display_name(self, value, record=None):
""" convert ``value`` from the cache to a suitable display name. """
def convert_to_display_name(self, value, record):
""" Convert ``value`` from the record format to a suitable display name. """
return ustr(value)
############################################################################
@@ -821,28 +822,23 @@ class Field(object):
if record is None:
return self # the field is accessed through the owner class
if not record:
# null record -> return the null value for this field
return self.null(record.env)
# only a single record may be accessed
record.ensure_one()
try:
return record._cache[self]
except KeyError:
pass
# cache miss, retrieve value
if record.id:
# normal record -> read or compute value for this field
self.determine_value(record)
if record:
# only a single record may be accessed
record.ensure_one()
try:
value = record._cache[self]
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]
else:
# draft record -> compute the value or let it be null
self.determine_draft_value(record)
# null record -> return the null value for this field
value = self.convert_to_cache(False, record, validate=False)
# the result should be in cache now
return record._cache[self]
return self.convert_to_record(value, record)
def __set__(self, record, value):
""" set the value of field ``self`` on ``record`` """
@@ -862,7 +858,7 @@ class Field(object):
record._cache[self] = value
if env.in_onchange:
for invf in record._field_inverses[self]:
invf._update(value, record)
invf._update(record[self.name], record)
record._set_dirty(self.name)
# determine more dependent fields, and invalidate them
@@ -872,7 +868,8 @@ class Field(object):
else:
# simply write to the database, and update cache
record.write({self.name: self.convert_to_write(value)})
write_value = self.convert_to_write(self.convert_to_record(value, record), record)
record.write({self.name: write_value})
record._cache[self] = value
############################################################################
@@ -885,7 +882,8 @@ class Field(object):
# initialize the fields to their corresponding null value in cache
computed = records._field_computed[self]
for field in computed:
records._cache[field] = field.null(records.env)
for record in records:
record._cache[field] = field.convert_to_cache(False, record, validate=False)
records.env.computed[field].update(records._ids)
if isinstance(self.compute, basestring):
getattr(records, self.compute)()
@@ -947,14 +945,15 @@ class Field(object):
else:
# this is a non-stored non-computed field
record._cache[self] = self.null(env)
record._cache[self] = self.convert_to_cache(False, record, validate=False)
def determine_draft_value(self, record):
""" Determine the value of ``self`` for the given draft ``record``. """
if self.compute:
self._compute_value(record)
else:
record._cache[self] = SpecialValue(self.null(record.env))
null = self.convert_to_cache(False, record, validate=False)
record._cache[self] = SpecialValue(null)
def determine_inverse(self, records):
""" Given the value of ``self`` on ``records``, inverse the computation. """
@@ -1047,8 +1046,8 @@ class Boolean(Field):
def convert_to_cache(self, value, record, validate=True):
return bool(value)
def convert_to_export(self, value, env):
if env.context.get('export_raw_data'):
def convert_to_export(self, value, record):
if record._context.get('export_raw_data'):
return value
return ustr(value)
@@ -1068,7 +1067,7 @@ class Integer(Field):
return value.get('id', False)
return int(value or 0)
def convert_to_read(self, value, use_name_get=True):
def convert_to_read(self, value, record, use_name_get=True):
# Integer values greater than 2^31-1 are not supported in pure XMLRPC,
# so we have to pass them as floats :-(
if value and value > xmlrpclib.MAXINT:
@@ -1079,9 +1078,9 @@ class Integer(Field):
# special case, when an integer field is used as inverse for a one2many
records._cache[self] = value.id or 0
def convert_to_export(self, value, env):
def convert_to_export(self, value, record):
if value or value == 0:
return value if env.context.get('export_raw_data') else ustr(value)
return value if record._context.get('export_raw_data') else ustr(value)
return ''
@@ -1125,9 +1124,9 @@ class Float(Field):
digits = self.digits
return float_round(value, precision_digits=digits[1]) if digits else value
def convert_to_export(self, value, env):
def convert_to_export(self, value, record):
if value or value == 0.0:
return value if env.context.get('export_raw_data') else ustr(value)
return value if record._context.get('export_raw_data') else ustr(value)
return ''
@@ -1165,11 +1164,12 @@ class Monetary(Field):
"Field %s with unknown currency_field %r" % (self, self.currency_field)
def convert_to_cache(self, value, record, validate=True):
currency = record[self.currency_field]
# FIXME @rco-odoo: currency may not be already initialized if it is a
# function or related field!
if currency:
return currency.round(float(value or 0.0))
if validate:
currency = record[self.currency_field]
# FIXME @rco-odoo: currency may not be already initialized if it is
# a function or related field!
if currency:
return currency.round(float(value or 0.0))
return float(value or 0.0)
@@ -1255,6 +1255,7 @@ class Char(_String):
return False
return ustr(value)[:self.size]
class Text(_String):
""" Very similar to :class:`~.Char` but used for longer contents, does not
have a size and usually displayed as a multiline text box.
@@ -1272,6 +1273,7 @@ class Text(_String):
return False
return ustr(value)
class Html(_String):
type = 'html'
_slots = {
@@ -1362,10 +1364,10 @@ class Date(Field):
return value[:DATE_LENGTH]
return self.to_string(value)
def convert_to_export(self, value, env):
def convert_to_export(self, value, record):
if not value:
return ''
return self.from_string(value) if env.context.get('export_raw_data') else ustr(value)
return self.from_string(value) if record._context.get('export_raw_data') else ustr(value)
class Datetime(Field):
@@ -1433,12 +1435,12 @@ class Datetime(Field):
return value
return self.to_string(value)
def convert_to_export(self, value, env):
def convert_to_export(self, value, record):
if not value:
return ''
return self.from_string(value) if env.context.get('export_raw_data') else ustr(value)
return self.from_string(value) if record._context.get('export_raw_data') else ustr(value)
def convert_to_display_name(self, value, record=None):
def convert_to_display_name(self, value, record):
assert record, 'Record expected'
return Datetime.to_string(Datetime.context_timestamp(record, Datetime.from_string(value)))
@@ -1544,11 +1546,11 @@ class Selection(Field):
return False
raise ValueError("Wrong value for %s: %r" % (self, value))
def convert_to_export(self, value, env):
def convert_to_export(self, value, record):
if not isinstance(self.selection, list):
# FIXME: this reproduces an existing buggy behavior!
return value if value else ''
for item in self._description_selection(env):
for item in self._description_selection(record.env):
if item[0] == value:
return item[1]
return False
@@ -1569,24 +1571,31 @@ class Reference(Selection):
"Reference field %s with non-integer size %r" % (self, self.size)
def convert_to_cache(self, value, record, validate=True):
# cache format: (res_model, res_id) or False
def process(res_model, res_id):
record._prefetch[res_model].add(res_id)
return (res_model, res_id)
if isinstance(value, BaseModel):
if ((not validate or value._name in self.get_values(record.env))
and len(value) <= 1):
return value.with_env(record.env) or False
if not validate or (value._name in self.get_values(record.env) and len(value) <= 1):
return process(value._name, value.id) if value else False
elif isinstance(value, basestring):
res_model, res_id = value.split(',')
return record.env[res_model].browse(int(res_id))
return process(res_model, int(res_id))
elif not value:
return False
raise ValueError("Wrong value for %s: %r" % (self, value))
def convert_to_read(self, value, use_name_get=True):
def convert_to_record(self, value, record):
return value and record.env[value[0]].browse([value[1]], record._prefetch)
def convert_to_read(self, value, record, use_name_get=True):
return "%s,%s" % (value._name, value.id) if value else False
def convert_to_export(self, value, env):
def convert_to_export(self, value, record):
return value.name_get()[0][1] if value else ''
def convert_to_display_name(self, value, record=None):
def convert_to_display_name(self, value, record):
return ustr(value and value.display_name)
@@ -1625,8 +1634,8 @@ class _Relational(Field):
_column_domain = property(attrgetter('domain'))
_column_context = property(attrgetter('context'))
def null(self, env):
return env[self.comodel_name]
def null(self, record):
return record.env[self.comodel_name]
def modified(self, records):
# Invalidate cache for inverse fields, too. Note that the recomputation
@@ -1682,47 +1691,54 @@ class Many2one(_Relational):
_column_auto_join = property(attrgetter('auto_join'))
def _update(self, records, value):
""" Update the cached value of ``self`` for ``records`` with ``value``. """
records._cache[self] = 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)
def convert_to_cache(self, value, record, validate=True):
if isinstance(value, (NoneType, int, long)):
return record.env[self.comodel_name].browse(value)
if isinstance(value, BaseModel):
if value._name == self.comodel_name and len(value) <= 1:
return value.with_env(record.env)
# cache format: tuple(ids)
def process(ids):
return record._prefetch[self.comodel_name].update(ids) or ids
if type(value) in IdType:
return process((value,))
elif isinstance(value, BaseModel):
if not validate or (value._name == self.comodel_name and len(value) <= 1):
return process(value._ids)
raise ValueError("Wrong value for %s: %r" % (self, value))
elif isinstance(value, tuple):
return record.env[self.comodel_name].browse(value[0])
return process((value[0],))
elif isinstance(value, dict):
return record.env[self.comodel_name].new(value)
return process(record.env[self.comodel_name].new(value)._ids)
else:
return self.null(record.env)
return ()
def convert_to_read(self, value, use_name_get=True):
def convert_to_record(self, value, record):
return record.env[self.comodel_name]._browse(value, record.env, record._prefetch)
def convert_to_read(self, value, record, use_name_get=True):
if use_name_get and value:
# evaluate name_get() as superuser, because the visibility of a
# many2one field value (id and name) depends on the current record's
# access rights, and not the value's access rights.
try:
value_sudo = value.sudo()
# performance trick: make sure that all records of the same
# model as value in value.env will be prefetched in value_sudo.env
value_sudo.env.prefetch[value._name].update(value.env.prefetch[value._name])
return value_sudo.name_get()[0]
# performance: value.sudo() prefetches the same records as value
return value.sudo().name_get()[0]
except MissingError:
# Should not happen, unless the foreign key is missing.
return False
else:
return value.id
def convert_to_write(self, value):
def convert_to_write(self, value, record):
return value.id
def convert_to_export(self, value, env):
def convert_to_export(self, value, record):
return value.name_get()[0][1] if value else ''
def convert_to_display_name(self, value, record=None):
def convert_to_display_name(self, value, record):
return ustr(value.display_name)
@@ -1738,8 +1754,9 @@ class UnionUpdate(SpecialValue):
# 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
record._cache[field] = new_value = record[field.name] | value
return new_value
value = field.convert_to_cache(record[field.name] | value, record, validate=False)
record._cache[field] = value
return value
class _RelationalMulti(_Relational):
@@ -1749,14 +1766,19 @@ class _RelationalMulti(_Relational):
""" Update the cached value of ``self`` for ``records`` with ``value``. """
for record in records:
if self in record._cache:
record._cache[self] = record[self.name] | value
value = self.convert_to_cache(record[self.name] | value, record, validate=False)
else:
record._cache[self] = UnionUpdate(self, record, value)
value = UnionUpdate(self, record, value)
record._cache[self] = value
def convert_to_cache(self, value, record, validate=True):
# cache format: tuple(ids)
def process(ids):
return record._prefetch[self.comodel_name].update(ids) or ids
if isinstance(value, BaseModel):
if value._name == self.comodel_name:
return value.with_env(record.env)
if not validate or (value._name == self.comodel_name):
return process(value._ids)
elif isinstance(value, list):
# value is a list of record ids or commands
comodel = record.env[self.comodel_name]
@@ -1785,39 +1807,43 @@ class _RelationalMulti(_Relational):
ids.add(comodel.new(command).id)
else:
ids.add(command)
# return result as a recordset
return comodel.browse(list(ids))
# return result as a tuple
return process(tuple(ids))
elif not value:
return self.null(record.env)
return ()
raise ValueError("Wrong value for %s: %s" % (self, value))
def convert_to_read(self, value, use_name_get=True):
def convert_to_record(self, value, record):
return record.env[self.comodel_name]._browse(value, record.env, record._prefetch)
def convert_to_read(self, value, record, use_name_get=True):
return value.ids
def convert_to_write(self, value):
def convert_to_write(self, value, record):
# make result with new and existing records
result = [(5,)]
for record in value:
if not record.id:
values = dict(record._cache)
values = {name: record[name] for name in record._cache}
values = record._convert_to_write(values)
result.append((0, 0, values))
elif record._is_dirty():
values = {k: record._cache[k] for k in record._get_dirty()}
values = {name: record[name] for name in record._get_dirty()}
values = record._convert_to_write(values)
result.append((1, record.id, values))
else:
result.append((4, record.id))
return result
def convert_to_onchange(self, value, fnames=None):
def convert_to_onchange(self, value, record, fnames=()):
# return the recordset value as a list of commands; the commands may
# give all fields values, the client is responsible for figuring out
# which fields are actually dirty
fields = [(name, value._fields[name]) for name in (fnames or []) if name != 'id']
converters = [(name, value._fields[name].convert_to_onchange)
for name in fnames if name != 'id']
result = [(5,)]
for record in value:
vals = {name: field.convert_to_onchange(record[name]) for name, field in fields}
vals = {name: convert(record[name], record) for name, convert in converters}
if not record.id:
result.append((0, 0, vals))
elif vals:
@@ -1826,10 +1852,10 @@ class _RelationalMulti(_Relational):
result.append((4, record.id))
return result
def convert_to_export(self, value, env):
def convert_to_export(self, value, record):
return ','.join(name for id, name in value.name_get()) if value else ''
def convert_to_display_name(self, value, record=None):
def convert_to_display_name(self, value, record):
raise NotImplementedError()
def _compute_related(self, records):
@@ -1898,11 +1924,11 @@ class One2many(_RelationalMulti):
_column_auto_join = property(attrgetter('auto_join'))
_column_limit = property(attrgetter('limit'))
def convert_to_onchange(self, value, fnames=None):
def convert_to_onchange(self, value, record, fnames=()):
if fnames:
# do not serialize self's inverse field
fnames = [name for name in fnames if name != self.inverse_name]
return super(One2many, self).convert_to_onchange(value, fnames)
return super(One2many, self).convert_to_onchange(value, record, fnames)
class Many2many(_RelationalMulti):
@@ -2016,8 +2042,9 @@ class Id(Field):
def __set__(self, record, value):
raise TypeError("field 'id' cannot be assigned")
# imported here to avoid dependency cycle issues
from openerp import SUPERUSER_ID
from .exceptions import Warning, AccessError, MissingError
from .models import check_pg_name, BaseModel, MAGIC_COLUMNS
from .exceptions import AccessError, MissingError
from .models import check_pg_name, BaseModel, IdType
from .osv import fields
+64 -34
View File
@@ -269,7 +269,7 @@ class NewId(object):
def __nonzero__(self):
return False
IdType = (int, long, basestring, NewId)
IdType = (int, long, str, unicode, NewId)
# maximum number of prefetched records
@@ -945,7 +945,7 @@ class BaseModel(object):
# this part could be simpler, but it has to be done this way
# in order to reproduce the former behavior
if not isinstance(value, BaseModel):
current[i] = field.convert_to_export(value, self.env)
current[i] = field.convert_to_export(value, record)
else:
primary_done.append(name)
@@ -1383,7 +1383,6 @@ class BaseModel(object):
parent_fields[field.model_name].append(field.name)
# convert default values to the right format
defaults = self._convert_to_cache(defaults, validate=False)
defaults = self._convert_to_write(defaults)
# add default values for inherited fields
@@ -3381,7 +3380,8 @@ class BaseModel(object):
else:
_logger.warning("%s.read() with unknown field '%s'", self._name, name)
# fetch stored fields from the database to the cache
# fetch stored fields from the database to the cache; this should feed
# the prefetching of secondary records
self._read_from_database(stored, inherited)
# retrieve results from records; this takes values from the cache and
@@ -3393,7 +3393,7 @@ class BaseModel(object):
try:
values = {'id': record.id}
for name, field in name_fields:
values[name] = field.convert_to_read(record[name], use_name_get)
values[name] = field.convert_to_read(record[name], record, use_name_get)
result.append(values)
except MissingError:
pass
@@ -3450,6 +3450,7 @@ class BaseModel(object):
# fetch records with read()
assert self in records and field in fs
records = records.with_prefetch(self._prefetch)
result = []
try:
result = records.read([f.name for f in fs], load='_classic_write')
@@ -3460,7 +3461,7 @@ class BaseModel(object):
# check the cache, and update it if necessary
if field not in self._cache:
for values in result:
record = self.browse(values.pop('id'))
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))
@@ -3546,7 +3547,7 @@ class BaseModel(object):
# store result in cache for POST fields
for vals in result:
record = self.browse(vals['id'])
record = self.browse(vals['id'], self._prefetch)
record._cache.update(record._convert_to_cache(vals, validate=False))
# determine the fields that must be processed now;
@@ -3588,7 +3589,7 @@ class BaseModel(object):
# store result in cache
for vals in result:
record = self.browse(vals.pop('id'))
record = self.browse(vals.pop('id'), self._prefetch)
record._cache.update(record._convert_to_cache(vals, validate=False))
# store failed values in cache for the records that could not be read
@@ -5510,24 +5511,30 @@ class BaseModel(object):
#
@classmethod
def _browse(cls, env, ids):
""" Create an instance attached to ``env``; ``ids`` is a tuple of record
ids.
def _browse(cls, ids, env, prefetch=None):
""" Create a recordset instance.
:param ids: a tuple of record ids
:param env: an environment
:param prefetch: an optional prefetch object
"""
records = object.__new__(cls)
records.env = env
records._ids = ids
env.prefetch[cls._name].update(ids)
if prefetch is None:
prefetch = defaultdict(set) # {model_name: set(ids)}
records._prefetch = prefetch
prefetch[cls._name].update(ids)
return records
@api.v7
def browse(self, cr, uid, arg=None, context=None):
ids = _normalize_ids(arg)
#assert all(isinstance(id, IdType) for id in ids), "Browsing invalid ids: %s" % ids
return self._browse(Environment(cr, uid, context or {}), ids)
return self._browse(ids, Environment(cr, uid, context or {}))
@api.v8
def browse(self, arg=None):
def browse(self, arg=None, prefetch=None):
""" browse([ids]) -> records
Returns a recordset for the ids provided as parameter in the current
@@ -5537,7 +5544,7 @@ class BaseModel(object):
"""
ids = _normalize_ids(arg)
#assert all(isinstance(id, IdType) for id in ids), "Browsing invalid ids: %s" % ids
return self._browse(self.env, ids)
return self._browse(ids, self.env, prefetch)
#
# Internal properties, for manipulating the instance's implementation
@@ -5575,10 +5582,11 @@ class BaseModel(object):
The new environment will not benefit from the current
environment's data cache, so later data access may incur extra
delays while re-fetching from the database.
The returned recordset has the same prefetch object as ``self``.
:type env: :class:`~openerp.api.Environment`
"""
return self._browse(env, self._ids)
return self._browse(self._ids, env, self._prefetch)
def sudo(self, user=SUPERUSER_ID):
""" sudo([user=SUPERUSER])
@@ -5606,6 +5614,7 @@ class BaseModel(object):
re-evaluated, the new recordset will not benefit from the current
environment's data cache, so later data access may incur extra
delays while re-fetching from the database.
The returned recordset has the same prefetch object as ``self``.
"""
return self.with_env(self.env(user=user))
@@ -5625,10 +5634,22 @@ class BaseModel(object):
# -> r2._context is {'key2': True}
r2 = records.with_context(key2=True)
# -> r2._context is {'key1': True, 'key2': True}
.. note:
The returned recordset has the same prefetch object as ``self``.
"""
context = dict(args[0] if args else self._context, **kwargs)
return self.with_env(self.env(context=context))
def with_prefetch(self, prefetch=None):
""" with_prefetch([prefetch]) -> records
Return a new version of this recordset that uses the given prefetch
object, or a new prefetch object if not given.
"""
return self._browse(self._ids, self.env, prefetch)
def _convert_to_cache(self, values, update=False, validate=True):
""" Convert the ``values`` dictionary into cached values.
@@ -5637,20 +5658,32 @@ class BaseModel(object):
:param validate: whether values must be checked
"""
fields = self._fields
target = self if update else self.browse()
target = self if update else self.browse([], self._prefetch)
return {
name: fields[name].convert_to_cache(value, target, validate=validate)
for name, value in values.iteritems()
if name in fields
}
def _convert_to_record(self, values):
""" Convert the ``values`` dictionary from the cache format to the
record format.
"""
return {
name: self._fields[name].convert_to_record(value, self)
for name, value in values.iteritems()
}
def _convert_to_write(self, values):
""" Convert the ``values`` dictionary into the format of :meth:`write`. """
fields = self._fields
result = {}
for name, value in values.iteritems():
if name in fields:
value = fields[name].convert_to_write(value)
field = fields[name]
value = field.convert_to_cache(value, self, validate=False)
value = field.convert_to_record(value, self)
value = field.convert_to_write(value, self)
if not isinstance(value, NewId):
result[name] = value
return result
@@ -5696,8 +5729,8 @@ class BaseModel(object):
recs = self
for name in name_seq.split('.'):
field = recs._fields[name]
null = field.null(self.env)
recs = recs.mapped(lambda rec: rec._cache.get(field, null))
null = field.convert_to_cache(False, self, validate=False)
recs = recs.mapped(lambda rec: field.convert_to_record(rec._cache.get(field, null), rec))
return recs
def filtered(self, func):
@@ -5797,7 +5830,7 @@ class BaseModel(object):
def __iter__(self):
""" Return an iterator over ``self``. """
for id in self._ids:
yield self._browse(self.env, (id,))
yield self._browse((id,), self.env, self._prefetch)
def __contains__(self, item):
""" Test whether ``item`` (record or field name) is an element of ``self``.
@@ -5907,9 +5940,9 @@ class BaseModel(object):
# important: one must call the field's getter
return self._fields[key].__get__(self, type(self))
elif isinstance(key, slice):
return self._browse(self.env, self._ids[key])
return self._browse(self._ids[key], self.env)
else:
return self._browse(self.env, (self._ids[key],))
return self._browse((self._ids[key],), self.env)
def __setitem__(self, key, value):
""" Assign the field ``key`` to ``value`` in record ``self``. """
@@ -5931,10 +5964,7 @@ class BaseModel(object):
the records of model ``self`` in cache that have no value for ``field``
(:class:`Field` instance).
"""
env = self.env
prefetch_ids = env.prefetch[self._name]
prefetch_ids.update(self._ids)
ids = filter(None, prefetch_ids - set(env.cache[field]))
ids = filter(None, self._prefetch[self._name] - set(self.env.cache[field]))
return self.browse(ids)
@api.model
@@ -6078,7 +6108,7 @@ class BaseModel(object):
return
if res.get('value'):
res['value'].pop('id', None)
self.update(self._convert_to_cache(res['value'], validate=False))
self.update({key: val for key, val in res['value'].iteritems() if key in self._fields})
if res.get('domain'):
result.setdefault('domain', {}).update(res['domain'])
if res.get('warning'):
@@ -6111,9 +6141,9 @@ class BaseModel(object):
def __init__(self, record):
self._record = record
def __getitem__(self, name):
field = self._record._fields[name]
value = self._record[name]
return field.convert_to_write(value)
record = self._record
field = record._fields[name]
return field.convert_to_write(record[name], record)
def __getattr__(self, name):
return self[name]
@@ -6167,7 +6197,7 @@ class BaseModel(object):
# create a new record with values, and attach ``self`` to it
with env.do_in_onchange():
record = self.new(values)
values = dict(record._cache)
values = {name: record[name] for name in record._cache}
# attach ``self`` with a different context (for cache consistency)
record._origin = self.with_context(__onchange=True)
@@ -6228,7 +6258,7 @@ class BaseModel(object):
# collect values from dirty fields
result['value'] = {
name: self._fields[name].convert_to_onchange(record[name], subfields.get(name))
name: self._fields[name].convert_to_onchange(record[name], record, subfields[name])
for name in dirty
}
@@ -6392,7 +6422,7 @@ PGERROR_TO_OE = defaultdict(
'23505': convert_pgerror_23505,
})
def _normalize_ids(arg, atoms={int, long, str, unicode, NewId}):
def _normalize_ids(arg, atoms=set(IdType)):
""" Normalizes the ids argument for ``browse`` (v7 and v8) to a tuple.
Various implementations were tested on the corpus of all browse() calls