Cache Function field getter without context in readonly transactions

issue10936
review367781002
This commit is contained in:
Cédric Krier
2021-11-28 13:27:52 +01:00
parent a4a2ad0c9d
commit a00d26dd3c
7 changed files with 111 additions and 7 deletions
+1
View File
@@ -1,3 +1,4 @@
* Cache Function field getter without context in readonly transactions
* Use expire delay instead of UTC datetime in reset password email
* Use RENAME and DROP COLUMN with SQLite backend
* Support window functions with SQLite backend
+9 -1
View File
@@ -961,7 +961,7 @@ Instance methods:
Function
--------
.. class:: Function(field, getter[, setter[, searcher]])
.. class:: Function(field, getter[, setter[, searcher[, getter_with_context]]])
A function field can emulate any other given ``field``.
@@ -1019,6 +1019,14 @@ A function field can emulate any other given ``field``.
It must return a list of :ref:`domain <topics-domain>` clauses but the
``operand`` can be a SQL query.
.. attribute:: Function.getter_with_context
A boolean telling if the getter result depends on the context.
If it does not depend, the getter is called without context and the result
is stored in the transaction cache when readonly.
The default value is ``True``.
Instance methods:
.. method:: Function.get(ids, model, name[, values])
+26 -3
View File
@@ -3,6 +3,7 @@
import inspect
import copy
from functools import wraps
from trytond.i18n import gettext
from trytond.model.fields.field import Field
@@ -10,13 +11,29 @@ from trytond.tools import is_instance_method
from trytond.transaction import Transaction
def getter_context(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if not self.getter_with_context:
transaction = Transaction()
context = {
k: v for k, v in transaction.context.items()
if k in transaction.cache_keys}
with transaction.reset_context(), \
transaction.set_context(context):
return func(self, *args, **kwargs)
else:
return func(self, *args, **kwargs)
return wrapper
class Function(Field):
'''
Define function field (any).
'''
def __init__(self, field, getter, setter=None, searcher=None,
loading='lazy'):
getter_with_context=True, loading='lazy'):
'''
:param field: The field of the function.
:param getter: The name of the function for getting values.
@@ -29,6 +46,7 @@ class Function(Field):
self._field = field
self._type = field._type
self.getter = getter
self.getter_with_context = getter_with_context
self.setter = setter
if not self.setter:
self._field.readonly = True
@@ -41,11 +59,15 @@ class Function(Field):
def __copy__(self):
return Function(copy.copy(self._field), self.getter,
setter=self.setter, searcher=self.searcher, loading=self.loading)
setter=self.setter, searcher=self.searcher,
getter_with_context=self.getter_with_context,
loading=self.loading)
def __deepcopy__(self, memo):
return Function(copy.deepcopy(self._field, memo), self.getter,
setter=self.setter, searcher=self.searcher, loading=self.loading)
setter=self.setter, searcher=self.searcher,
getter_with_context=self.getter_with_context,
loading=self.loading)
def __getattr__(self, name):
return getattr(self._field, name)
@@ -81,6 +103,7 @@ class Function(Field):
'ir.msg_search_function_missing',
**Model.__names__(self.name)))
@getter_context
def get(self, ids, Model, name, values=None):
'''
Call the getter.
+7 -2
View File
@@ -845,7 +845,9 @@ class ModelSQL(ModelStorage):
for fname in getter_fields:
field = cls._fields[fname]
if isinstance(field, fields.Function):
key = (field.getter, getattr(field, 'datetime_field', None))
key = (
field.getter, field.getter_with_context,
getattr(field, 'datetime_field', None))
func_fields.setdefault(key, [])
func_fields[key].append(fname)
elif getattr(field, 'datetime_field', None):
@@ -865,7 +867,7 @@ class ModelSQL(ModelStorage):
field_list = func_fields[key]
fname = field_list[0]
field = cls._fields[fname]
_, datetime_field = key
_, getter_with_context, datetime_field = key
if datetime_field:
for row in result:
with Transaction().set_context(
@@ -895,6 +897,9 @@ class ModelSQL(ModelStorage):
for row in sub_results:
if row['id'] in sub_ids:
row[fname] = getter_result[row['id']]
if (transaction.readonly
and not getter_with_context):
cache[row['id']][fname] = row[fname]
def read_related(field, Target, rows, fields):
name = field.name
+3 -1
View File
@@ -1720,7 +1720,9 @@ class ModelStorage(Model):
if (field._type in no_local_cache
or field.context
or getattr(field, 'datetime_field', None)
or isinstance(field, fields.Function)):
or (isinstance(field, fields.Function)
and (not transaction.readonly
or field.getter_with_context))):
to_delete.add(fname)
self._cache[id_]._update(
**{k: v for k, v in data.items() if k not in to_delete})
+19
View File
@@ -3,6 +3,7 @@
from trytond.model import ModelSQL, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
class FunctionAccessor(ModelSQL):
@@ -25,8 +26,26 @@ class FunctionAccessorTarget(ModelSQL):
__name__ = 'test.function.accessor.target'
class FunctionGetterContext(ModelSQL):
"Function Getter Context"
__name__ = 'test.function.getter_context'
function_with_context = fields.Function(
fields.Char("Function"),
'getter', getter_with_context=True)
function_without_context = fields.Function(
fields.Char("Function"),
'getter', getter_with_context=False)
def getter(self, name):
context = Transaction().context
return '%s - %s' % (
context.get('language', 'empty'), context.get('test', 'empty'))
def register(module):
Pool.register(
FunctionAccessor,
FunctionAccessorTarget,
FunctionGetterContext,
module=module, type_='model')
+46
View File
@@ -1,9 +1,11 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import unittest
from unittest.mock import patch
from trytond.pool import Pool
from trytond.tests.test_tryton import activate_module, with_transaction
from trytond.transaction import Transaction
class FieldFunctionTestCase(unittest.TestCase):
@@ -27,6 +29,50 @@ class FieldFunctionTestCase(unittest.TestCase):
self.assertEqual(record.function, target)
@with_transaction()
def test_getter_context(self):
"Test getter context"
pool = Pool()
Model = pool.get('test.function.getter_context')
record = Model()
record.save()
with Transaction().set_context(language='en', test='foo'):
record = Model(record.id)
self.assertEqual(record.function_with_context, 'en - foo')
self.assertEqual(record.function_without_context, 'en - empty')
@with_transaction()
def test_getter_cached_readonly_transaction(self):
"Test getter cached in read only transaction"
pool = Pool()
Model = pool.get('test.function.getter_context')
with Transaction().new_transaction(readonly=True), \
patch.object(Model, 'getter') as getter:
record = Model()
record.save()
record.function_with_context
record.function_without_context
self.assertEqual(getter.call_count, 2)
getter.reset_mock()
with Transaction().set_context(test='foo'):
record = Model(record.id)
record.function_with_context
record.function_without_context
self.assertEqual(getter.call_count, 1)
getter.reset_mock()
# Change transaction key context
with Transaction().set_context(language='en'):
record = Model(record.id)
record.function_with_context
record.function_without_context
self.assertEqual(getter.call_count, 2)
def suite():
return unittest.TestLoader().loadTestsFromTestCase(FieldFunctionTestCase)