Skip to content

Commit

Permalink
[FIX] models: make read_group() call name_get() on demand only
Browse files Browse the repository at this point in the history
This improves the performance of code using read_group() without actually using
the display names of the result.  It also optimizes name_search().
  • Loading branch information
rco-odoo committed Jul 24, 2018
1 parent 26fbf93 commit fd9de4d
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 5 deletions.
40 changes: 40 additions & 0 deletions odoo/addons/test_performance/tests/test_performance.py
@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import defaultdict

from odoo.tests.common import TransactionCase, users, warmup
from odoo.tools import pycompat


class TestPerformance(TransactionCase):
Expand Down Expand Up @@ -98,3 +101,40 @@ def test_several_prefetch(self):
'delete from test_performance_base where id not in %s',
(tuple(initial_records.ids),)
)

def expected_read_group(self):
groups = defaultdict(list)
for record in self.env['test_performance.base'].search([]):
groups[record.partner_id.id].append(record.value)
partners = self.env['res.partner'].search([('id', 'in', list(groups))])
return [{
'__domain': [('partner_id', '=', partner.id)],
'partner_id': (partner.id, partner.display_name),
'partner_id_count': len(groups[partner.id]),
'value': sum(groups[partner.id]),
} for partner in partners]

@users('admin', 'demo')
def test_read_group_with_name_get(self):
model = self.env['test_performance.base']
expected = self.expected_read_group()
# use read_group and check the expected result
with self.assertQueryCount(admin=2, demo=2):
model.invalidate_cache()
result = model.read_group([], ['partner_id', 'value'], ['partner_id'])
self.assertEqual(result, expected)

@users('admin', 'demo')
def test_read_group_without_name_get(self):
model = self.env['test_performance.base']
expected = self.expected_read_group()
# use read_group and check the expected result
with self.assertQueryCount(admin=1, demo=1):
model.invalidate_cache()
result = model.read_group([], ['partner_id', 'value'], ['partner_id'])
self.assertEqual(len(result), len(expected))
for res, exp in pycompat.izip(result, expected):
self.assertEqual(res['__domain'], exp['__domain'])
self.assertEqual(res['partner_id'][0], exp['partner_id'][0])
self.assertEqual(res['partner_id_count'], exp['partner_id_count'])
self.assertEqual(res['value'], exp['value'])
13 changes: 10 additions & 3 deletions odoo/models.py
Expand Up @@ -1583,7 +1583,7 @@ def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get
access_rights_uid = name_get_uid or self._uid
ids = self._search(args, limit=limit, access_rights_uid=access_rights_uid)
recs = self.browse(ids)
return recs.sudo(access_rights_uid).name_get()
return lazy_name_get(recs.sudo(access_rights_uid))

@api.model
def _add_missing_default_values(self, values):
Expand Down Expand Up @@ -1654,7 +1654,7 @@ def _read_group_fill_results(self, domain, groupby, remaining_groupbys,
order = tools.reverse_order(order)
groups = getattr(self, field.group_expand)(groups, domain, order)
groups = groups.sudo()
values = groups.name_get()
values = lazy_name_get(groups)
value2key = lambda value: value and value[0]

else:
Expand Down Expand Up @@ -2061,7 +2061,7 @@ def _read_group_resolve_many2one_fields(self, data, fields):
for field in many2onefields:
ids_set = {d[field] for d in data if d[field]}
m2o_records = self.env[self._fields[field].comodel_name].browse(ids_set)
data_dict = dict(m2o_records.sudo().name_get())
data_dict = dict(lazy_name_get(m2o_records.sudo()))
for d in data:
d[field] = (d[field], data_dict[d[field]]) if d[field] else False

Expand Down Expand Up @@ -5252,6 +5252,13 @@ def _normalize_ids(arg, atoms=set(IdType)):

return tuple(arg)


def lazy_name_get(self):
""" Evaluate self.name_get() lazily. """
names = tools.lazy(lambda: dict(self.name_get()))
return [(rid, tools.lazy(operator.getitem, names, rid)) for rid in self.ids]


# keep those imports here to avoid dependency cycle errors
from .osv import expression
from .fields import Field
133 changes: 131 additions & 2 deletions odoo/tools/func.py
Expand Up @@ -2,9 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

__all__ = ['synchronized', 'lazy_classproperty', 'lazy_property',
'classproperty', 'conditional']
'classproperty', 'conditional', 'lazy']

from functools import wraps
from functools import wraps, partial
from inspect import getsourcefile


Expand Down Expand Up @@ -112,3 +112,132 @@ def __get__(self, cls, owner):

def classproperty(func):
return _ClassProperty(classmethod(func))


class lazy(object):
""" A proxy to the (memoized) result of a lazy evaluation::
foo = lazy(func, arg) # func(arg) is not called yet
bar = foo + 1 # eval func(arg) and add 1
baz = foo + 2 # use result of func(arg) and add 2
"""
__slots__ = ['_func', '_value']

def __init__(self, func, *args, **kwargs):
self._func = partial(func, *args, **kwargs)

def __getattr__(self, name):
if name == '_value':
self._value = value = self._func()
self._func = None
return value
return getattr(self._value, name)

def __setattr__(self, name, value):
if name in ('_func', '_value'):
return object.__setattr__(self, name, value)
return setattr(self._value, name, value)

def __delattr__(self, name):
if name in ('_func', '_value'):
return object.__delattr__(self, name)
return delattr(self._value, name)

def __repr__(self):
return repr(self._value) if self._func is None else object.__repr__(self)
def __str__(self): return str(self._value)
def __bytes__(self): return bytes(self._value)
def __format__(self, format_spec): return format(self._value, format_spec)

def __lt__(self, other): return self._value < other
def __le__(self, other): return self._value <= other
def __eq__(self, other): return self._value == other
def __ne__(self, other): return self._value != other
def __gt__(self, other): return self._value > other
def __ge__(self, other): return self._value >= other

def __hash__(self): return hash(self._value)
def __bool__(self): return bool(self._value)

def __call__(self, *args, **kwargs): return self._value(*args, **kwargs)

def __len__(self): return len(self._value)
def __getitem__(self, key): return self._value[key]
def __missing__(self, key): return self._value.__missing__(key)
def __setitem__(self, key, value): self._value[key] = value
def __delitem__(self, key): del self._value[key]
def __iter__(self): return iter(self._value)
def __reversed__(self): return reversed(self._value)
def __contains__(self, key): return key in self._value

def __add__(self, other): return self._value.__add__(other)
def __sub__(self, other): return self._value.__sub__(other)
def __mul__(self, other): return self._value.__mul__(other)
def __matmul__(self, other): return self._value.__matmul__(other)
def __truediv__(self, other): return self._value.__truediv__(other)
def __floordiv__(self, other): return self._value.__floordiv__(other)
def __mod__(self, other): return self._value.__mod__(other)
def __divmod__(self, other): return self._value.__divmod__(other)
def __pow__(self, other): return self._value.__pow__(other)
def __lshift__(self, other): return self._value.__lshift__(other)
def __rshift__(self, other): return self._value.__rshift__(other)
def __and__(self, other): return self._value.__and__(other)
def __xor__(self, other): return self._value.__xor__(other)
def __or__(self, other): return self._value.__or__(other)

def __radd__(self, other): return self._value.__radd__(other)
def __rsub__(self, other): return self._value.__rsub__(other)
def __rmul__(self, other): return self._value.__rmul__(other)
def __rmatmul__(self, other): return self._value.__rmatmul__(other)
def __rtruediv__(self, other): return self._value.__rtruediv__(other)
def __rfloordiv__(self, other): return self._value.__rfloordiv__(other)
def __rmod__(self, other): return self._value.__rmod__(other)
def __rdivmod__(self, other): return self._value.__rdivmod__(other)
def __rpow__(self, other): return self._value.__rpow__(other)
def __rlshift__(self, other): return self._value.__rlshift__(other)
def __rrshift__(self, other): return self._value.__rrshift__(other)
def __rand__(self, other): return self._value.__rand__(other)
def __rxor__(self, other): return self._value.__rxor__(other)
def __ror__(self, other): return self._value.__ror__(other)

def __iadd__(self, other): return self._value.__iadd__(other)
def __isub__(self, other): return self._value.__isub__(other)
def __imul__(self, other): return self._value.__imul__(other)
def __imatmul__(self, other): return self._value.__imatmul__(other)
def __itruediv__(self, other): return self._value.__itruediv__(other)
def __ifloordiv__(self, other): return self._value.__ifloordiv__(other)
def __imod__(self, other): return self._value.__imod__(other)
def __ipow__(self, other): return self._value.__ipow__(other)
def __ilshift__(self, other): return self._value.__ilshift__(other)
def __irshift__(self, other): return self._value.__irshift__(other)
def __iand__(self, other): return self._value.__iand__(other)
def __ixor__(self, other): return self._value.__ixor__(other)
def __ior__(self, other): return self._value.__ior__(other)

def __neg__(self): return self._value.__neg__()
def __pos__(self): return self._value.__pos__()
def __abs__(self): return self._value.__abs__()
def __invert__(self): return self._value.__invert__()

def __complex__(self): return complex(self._value)
def __int__(self): return int(self._value)
def __float__(self): return float(self._value)

def __index__(self): return self._value.__index__()

def __round__(self): return self._value.__round__()
def __trunc__(self): return self._value.__trunc__()
def __floor__(self): return self._value.__floor__()
def __ceil__(self): return self._value.__ceil__()

def __enter__(self): return self._value.__enter__()
def __exit__(self, exc_type, exc_value, traceback):
return self._value.__exit__(exc_type, exc_value, traceback)

def __await__(self): return self._value.__await__()
def __aiter__(self): return self._value.__aiter__()
def __anext__(self): return self._value.__anext__()
def __aenter__(self): return self._value.__aenter__()
def __aexit__(self, exc_type, exc_value, traceback):
return self._value.__aexit__(exc_type, exc_value, traceback)

0 comments on commit fd9de4d

Please sign in to comment.