即使一个人,也要活得像军队一样!

Odoo13-- 浅谈@ormcache装饰器的使用

有时候,有些业务场景需要返回一些特定的数据。在调用后端接口时,比较频繁。不断的和数据库进行交互。
此时,我们就可以使用@ormcache装饰器装饰该接口。将相同条件的数据,存储在缓存中,下次相同的查询条件就可以直接从缓存中拿。

举栗子:

现在,我有一张表params。用来存放系统的各个模块的参数以及参数值。

1
2
3
4
5
6
7
8
class Params(models.AbstractModel):
_name = 'params'
_description = 'Params' # 系统参数表

module_id = fields.Many2one('ir.module.module', string='Module') # 模块
org_id = fields.Many2one('ir.orgs', string='Org') # 组织
key = fields.Char(string="Params Key") # 参数键
value = fields.Char(string="Params Value") # 参数值

下面我们定义一个获取参数的方法get_params

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def _get_params(self, module_id=False, key_list=False):
"""
:desc: 获取参数
:param module_id: 模块ID。
:param key_list: 参数的键 列表。
:return: 返回参数集合
"""
domain = [('module_id', '=', module_id)]
if key_list and isinstance(key_list, list):
domain.append(('key', 'in', key_list))
records = self.search(domain)
return records

def get_params(self, module_id=False, key_list=False):
"""
:desc: 获取参数
:param module_id: 模块ID。
:param key: 参数的键。
:return: 返回参数集合
"""
records = self._get_params(module_id, key_list)
return records.read()

我们正常查询参数,可以通过直接调用get_params方法获取。

1
2
3
module_records = self.env['params'].get_params(module_id=1)  # 查询模块id=1的参数
name_records = self.env['params'].get_params(module_id=1, key=['name', ]) # 查询模块id=1并且key='name'的参数
age_records = self.env['params'].get_params(module_id=1, key=['age', ]) # 查询模块id=1并且key='age'的参数

此时,我们可以正常的查询出结果。但是如果查询条件变成这样呢?

1
2
3
4
5
6
# 第一次调用
module_records = self.env['params'].get_params(module_id=1) # 查询模块id=1的参数
# 第二次调用
module_records = self.env['params'].get_params(module_id=1) # 查询模块id=1的参数
# 第三次调用
module_records = self.env['params'].get_params(module_id=1) # 查询模块id=1的参数

我们发现,相同条件,一直和数据库进行交互操作,明显的占用资源。此时,我们的ormcache就可以派上用场了,更改查询方法。

1
2
3
4
5
6
7
8
9
10
@ormcache()
def get_params(self, module_id=False, key_list=False):
"""
:desc: 获取参数
:param module_id: 模块ID。
:param key: 参数的键。
:return: 返回参数集合
"""
records = self._get_params(module_id, key_list)
return records.read()

再执行上述的操作, 打断点发现,我们的get_params方法中代码只执行了一次。这就降低了与数据库交互的频率。

但是我们发现,再执行name_records = self.env['params'].get_params(module_id=1, key=['name', ]) # 查询模块id=1并且key='name'的参数的时候,还是和上面的结果一样。原因是我们没有指定参数。

我们下面指定参数:

1
2
3
4
5
6
7
8
9
10
@ormcache('module_id')
def get_params(self, module_id=False, key_list=False):
"""
:desc: 获取参数
:param module_id: 模块ID。
:param key: 参数的键。
:return: 返回参数集合
"""
records = self._get_params(module_id, key_list)
return records.read()

再次执行

1
2
3
module_records = self.env['params'].get_params(module_id=1)  # 查询模块id=1的参数
name_records = self.env['params'].get_params(module_id=1, key=['name', ]) # 查询模块id=1并且key='name'的参数
age_records = self.env['params'].get_params(module_id=1, key=['age', ]) # 查询模块id=1并且key='age'的参数

我们发现,虽然执行了两次,但是 name_recordsage_records是一样的。 原因是我们在装饰器中指定的参数,作为缓存中存储的条件并没有发生变化

所以,我们再加一个参数。

1
2
3
4
5
6
7
8
9
10
@ormcache('module_id', 'key_list')
def get_params(self, module_id=False, key_list=False):
"""
:desc: 获取参数
:param module_id: 模块ID。
:param key: 参数的键。
:return: 返回参数集合
"""
records = self._get_params(module_id, key_list)
return records.read()

运行发现,报错了。原因是作为缓存的键不能是可变对象,和字典的键一样。
因此,我们做出改变。

1
2
3
4
5
6
7
8
9
10
@ormcache('module_id', 'str(key_list)')
def get_params(self, module_id=False, key_list=False):
"""
:desc: 获取参数
:param module_id: 模块ID。
:param key: 参数的键。
:return: 返回参数集合
"""
records = self._get_params(module_id, key_list)
return records.read()

我们再执行

1
2
3
module_records = self.env['params'].get_params(module_id=1)  # 查询模块id=1的参数
name_records = self.env['params'].get_params(module_id=1, key=['name', ]) # 查询模块id=1并且key='name'的参数
age_records = self.env['params'].get_params(module_id=1, key=['age', ]) # 查询模块id=1并且key='age'的参数

打断点发现,方法又被调用3次。但是接下来的查询,如果有相同条件的就不会再执行了。

但是,问题又来了,我参数表 修改参数、新增参数、删除参数。我们再次使用相同条件查询时候,发现我们获得的数据根本就不是我们想要的。出现了数据脏读。

原因是什么呢?
我们在对表进行修改、增加、删除操作时候,没有清除缓存导致的。

所以,我们再次做出更改。

1
2
3
4
5
6
7
8
9
10
11
12
@api.model
def create(self, vals):
self.clear_caches()
return super(Params, self).create(vals)

def write(self, vals):
self.clear_caches()
return super(Params, self).write(vals)

def unlink(self):
self.clear_caches()
return super(Params, self).unlink()

至此,就已经能够正常使用了。

下面,带大家浏览一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ormcache(object):
""" LRU cache decorator for model methods.
The parameters are strings that represent expressions referring to the
signature of the decorated method, and are used to compute a cache key::

@ormcache('model_name', 'mode')
def _compute_domain(self, model_name, mode="read"):
...

For the sake of backward compatibility, the decorator supports the named
parameter `skiparg`::

@ormcache(skiparg=1)
def _compute_domain(self, model_name, mode="read"):
...

Methods implementing this decorator should never return a Recordset,
because the underlying cursor will eventually be closed and raise a
`psycopg2.OperationalError`.
"""
def __init__(self, *args, **kwargs):
self.args = args
self.skiparg = kwargs.get('skiparg')

def __call__(self, method):
self.method = method
self.determine_key()
lookup = decorator(self.lookup, method)
lookup.clear_cache = self.clear
return lookup

def determine_key(self):
""" Determine the function that computes a cache key from arguments. """
if self.skiparg is None:
# build a string that represents function code and evaluate it
args = formatargspec(*getargspec(self.method))[1:-1]
if self.args:
code = "lambda %s: (%s,)" % (args, ", ".join(self.args))
else:
code = "lambda %s: ()" % (args,)
self.key = unsafe_eval(code)
else:
# backward-compatible function that uses self.skiparg
self.key = lambda *args, **kwargs: args[self.skiparg:]

def lru(self, model):
counter = STAT[(model.pool.db_name, model._name, self.method)]
return model.pool.cache, (model._name, self.method), counter

def lookup(self, method, *args, **kwargs):
d, key0, counter = self.lru(args[0])
key = key0 + self.key(*args, **kwargs)
try:
r = d[key]
counter.hit += 1
return r
except KeyError:
counter.miss += 1
value = d[key] = self.method(*args, **kwargs)
return value
except TypeError:
_logger.warn("cache lookup error on %r", key, exc_info=True)
counter.err += 1
return self.method(*args, **kwargs)

def clear(self, model, *args):
""" Clear the registry cache """
model.pool._clear_cache()

通过阅读源码以及相关注释, 我们了解到 @ormcache 的内部是通过 LRU (最近最少使用) 缓存机制实现的。
不知道的同学,自己去百度一下。不过有一个注意事项:

就是对于返回记录集的方法,使用改装饰器是无效的。原因是不可序列化。

以上。

-------------本文结束感谢您的阅读-------------