Use custom class to store record data

By using a class with slots instead of standard dict, we can reduce a lot the
memory usage to store the record data in the caches and values.
Also we can rely on the data in the cache to exist only for real field which
can be used to create a fast path in ModelStorage.__getattr__.
As now storing data in the cache has no extra cost, we can store data from the
transactional cache in the local. This allow also to have a shorter path in
ModelStorage.__getattr__.
For more memory efficiency, we remove the record from the cache instead of
clearing them. It was better to clear a dict but a record is less expensive to
re-instantiate than clearing.

issue9369
issue9225
review297861002
This commit is contained in:
Cédric Krier
2020-06-13 09:48:03 +02:00
parent 386347e46f
commit b3f8d125f9
15 changed files with 465 additions and 63 deletions
+1
View File
@@ -1,3 +1,4 @@
* Use custom class to store record data
* Add configuration check
* Add support for properties to fields.depends
* Allow combining authentication methods together
+11
View File
@@ -36,6 +36,17 @@ Class attributes are:
It returns a queue caller for the model. The called method will be pushed
into the queue.
.. attribute:: Model._fields
It contains a dictionary with the field name as key and its
:class:`~trytond.model.field` instance as value.
.. attribute:: Model._record
It stores the record class to store internaly the values of the instances.
.. attribute:: Model._defaults
It contains a dictionary with the field name as key and its default method
as value.
Class methods:
.. classmethod:: Model.__setup__()
+12 -3
View File
@@ -18,7 +18,7 @@ from trytond.config import config
from trytond.transaction import Transaction
from trytond.tools import resolve, grouped_slice
__all__ = ['BaseCache', 'Cache', 'LRUDict']
__all__ = ['BaseCache', 'Cache', 'LRUDict', 'LRUDictTransaction']
_clear_timeout = config.getint('cache', 'clean_timeout', default=5 * 60)
logger = logging.getLogger(__name__)
@@ -331,19 +331,28 @@ class LRUDict(OrderedDict):
"""
Dictionary with a size limit.
If size limit is reached, it will remove the first added items.
The default_factory provides the same behavior as in standard
collections.defaultdict.
"""
__slots__ = ('size_limit',)
def __init__(self, size_limit, *args, **kwargs):
def __init__(self, size_limit, default_factory=None, *args, **kwargs):
assert size_limit > 0
self.size_limit = size_limit
super(LRUDict, self).__init__(*args, **kwargs)
self.default_factory = default_factory
self._check_size_limit()
def __setitem__(self, key, value):
super(LRUDict, self).__setitem__(key, value)
self._check_size_limit()
def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
self[key] = value = self.default_factory()
return value
def update(self, *args, **kwargs):
super(LRUDict, self).update(*args, **kwargs)
self._check_size_limit()
@@ -360,7 +369,7 @@ class LRUDict(OrderedDict):
class LRUDictTransaction(LRUDict):
"""
Dictionary with a size limit. (see LRUDict)
Dictionary with a size limit and default_factory. (see LRUDict)
It is refreshed when transaction counter is changed.
"""
__slots__ = ('transaction', 'counter')
+3 -2
View File
@@ -142,7 +142,8 @@ def instanciate_values(Target, value, **extra):
kwargs = {}
ids = []
if issubclass(Target, ModelStorage):
kwargs['_local_cache'] = LRUDictTransaction(cache_size())
kwargs['_local_cache'] = LRUDictTransaction(
cache_size(), Target._record)
kwargs['_ids'] = ids
def instance(data):
@@ -338,7 +339,7 @@ class Field(object):
def __set__(self, inst, value):
assert self.name is not None
if inst._values is None:
inst._values = {}
inst._values = inst._record()
inst._values[self.name] = value
def sql_format(self, value):
+128 -3
View File
@@ -2,8 +2,11 @@
# this repository contains the full copyright notices and license terms.
import copy
import collections.abc
import sys
from collections import defaultdict
from functools import total_ordering
from itertools import chain
from trytond.i18n import lazy_gettext
from trytond.model import fields
@@ -71,6 +74,7 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
continue
if isinstance(getattr(cls, attr), fields.Field):
cls._fields[attr] = getattr(cls, attr)
cls._record = record(cls.__name__ + '._record', cls._fields.keys())
# Set _defaults
cls._defaults = {}
@@ -248,7 +252,7 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
self._id = id
self._deleted = self._removed = None
if kwargs:
self._values = {}
self._values = self._record()
parent_values = defaultdict(dict)
has_context = {}
for name, value in kwargs.items():
@@ -268,7 +272,7 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
# to ensure it was evaluated with all the fields
for name, value in has_context.items():
setattr(self, name, value)
self._init_values = self._values.copy()
self._init_values = self._values._copy()
else:
self._values = None
self._init_values = None
@@ -328,7 +332,7 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
"""
values = {}
if self._values:
for fname, value in self._values.items():
for fname, value in self._values._items():
field = self._fields[fname]
if field._type in ('many2one', 'one2one', 'reference'):
if value:
@@ -343,3 +347,124 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
value = [r.id for r in value]
values[fname] = value
return values
def record(name, field_names):
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def _getitem(self, field):
try:
return getattr(self, field)
except AttributeError:
raise KeyError(field)
def _setitem(self, field, value):
try:
return setattr(self, field, value)
except AttributeError:
raise KeyError(field)
def _contains(self, field):
try:
getattr(self, field)
return True
except AttributeError:
return False
def _clear(self):
for fname in self.__slots__:
try:
delattr(self, fname)
except AttributeError:
pass
def _copy(self):
return copy.copy(self)
def _get(self, field, default=None):
if field not in self.__slots__:
raise KeyError(field)
return getattr(self, field, default)
def _keys(self):
for fname in self.__slots__:
if hasattr(self, fname):
yield fname
def _items(self):
for fname in self.__slots__:
try:
yield fname, getattr(self, fname)
except AttributeError:
pass
_undefined = object()
def _pop(self, field, value=_undefined):
if field not in self.__slots__:
raise KeyError(field)
if value != _undefined:
value = getattr(self, field, value)
else:
try:
value = getattr(self, field)
except AttributeError:
raise KeyError(field)
try:
delattr(self, field)
except AttributeError:
pass
return value
def _popitem(self, field, value=_undefined):
return (field, self._pop(field, value=value))
def _setdefault(self, field, default=None):
try:
return getattr(self, field)
except AttributeError:
setattr(self, field, default)
return default
def _update(self, _other=None, **kwargs):
if isinstance(_other, collections.abc.Mapping):
_other = _other.items()
elif _other is None:
_other = []
chained = chain(_other, kwargs.items())
for key, value in chained:
setattr(self, key, value)
def _values(self):
for fname in self.__slots__:
try:
yield getattr(self, fname)
except AttributeError:
pass
field_names = set(field_names)
for fname in field_names:
if fname.startswith('_'):
raise ValueError(
"Field names cannot start with an underscore: %r" % name)
field_names = tuple(map(sys.intern, field_names))
type_dict = {
'__slots__': field_names,
'__init__': __init__,
'__getitem__': _getitem,
'__setitem__': _setitem,
'__contains__': _contains,
'_clear': _clear,
'_copy': _copy,
'_get': _get,
'_keys': _keys,
'_items': _items,
'_pop': _pop,
'_popitem': _popitem,
'_setdefault': _setdefault,
'_update': _update,
'_values': _values,
}
return type(name, (), type_dict)
+3 -6
View File
@@ -794,10 +794,8 @@ class ModelSQL(ModelStorage):
if getter_fields and cachable_fields:
cache = transaction.get_cache().setdefault(
cls.__name__, LRUDict(cache_size()))
cls.__name__, LRUDict(cache_size(), cls._record))
for row in result:
if row['id'] not in cache:
cache[row['id']] = {}
for fname in cachable_fields:
cache[row['id']][fname] = row[fname]
@@ -1319,7 +1317,7 @@ class ModelSQL(ModelStorage):
rows = list(cursor_dict(cursor, transaction.database.IN_MAX))
cache = transaction.get_cache()
if cls.__name__ not in cache:
cache[cls.__name__] = LRUDict(cache_size())
cache[cls.__name__] = LRUDict(cache_size(), cls._record)
delete_records = transaction.delete_records.setdefault(cls.__name__,
set())
@@ -1375,7 +1373,6 @@ class ModelSQL(ModelStorage):
keys = list(data.keys())
for k in keys[:]:
if k in ('_timestamp', '_datetime', '__id'):
keys.remove(k)
continue
field = cls._fields[k]
if not getattr(field, 'datetime_field', None):
@@ -1383,7 +1380,7 @@ class ModelSQL(ModelStorage):
continue
for k in keys:
del data[k]
cache[cls.__name__].setdefault(data['id'], {}).update(data)
cache[cls.__name__][data['id']]._update(data)
if len(rows) >= transaction.database.IN_MAX:
if (cls._history
+39 -36
View File
@@ -211,16 +211,14 @@ class ModelStorage(Model):
# Clean local cache
for record in all_records:
local_cache = record._local_cache.get(record.id)
if local_cache:
local_cache.clear()
record._local_cache.pop(record.id, None)
# Clean transaction cache
for cache in Transaction().cache.values():
if cls.__name__ in cache:
cache_cls = cache[cls.__name__]
for record in all_records:
if record.id in cache[cls.__name__]:
cache[cls.__name__][record.id].clear()
cache_cls.pop(record.id, None)
@classmethod
@without_check_access
@@ -597,7 +595,7 @@ class ModelStorage(Model):
'''
transaction = Transaction()
ids = list(map(int, ids))
local_cache = LRUDictTransaction(cache_size())
local_cache = LRUDictTransaction(cache_size(), cls._record)
transaction_cache = transaction.get_cache()
return [cls(x, _ids=ids,
_local_cache=local_cache,
@@ -1385,7 +1383,7 @@ class ModelStorage(Model):
assert isinstance(_local_cache, LRUDictTransaction)
self._local_cache = _local_cache
else:
self._local_cache = LRUDictTransaction(cache_size())
self._local_cache = LRUDictTransaction(cache_size(), self._record)
super(ModelStorage, self).__init__(id, **kwargs)
@@ -1393,31 +1391,36 @@ class ModelStorage(Model):
def _cache(self):
cache = self._transaction_cache
if self.__name__ not in cache:
cache[self.__name__] = LRUDict(cache_size())
cache[self.__name__] = LRUDict(cache_size(), self._record)
return cache[self.__name__]
def __getattr__(self, name):
try:
return super(ModelStorage, self).__getattr__(name)
except AttributeError:
if self.id is None or self.id < 0:
if self.id is None or self.id < 0 or name.startswith('_'):
raise
self._local_cache.refresh()
try:
return self._local_cache[self.id][name]
except KeyError:
pass
# fetch the definition of the field
try:
field = self._fields[name]
except KeyError:
raise AttributeError('"%s" has no attribute "%s"' % (self, name))
try:
return self._local_cache[self.id][name]
except KeyError:
pass
try:
if field._type not in ('many2one', 'reference'):
return self._cache[self.id][name]
# fill local cache for quicker access later
value \
= self._local_cache[self.id][name] \
= self._cache[self.id][name]
return value
else:
skip_eager = name in self._cache[self.id]
except KeyError:
@@ -1437,8 +1440,10 @@ class ModelStorage(Model):
def not_cached(item):
fname, field = item
return (fname not in self._cache.get(self.id, {})
and fname not in self._local_cache.get(self.id, {}))
return ((self.id not in self._cache
or fname not in self._cache[self.id])
and (self.id not in self._local_cache
or fname not in self._local_cache[self.id]))
def to_load(item):
fname, field = item
@@ -1477,8 +1482,11 @@ class ModelStorage(Model):
def filter_(id_):
return (id_ == self.id # Ensure the value is read
or (name not in self._cache.get(id_, {})
and name not in self._local_cache.get(id_, {})))
or ((
id_ not in self._cache
or name not in self._cache[id_])
and (id_ not in self._local_cache
or name not in self._local_cache[id_])))
def unique(ids):
s = set()
@@ -1527,7 +1535,7 @@ class ModelStorage(Model):
kwargs = {}
key = (Model, freeze(ctx))
kwargs['_local_cache'] = model2cache.setdefault(key,
LRUDictTransaction(cache_size()))
LRUDictTransaction(cache_size(), Model._record))
kwargs['_ids'] = ids = model2ids.setdefault(key, [])
kwargs['_transaction_cache'] = transaction.get_cache()
kwargs['_transaction'] = transaction
@@ -1560,28 +1568,23 @@ class ModelStorage(Model):
read_data = self.read(list(ids), list(ffields.keys()))
# create browse records for 'remote' models
for data in read_data:
id_ = data['id']
for fname, field in ffields.items():
fvalue = data[fname]
if field._type in ('many2one', 'one2one', 'one2many',
'many2many', 'reference'):
if field._type in {
'many2one', 'one2one', 'one2many', 'many2many',
'reference'}:
fvalue = instantiate(field, data[fname], data)
if data['id'] == self.id and fname == name:
if id_ == self.id and fname == name:
value = fvalue
if (field._type not in ('many2one', 'one2one', 'one2many',
'many2many', 'reference', 'binary')
and not isinstance(field, fields.Function)):
continue
if data['id'] not in self._local_cache:
self._local_cache[data['id']] = {}
self._local_cache[data['id']][fname] = fvalue
if (field._type not in ('many2one', 'reference')
self._local_cache[id_][fname] = fvalue
if (field._type in {
'one2one', 'one2many', 'many2many', 'binary'}
or field.context
or getattr(field, 'datetime_field', None)
or isinstance(field, fields.Function)):
del data[fname]
if data['id'] not in self._cache:
self._cache[data['id']] = {}
self._cache[data['id']].update(data)
self._cache[id_]._update(data)
return value
@property
@@ -1589,7 +1592,7 @@ class ModelStorage(Model):
values = {}
if not self._values:
return values
for fname, value in self._values.items():
for fname, value in self._values._items():
field = self._fields[fname]
if isinstance(field, fields.Function) and not field.setter:
continue
@@ -1619,9 +1622,9 @@ class ModelStorage(Model):
if (field._type == 'one2many'
and field.field
and target._values):
t_values = target._values.copy()
t_values = target._values._copy()
# Don't look at reverse field
target._values.pop(field.field, None)
target._values._pop(field.field, None)
else:
t_values = None
try:
+5 -5
View File
@@ -795,10 +795,10 @@ class ModelView(Model):
"""
from .modelstorage import ModelStorage
changed = {}
init_values = self._init_values or {}
init_values = self._init_values or self._record()
if not self._values:
return changed
for fname, value in self._values.items():
for fname, value in self._values._items():
field = self._fields[fname]
# Always test key presence in case value is None
if (fname in init_values
@@ -820,7 +820,7 @@ class ModelView(Model):
value = value.id
elif field._type in ['one2many', 'many2many']:
targets = value
init_targets = list(init_values.get(
init_targets = list(init_values._get(
fname, targets if field._type == 'one2many' else []))
value = collections.defaultdict(list)
previous = [t.id for t in init_targets if t.id]
@@ -828,9 +828,9 @@ class ModelView(Model):
if (field._type == 'one2many'
and field.field
and target._values):
t_values = target._values.copy()
t_values = target._values._copy()
# Don't look at reverse field
target._values.pop(field.field, None)
target._values._pop(field.field, None)
else:
t_values = None
try:
+2
View File
@@ -274,6 +274,8 @@ def load_module_graph(graph, pool, update=None, lang=None):
# Avoid clearing cache to prevent dead lock on ir.cache table
Cache.rollback(transaction)
transaction.commit()
# Clear transaction cache to update default_factory
transaction.cache.clear()
if not update:
pool.setup()
+1 -2
View File
@@ -384,8 +384,7 @@ class User(DeactivableMixin, ModelSQL, ModelView):
for cache in Transaction().cache.values():
if cls.__name__ in cache:
for user in all_users:
if user.id in cache[cls.__name__]:
cache[cls.__name__][user.id].clear()
cache[cls.__name__].pop(user.id, None)
# Restart the cache for domain_get method
pool = Pool()
pool.get('ir.rule')._domain_get_cache.clear()
+7
View File
@@ -76,6 +76,12 @@ class ModelSQLOne2ManyTarget(ModelSQL):
origin = fields.Many2One('test.modelsql.one2many', "Origin")
class ModelSQLSearch(ModelSQL):
"ModelSQL Search"
__name__ = 'test.modelsql.search'
name = fields.Char("Name")
class NullOrder(ModelSQL):
"Null Order"
__name__ = 'test.modelsql.null_order'
@@ -153,6 +159,7 @@ def register(module):
ModelSQLFieldSet,
ModelSQLOne2Many,
ModelSQLOne2ManyTarget,
ModelSQLSearch,
NullOrder,
ModelTranslation,
ModelCheck,
+84 -2
View File
@@ -5,7 +5,7 @@ import time
import unittest
from trytond import backend, cache as cache_mod
from trytond.cache import freeze, MemoryCache
from trytond.cache import freeze, MemoryCache, LRUDict, LRUDictTransaction
from trytond.tests.test_tryton import with_transaction, activate_module
from trytond.tests.test_tryton import DB_NAME, USER
from trytond.transaction import Transaction
@@ -175,12 +175,94 @@ class MemoryCacheChannelTestCase(MemoryCacheTestCase):
super().test_memory_cache_sync()
class LRUDictTestCase(unittest.TestCase):
"Test LRUDict"
def test_setitem(self):
lru_dict = LRUDict(1)
lru_dict['foo'] = 'foo'
self.assertEqual(len(lru_dict), 1)
lru_dict['bar'] = 'bar'
self.assertEqual(len(lru_dict), 1)
self.assertEqual(lru_dict, {'bar': 'bar'})
def test_update(self):
lru_dict = LRUDict(1)
lru_dict['foo'] = 'foo'
self.assertEqual(len(lru_dict), 1)
lru_dict.update(bar='bar', baz='baz')
self.assertEqual(len(lru_dict), 1)
self.assertEqual(lru_dict, {'baz': 'baz'})
def test_setdefault(self):
lru_dict = LRUDict(1)
lru_dict['foo'] = 'foo'
self.assertEqual(len(lru_dict), 1)
lru_dict.setdefault('bar', 'value')
self.assertEqual(len(lru_dict), 1)
self.assertEqual(lru_dict, {'bar': 'value'})
def test_default_factory(self):
lru_dict = LRUDict(1, default_factory=list)
self.assertEqual(lru_dict['foo'], [])
lru_dict['bar'].append('bar')
self.assertEqual(lru_dict, {'bar': ['bar']})
class LRUDictTransactionTestCase(unittest.TestCase):
"Test LRUDictTransaction"
@with_transaction()
def test_init(self):
"Test init set to transaction counter"
lru_dict = LRUDictTransaction(48)
self.assertEqual(lru_dict.counter, Transaction().counter)
@with_transaction()
def test_clear(self):
"Test clear reset counter"
lru_dict = LRUDictTransaction(48)
Transaction().counter += 1
lru_dict.clear()
self.assertEqual(lru_dict.counter, Transaction().counter)
@with_transaction()
def test_refresh(self):
"Test refresh"
lru_dict = LRUDictTransaction(48)
lru_dict['foo'] = 'foo'
lru_dict.refresh()
self.assertEqual(lru_dict, {'foo': 'foo'})
Transaction().counter += 1
lru_dict.refresh()
self.assertEqual(lru_dict, {})
self.assertEqual(lru_dict.counter, Transaction().counter)
def suite():
func = unittest.TestLoader().loadTestsFromTestCase
suite = unittest.TestSuite()
for testcase in [
CacheTestCase,
MemoryCacheTestCase,
MemoryCacheChannelTestCase]:
MemoryCacheChannelTestCase,
LRUDictTestCase,
LRUDictTransactionTestCase,
]:
suite.addTests(func(testcase))
return suite
+154 -3
View File
@@ -3,6 +3,7 @@
import unittest
from trytond.model.model import record
from trytond.pool import Pool
from trytond.tests.test_tryton import activate_module, with_transaction
from trytond.transaction import Transaction
@@ -219,9 +220,159 @@ class ModelTranslationTestCase(unittest.TestCase):
self.assertEqual(other['string'], "Nom")
class RecordTestCase(unittest.TestCase):
"Test record"
def test_creation(self):
"Test record creation"
Record = record('model', ['foo', 'bar'])
self.assertEqual(Record.__name__, 'model')
self.assertEqual(set(Record.__slots__), {'foo', 'bar'})
def test_init(self):
Record = record('model', ['foo', 'bar'])
rec = Record(foo='foo')
self.assertEqual(rec.foo, 'foo')
def test_getitem(self):
Record = record('model', ['foo', 'bar'])
rec = Record(foo='foo')
self.assertEqual(rec['foo'], 'foo')
def test_setitem(self):
Record = record('model', ['foo', 'bar'])
rec = Record(foo='foo')
rec['foo'] = 'foo'
self.assertEqual(rec['foo'], 'foo')
def test_contains(self):
"Test record contains"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='foo')
self.assertTrue('foo' in rec)
self.assertFalse('bar' in rec)
def test_clear(self):
"Test record clear"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='foo', bar='bar')
rec._clear()
with self.assertRaises(AttributeError):
rec.foo
with self.assertRaises(AttributeError):
rec.bar
def test_copy(self):
"Test copy record"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='baz')
copy = rec._copy()
self.assertEqual(copy.foo, 'baz')
self.assertNotEqual(rec, copy)
self.assertIsInstance(copy, Record)
def test_get(self):
"Test get field from record"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='baz')
self.assertEqual(rec._get('foo'), 'baz')
self.assertEqual(rec._get('bar'), None)
self.assertEqual(rec._get('bar', 42), 42)
with self.assertRaises(KeyError):
rec._get('baz')
def test_keys(self):
"Test record keys"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='baz')
self.assertEqual(list(rec._keys()), ['foo'])
def test_items(self):
"Test record items"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='baz')
self.assertEqual(list(rec._items()), [('foo', 'baz')])
def test_pop(self):
"Test pop field from record"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='baz')
value = rec._pop('foo')
self.assertEqual(value, 'baz')
with self.assertRaises(AttributeError):
rec.foo
with self.assertRaises(KeyError):
rec._pop('baz')
def test_popitem(self):
"Test popitem field from record"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='baz')
value = rec._popitem('foo')
self.assertEqual(value, ('foo', 'baz'))
def test_setdefault(self):
"Test set default field on record"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='foo')
self.assertEqual(rec._setdefault('bar', 'bar'), 'bar')
self.assertEqual(rec.bar, 'bar')
self.assertEqual(rec._setdefault('foo', 'baz'), 'foo')
self.assertEqual(rec.foo, 'foo')
def test_update(self):
"Test update record"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='foo')
rec._update(foo='bar', bar='baz')
self.assertEqual(rec.foo, 'bar')
self.assertEqual(rec.bar, 'baz')
def test_values(self):
"Test record values"
Record = record('model', ['foo', 'bar'])
rec = Record(foo='baz')
self.assertEqual(list(rec._values()), [('baz')])
def suite():
func = unittest.TestLoader().loadTestsFromTestCase
suite_ = unittest.TestSuite()
suite_.addTests(unittest.TestLoader().loadTestsFromTestCase(ModelTestCase))
suite_.addTests(unittest.TestLoader().loadTestsFromTestCase(
ModelTranslationTestCase))
for testcase in [
ModelTestCase,
ModelTranslationTestCase,
RecordTestCase,
]:
suite_.addTests(func(testcase))
return suite_
+14
View File
@@ -742,6 +742,20 @@ class ModelSQLTranslationTestCase(unittest.TestCase):
self.assertEqual(record.name, "Foo")
self.assertEqual(other.name, "Baz")
@with_transaction()
def test_search_fill_transaction_cache(self):
"Test search fill the transaction cache"
pool = Pool()
Model = pool.get('test.modelsql.search')
Model.create([{'name': "Foo"}])
record, = Model.search([])
cache = Transaction().get_cache()[Model.__name__]
self.assertIn(record.id, cache)
self.assertEqual(cache[record.id]['name'], "Foo")
self.assertNotIn('_timestamp', cache[record.id])
def suite():
suite_ = unittest.TestSuite()
+1 -1
View File
@@ -82,7 +82,7 @@ class ModelView(unittest.TestCase):
# no initial value
record = Model()
record._values = {}
record._values = record._record()
target = Target(id=1)
record._values['targets'] = [target]
target.name = 'foo'