Odoo中具有层级关系的数据的存储和搜索

Odoo中有很多层级关系的数据,比如:产品的分类,多层级的BOM,项目中的任务及其子任务等等。在数据库中快速存储更新这些有层级关系的数据,并能实现高效的查询是一个颇具挑战的任务。

Odoo一直以来采用的方法称为:MPTT (Modified Preorder Tree Traversal)。 比如对于下面层级树,在数据库中每个节点都记录了parent_leftparent_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_ofparent_of这样的查询条件:

expression.py - child_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
expression.py - parent_of
        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算法来管理层级数据,使得对于层级数据的创建,更新,查询的效率有了较平衡的提高。