浅谈Django中的mptt

来源:互联网 发布:漂亮的ava女演员 知乎 编辑:程序博客网 时间:2024/06/05 15:59

层级结构

层级结构,也叫树形结构。在实际应用中,你经常需要保存层级结构到数据库中。比如说:你的网站上的目录。不过,除非使用类XML的数据库,通用的关系数据库很难做到这点。

对于树形数据的存储有很多种方案。主要的方法有两种:邻接表模型,以及修改过的前序遍历算法。因为mptt使用的是修改过的前序遍历算法,而此算法又是从邻接表改进得来的,所以本文先要说这两块。

本文以食品商店为例,通过类别、颜色以及种类来对其食品进行分类。

图1

邻接列表模型(ADJACENCY LIST MODEL)

邻接列表模型的实现很优雅,只需要一个简单的递归。

在我们的食品店中,邻接列表的表格如下:



通过邻接表模型存储法中,我们可以看到Pear,它的父节点是Green,而Green的父节点又是Fruit,以此类推。而根节点是没有父节点的。这里为了方便观看,parent字段使用的字符串,实际应用中只要使用每个节点的ID即可。


现在已经在数据库中插入完毕数据,接下来开始先显示这棵树。

打印

public static void displayTree(int parentId, int level)     throws SQLException {    setUp();    ResultSet result = dbc.query(        "SELECT ID, title FROM `adjacency_list` WHERE parentid="        + parentId);    while(result.next())    {        System.out.println(repeatStr("  ", level)             + result.getString("title"));        displayTree(result.getInt("ID"), level+1);    }}

打印结果如下

Food  Fruit    Red      Cherry    Yellow      Banana  Meat    Beef    Pork

注意如果你只想看一个子树,你可以告诉函数从另一个节点开始。例如,要显示“Fruit”子树,你只要displayTree(2,0)

节点的路径

利用类似的函数,我们也可以通过节点的ID来查询它的路径。

例如,“Cherry”的路径是“Food”>“Fruit”>“Red”。要获得这个路径,我们的函数要获得这个路径,这个函数必须从最深的层次开始:“Cheery”。然后查找这个节点的父节点,并添加到路径中。在我们的例子中,这个父节点是“Red”。如果我们知道“Red”是“Cherry”的父节点。

public static List<String> getPath(int id)    throws SQLException {    List<String> paths = new ArrayList<String>();    setUp();    ResultSet result = dbc.query(        "SELECT parentid, title FROM `adjacency_list` WHERE ID="        + id);    result.next();    int parentid = result.getInt("parentid");    if(parentid != 0)        paths.addAll(getPath(parentid));    paths.add(result.getString("title"));    return paths;}

结果如下

Food -> Fruit -> Red -> Cherry

优缺点

我们可以看到,用邻接表模型确实是个不错的方法。它简单易懂,而且实现的代码写起来也很容易。

但是,邻接表模型执行起来效率低下和缓慢。

第一个原因是数据库访问的代价,我们在递归中,每次访问一个节点就需要进行数据库查询。每个查询需要一些时间,这使得函数非常缓慢的在处理大树时。

第二个原因是递归这个方法自身的代价。对于一门程序语言来说,除了Lisp这种,大多数不是为了递归而设计。当一个节点深度为4时,它得同时生成4个函数实例,它们都需要花费时间、占用一定的内存空间。所以,邻接表模型效率的低下可想而知。

树前序遍历的算法变形

那么就让我们来看另外一种存储树形结构的方法。如之前所讲,我们希望能够减少查询的数量,最好是只做到查询一次数据库。

现在我们把树竖着放。如下图所示,我们首先从根节点(“Food”)开始,先在它左侧标记“1”,然后我们到“Fruit”,左侧标记“2”,接着按照前序遍历的顺序遍历完树,依次在每个节点的左右侧标记数字。最后一个数字写在“Food”节点右边的。

在这张图片里,你可以看到用数字标记的整个树和几个箭头指示编号顺序。

这里写图片描述

对于参加过算法竞赛的同学,这种方式就是一个种简单的dfs出入序列。我们通常可以通过这种序列处理一棵子树上面的问题。在这儿,我们称这些数字为Left和Right(例如“Food”的Left值是1,Right值是18)。

对于这样的序列如何处理子树,举一个例子:我们可以注意到,左数大于2、右数小于11的节点都是“Fruit”的子孙。所以这就是一个种区间包含的关系、

现在,所有的节点将以左数-右数的方式存储,这种通过遍历一个树、然后给每一个节点标注左数、右数的方式称为修改过的前序遍历算法。

在我们继续之前,让我们来看看在我们的表的这些值:

这里写图片描述

注意,“Left”和“Right”在SQL中有特殊的意义。因此,我们必须使用“lft”和“rgt”标识列。还要注意,我们并不真的需要“parent”列了。我们现在有lft和rgt值存储树结构。

获取树

如果你要通过左值和右值来显示这个树的话,你要首先标识出你要获取的那些节点。例如,如果你想获得“Fruit”子树,你要选择那些左值在2到11的节点。用SQL语句表达:

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;

这个会返回:


整个树只需要一次查询。

有时候,如果进行过增、删的操作,表中的数据可能就不是正确的顺序。没问题,只要使用“ORDER BY”语句就可以了,就像这样:

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;

现在唯一的问题是缩进问题。

显示树结构,孩子应该缩进略高于他们的父母。

正如我们面对树的问题常常会想到的方案——栈。这里,我们可以维护一个只保存右数的栈。我们知道该节点所有的孩子的Rgt值小于父的Rgt值,所以通过比较当前节点的Rgt值和堆栈中最后一个节点的Rgt值。当当前节点的Rgt值大于栈顶元素的值(说明栈顶元素的子树都以遍历完毕),这个时候弹出栈顶值。再循环检查栈顶值,直到栈顶值小于当前查询节点的Rgt值。这个时候只要检查栈中元素,有多少个元素说明当前查询节点有多少个祖先节点(设为n)。只需要打印n个空格即可。代码如下:

public static void displayTree(String root) throws SQLException{    setUp();    ResultSet result = dbc.query("SELECT lft, rgt "        + "FROM `modified_preorder_travesal` WHERE title='"        + root + "';");    result.next();    Stack<Integer> right = new Stack<Integer>();    result = dbc.query("SELECT title, lft, rgt "        + "FROM `modified_preorder_travesal`"        + " WHERE lft BETWEEN "        + String.valueOf(result.getInt("lft"))        + " AND "        + String.valueOf(result.getInt("rgt"))        + " ORDER BY lft ASC;");    while(result.next()){        if(right.size() > 0){            Integer current = right.peek();            while(current < result.getInt("rgt")){                right.pop();                current = right.peek();            }        }        System.out.println(repeatStr("  ", right.size())             + result.getString("title"));        right.push(result.getInt("rgt"));    }}

运行代码,打印结果和之前邻接表模型打印的结果一样。但是新方法更快,原因就是:没有递归,且一共只使用两次查询。

求节点的路径

在修改过的前序遍历算法的实现中,我们同样需要求节点的路径。不过这不是很困难,对于某节点,我们只需求出左数值小于其左数值、右数大于其右数的所有节点。比如说“Cherry”这个节点(4-5),我们可以这么写SQL查询:

SELECT title FROM tree WHERE lft < 4 AND rgt > 5 ORDER BY lft ASC;

这里同样别忘了添加“ORDER BY”语句。执行以后返回结果:

求有多少子孙

已知某节点的左数和右数,它的子孙的求法也就相当简单了,用如下方法:

descendants=(rightleft1)/2

从邻接表转化

我们在开始的临界表上增加”lft“和”rgt“字段。执行以下代码,完成转换:

public static int rebuildTree(int parentId, int left) throws SQLException {    setUp();    int right = left + 1;    ResultSet result = dbc.query("SELECT ID, title FROM `adjacency_list` WHERE "        + "parentid=" + parentId);    while(result.next()){        right = rebuildTree(result.getInt("ID"), right);    }    dbc.update("UPDATE `adjacency_list` SET lft=" + String.valueOf(left)        + ", rgt=" + String.valueOf(right)        +" WHERE ID='" + parentId + "';");    return right + 1;}

开始执行只要运行rebuildTree(1, 1);

我们所写的运行函数是一个递归函数。对于某一节点,如果其没有子孙节点,那么他的右数值等于左数值+1;如果有那么返回其子树右数值+1。这个函数稍微有点复杂,不过梳理通了以后就不难理解。

这个函数将会从根节点开始遍历整个树。运行了可以发现和我们之前手动所建的表一样。这里有个快速检查的方法:那就是检查根节点的右数值,如果它等于节点总数的2倍,那就是正确的。

增加节点

增加节点有两种方法:

  1. 保留parentid字段,当增加节点后,运行一遍“rebuildTree”方法。这么做看起来很简单,不过你应该知道,这么做效率低下,尤其是大树时。那么第二种方法呢?
  2. 首先我们得为添加的节点腾出空间。比如,我们想添加“Strawberry“到”Red“节点下,那么“Red”节点的右数就得从6到8,而“Yellow”就得从7-10变成9-12,以此类推。更新Red节点就意味着大于5的左数和右数都要增加2。

我们先运行以下SQL语句:

UPDATE tree SET rgt=rgt+2 WHERE rgt>5;UPDATE tree SET lft=lft+2 WHERE lft>5;

现在我们可以添加“Strawberry”到“Red”下,其左数为6、右数为7。

INSERT INTO tree SET lft=6, rgt=7, title='Strawberry';

再次运行“displayTree”方法,会发现“Strawberry”已被添加其中。删除节点有着差不多的步骤,这里就略去不提了。

缺点

首先,修改过的前序遍历算法似乎更难理解。但是它有着邻接表模型无法比拟的速度优势,虽然,在增或着删数据的时候步骤多了些,但是,查询的时候只需要一条SQL语句。

不过,这里我要提醒,当使用前序遍历算法存储树的时候,要注意临界区问题,就是在增或者删的时候,不要出现其他的数据库操作。

mptt介绍以及使用

Django mptt是个Django第三方组件,目标是使Django项目能在数据库中存储层级数据(树形数据)。

它主要实现了修改过的前序遍历算法。当然,Django项目中使用mptt时,原理是可以不用了解的,因为具体的实现细节都已经隐藏。

unordered_list

在介绍mptt之前,如果你的需求仅仅是像这样显示下面的数据:

<li>Food    <ul>         <li>Fruit        <ul>             <li>Red            <ul>                <li>Cherry</li>             </ul>             </li>             <li>Yellow            <ul>                <li>Banana</li>             </ul>             </li>         </ul>         </li>         <li>Meat        <ul>             <li>Beef</li>             <li>Pork</li>         </ul>         </li>     </ul> </li> 

Django已经有内置模板过滤器来完成这个工作:unordered_list。如果你的需求不只这么简单,那就跳过这一段。不过这里还是要讲解一下unordered_list的做法。我们就来实现以上的结果。

首先要写一个简单的Model。

from django.db import modelsclass Food(models.Model):    title = models.CharField(max_length=50)    parent = models.ForeignKey("self", blank=True, null=True, related_name="children")    def __unicode__(self):        return self.title

开启自带的admin,在后台添加完数据。再使用unordered_list这个过滤器来显示树形图。

按照官方文档的说法,显示时传递给template的数据应该是这样(非常想leetcode里面某些数据的形式):

['Food', ['Fruit', ['Red', ['Cherry'], 'Yellow', ['Banana']], 'Meat', ['Beef', 'Pork']]]

我们需要写一个递归的工具函数:

def change(foods):    l = []    for food in foods:        l.append(food.title)        children = food.children.all()        if len(children) > 0:            l.append(display(food.children.all()))    return l

于是在views.py中,我们只要得到根节点,然后把change函数生成的列表传递给template:

from django.shortcuts import render_to_responsedef unordered_list(request):    foods = Food.objects.filter(parent=None)    var = change(foods)    return render_to_response('mpttexample/unordered_list.html', {'var': var})

最后在template中添加:

{{ var|unordered_list }}

因为有时候需求不止这么简单,比如有时需要展现样式、后台权限管理等等,unordered_list就远远不够了。这个时候就需要mptt,下面开始介绍mptt的用法。

mptt使用

官方文档链接

在models.py中,Model的实现几乎没有任何的变化,只需继承MPTTModel类:

class MPTTFood(MPTTModel):    title = models.CharField(max_length=50)    parent = models.ForeignKey("self", blank=True, null=True, related_name="children")    def __unicode__(self):        return self.title

这里需要说明的是,实际上MPTTModel隐藏了四个变量:level,lft,rght和tree_id。大多数时候我们是用不到这几个变量的。

对于继承MPTTModel的类的实例,将会有额外的方法,比如get_ancestors(更多参考文档)。

如果有自带的Admin,只需在admin.site注册,就可以在Admin模板中像这样显示数据:

from django.contrib import adminfrom mptt.admin import MPTTModelAdminadmin.site.register(MPTTFood, MPTTModelAdmin)

图1

接下来的话题,就是怎样在模板中显示的问题。我们来修改之前ordered_list的显示,结构是一样的,只是对于叶子节点,我们让它显示成红色。在模板中,不要忘了加{% load mptt_tags %}

{% load mptt_tags %}{% recursetree nodes %}<li>    {% if node.is_leaf_node %}    <span style="color: red;">{{ node.title }}</span>    {% else %}        {{ node.title }}        <ul>            {{ children }}        </ul>    {% endif %}</li>{% endrecursetree %}

这里在视图中传递给模板的参数名必须是nodes。views中就像这样:

def mptt(request):    nodes = MPTTFood.tree.all()    return render_to_response('mpttexample/mptt.html', {'nodes': nodes})

这是在前端的使用方法,而因为树形可以做非常多的事情,所以mptt也可以用于类似权限管理等和树形有关的应用。

我们将Group做成一个mptt的树形结构,每一个Group有不同的权限。如果我们需要在数据库中手工维护这个树形结构,就会花费大量的时间,而mptt却已经帮你实现了。所以我们只需要用别的方式去实现Group的权限分配就好了。

class GroupProfile(MPTTModel):    name = models.CharField(max_length=50, unique=True)    user_group = models.OneToOneField(Group, null=True, blank=True, related_name='user_profile')    admin_group = models.OneToOneField(Group, null=True, blank=True, related_name='admin_profile')    superadmin = models.ForeignKey(User, null=True, related_name='group_profile')    parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)    class Meta:        permissions = (            ('view_groupprofile', 'Can view Group Profile'),        )

参考文章

  • 在数据库中存储层次数据
原创粉丝点击