第八章 嵌套集模型
问题的起源
接触过odoo一段时间后的同学肯定会注意到,odoo种有这么一种数据库结构的存在,典型的有产品分类(product.template)、库位(stock.location)和业务伙伴(res.partner)。这几种数据对象的共性就是拥有上下级式的结构。
写过后端业务代码的同学肯定熟悉,我们在处理这种遗传式的结构时通常采用的一种解决方案就是添加一个parent字段,用于指明该记录的父记录是哪一个。这样做的好处就是很容易查询到记录的上级记录,追溯祖先记录也很方便,递归查找即可。但是这种方式的弊端就是在查找子记录的时候,我们很容易查询下一级的子记录,但是查找孙记录却只能递归子记录,试想如果这个层级如果很多,那么肯定会出现性能问题。
parent_left和parent_right就是为了解决这个问题而产生的一种思路,学名嵌套集模型,是由SQL领域的大神Joe Celko发明的。下面来简单介绍一下嵌套集模型的概念。
嵌套集模型
假设我们有两个节点A和B,A是B的父节点,那么嵌套模型的定义如下:
A.parent_left < B.parent_left, B.parent_right < A.parent_right
也就是说,A的parent_left和parent_right要把B完全包含在内,才算是A的子节点。
按照这种说法,假设我们有A,B,C,D,E五个节点,层级结构如下:
因此 A(1,10) B(2,5),C(3,4),D(6,9),E(7,8)
这样的好处就是,查找节点A的所有子孙节点,一句简单的sql就可以完成。
select * from table where parent_left >1 and parent_right < 10;
避免了由于递归查询造成的性能损耗。
odoo中的集成
12.0 版本之前,odoo对于嵌套模型的实现是通过parent_left和parent_right字段实现的,关于这点可以参考odoo11版本中的product.category和stock.location模型。
从12.0版本开始,odoo抛弃了之前的实现,由parent_path取代了parent_left和parent_right。
parent_path中存储的是当前节点到根节点的路径,以/分割。典型的一个数据结构示例如下:
这是一个更为高效地parent_of/child_of的实现。
我们要定义一个实现了嵌套集模型的模型,首先要在模型的类属性中指定_parent_name字段,并且将_parent_store设置为True,以标识将该字段存储到数据库中以提高筛选效率。
class DemoClass(models.Model):
_name = 'demo.class'
_parent_name = "parent_id"
_parent_store = True
...
然后定义三个字段,parent_id、parent_path和child_id,parent_id的字段名称可以自己定义,但是要在_parent_name中标明,parent_path和child_id不可以变更。
parent_id = fields.Many2one('demo.class', string="父级分类")
parent_path = fields.Char(index=True)
child_id = fields.One2many(
"demo.class", "parent_id", string="子分类")
parent_path字段最好添加索引以加快查询速度。
这样就完成了上下级分类模型的建立,安装模块后可以在数据库中查看到这些字段的作用。
使用上下级名称
我们在odoo系统中碰到使用上下级关系的模型的显示方式为: A/B/C...,现在我们来看一下是如何实现的。
complete_name = fields.Char(
'Complete Name', compute='_compute_complete_name', recursive=True,
store=True)
首先在模型中新建一个complate_name字段,然后将其属性设置为recursive=True, store=True 存储起来的目的是为了加快搜索速度。
_rec_name = 'complete_name'
同时将其_rece_name属性设置为complete_name。
最后我们看一下_compute_complete_name如何实现的:
@api.depends('name', 'parent_id.complete_name')
def _compute_complete_name(self):
for category in self:
if category.parent_id:
category.complete_name = '%s / %s' % (category.parent_id.complete_name, category.name)
else:
category.complete_name = category.name