在Odoo中有很多层级关系的数据,比如:产品的分类,多层级的BOM,项目中的任务及其子任务等等。在数据库中存储这些有层级关系的数据,并能实现高效的查询是一个蛮有挑战的任务。
Odoo一直以来采用的方法称为:MPTT (Modified Preorder Tree Traversal)。 比如对于下面层级树,在数据库中每个节点都包含了parent_left
和parent_right
信息:
root node|parent_left|parent_right / \ root|1 |20 a d a |2 |7 / \ / \ b |3 |4 b c e f c |5 |6 \ d |8 |19 g e |9 |10 / \ f |11 |18 h i g |12 |17 h |13 |14 i |15 |16
这样的存储结构对于查询某节点的子孙节点或是祖先节点很方便。比如想要找出所有d
的子节点,只要查询parent_left
, parent_right
位于8和19之间的节点就可以了。即:SELECT * FROM table WHERE lft BETWEEN 8 AND 19;
。效率非常高。
但是这样的数据结构对于插入,删除,移动节点等的操作是很低效的,要重算其他部分或全部节点的parent_left, parent_right值。具体算法请参照同事的博文:ODOO优化层级关系查询效率的方法
Odoo最近的这个提交意味着Odoo V12中将不再采用MPTT的算法来处理层级数据了,而是引入了一种新的算法:Materialized Path
这种新算法的原理很简单,就是在层级数据中增加一个字段:parent_path
,在这个字段中记录层级路径:
a node | id | parent_path / \ a | 42 | 42/ ... b b | 63 | 42/63/ / \ c | 84 | 42/63/84/ c d d | 85 | 42/63/85/
所以一个child_of
的查询就变成了一个类似child.parent_path LIKE (tree.parent_path || '%')
这样的字符串拼接的条件查询了。在Odoo的实现中主要在BaseModel
类中引入了四个以parent_store
起始的函数:
-
parent_store_compute
: 用以一次性更新所有的parent_path
字段的值,目前只有在测试用例中被调用过 -
parent_store_create
: 在对象的create
方法中被调用,用以在创建记录时自动计算parent_path
字段的值 -
parent_store_update_prepare
: 用以返回`self`中那些需要更新parent_path
值的记录, 在_write
方法中被调用 -
parent_store_update
: 在上面的函数所返回的记录上更新其parent_path
值,在_write
方法中被调用
有了parent_path
的值,Odoo就可以很简单的实现child_of
, parent_of
这样的查询条件:
def child_of_domain(left, ids, left_model, parent=None, prefix=''):
""" Return a domain implementing the child_of operator for [(left,child_of,ids)],
either as a range using the parent_path tree lookup field
(when available), or as an expanded [(left,in,child_ids)] """
if not ids:
return FALSE_DOMAIN
if left_model._parent_store:
doms = OR([
[('parent_path', '=like', rec.parent_path + '%')]
for rec in left_model.browse(ids)
])
if prefix:
return [(left, 'in', left_model.search(doms).ids)]
return doms
def parent_of_domain(left, ids, left_model, parent=None, prefix=''):
""" Return a domain implementing the parent_of operator for [(left,parent_of,ids)],
either as a range using the parent_path tree lookup field
(when available), or as an expanded [(left,in,parent_ids)] """
if left_model._parent_store:
parent_ids = [
int(label)
for rec in left_model.browse(ids)
for label in rec.parent_path.split('/')[:-1]
]
if prefix:
return [(left, 'in', parent_ids)]
return [('id', 'in', parent_ids)]
Odoo使用Materialized Path算法来管理层级数据,使得对于层级数据的创建,更新,查询的效率有了较平衡的提高。