浅谈Django中的mptt
来源:互联网 发布:漂亮的ava女演员 知乎 编辑:程序博客网 时间:2024/06/05 15:59
层级结构
层级结构,也叫树形结构。在实际应用中,你经常需要保存层级结构到数据库中。比如说:你的网站上的目录。不过,除非使用类XML的数据库,通用的关系数据库很难做到这点。
对于树形数据的存储有很多种方案。主要的方法有两种:邻接表模型,以及修改过的前序遍历算法。因为mptt使用的是修改过的前序遍历算法,而此算法又是从邻接表改进得来的,所以本文先要说这两块。
本文以食品商店为例,通过类别、颜色以及种类来对其食品进行分类。
邻接列表模型(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”语句。执行以后返回结果:
求有多少子孙
已知某节点的左数和右数,它的子孙的求法也就相当简单了,用如下方法:
从邻接表转化
我们在开始的临界表上增加”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倍,那就是正确的。
增加节点
增加节点有两种方法:
- 保留parentid字段,当增加节点后,运行一遍“rebuildTree”方法。这么做看起来很简单,不过你应该知道,这么做效率低下,尤其是大树时。那么第二种方法呢?
- 首先我们得为添加的节点腾出空间。比如,我们想添加“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'), )
参考文章
- 在数据库中存储层次数据
- 浅谈Django中的mptt
- Django mptt介绍以及使用
- 浅谈Django中的Signal
- mptt总结
- 浅谈Django中的RequestContext和Context
- 浅谈Python Django框架
- 浅谈reverse函数与django哲学
- 浅谈reverse函数与django哲学
- python学习之浅谈django模板
- 浅谈django 和 get post 方法
- 浅谈django的信号机制 Signals
- Django 中的 app
- Django中的MVC
- django中的signals
- django中的csrf
- Django中的查询
- Django中的分页
- Python/Django中的注释
- Failed to complete gradle execution
- CPU状态信息us,sy,ni,id,wa,hi,si,st含义及分析
- ORA-12638: 身份证明检索失败
- 2017中国软件技术大会即将于12月在京召开!
- 设计模式之抽象工厂Abstract Factory
- 浅谈Django中的mptt
- 1051. Pop Sequence (25)
- LeetCode.102 Binary Tree Level Order Traversal
- Android源码基础解析之Activity销毁流程
- 记忆网络之End-To-End Memory Networks
- PHP中的 抽象类abstract和 接口类interface的区别
- 【Power Designer】反向工程生成类图
- 理解spark闭包
- leetcode--136--Longest Substring Without Repeating Characters