论坛首页 编程语言技术论坛

Rails生成Ext Tree

浏览 12452 次
该帖已经被评为精华帖
作者 正文
   发表时间:2008-03-29  

在Rails中使用has_one 、has_many 、belongs_to 和 has_and_belongs_to_may 来声明关系型数据库中的一对一,一对多和多对多的关系,但当想以树形的数据结构来表示分类的时候,这些基本的关联功能并不够,Rails在has_XXX关系的基础上,提供了acts as的扩展功能,如acts_as_list 、acts_as_tree 、 acts_as_nested_set。acts_as_tree就提供树状的结构来组织记录。(不知道为什么Rails2.0以后会取消掉,需要通过插件的方式来安装)

 

acts_as_nested_set的官方解释:

A Nested Set is similar to a tree from ActsAsTree. However the nested set allows building of the entire hierarchy at once instead of querying each nodes children, and their children. When destroying an object, a before_destroy trigger prunes the rest of the branch of object under the current object.

上面是引用自rubyonrails.org上的对于acts_as_nested_set的描述,并提供了一个简单的示例:

 

SQL脚本: 

create table nested_objects (
  id int(11) unsigned not null auto_increment,
  parent_id int(11),
  lft int(11),
  rgt int(11),
  name varchar(32),
  primary key (id)
);

 

Ruby Model:

class NestedObject < ActiveRecord::Base
  acts_as_nested_set
end

 

acts_as_nested_set提供的方法:

  • root?() – 是否是根对象
  • child?() – 是否是子对象(拥有父对象)
  • unknown?() – 不知道该对象的状态(既不是根对象,也不是子对象)
  • add_child(child_object) – 为根对象添加一个子对象(如果child_object是一个根对象的话,则添加失败)
  • children_count() – 根对象的所有子对象的个数
  • full_set() – 重新找到所有对象
  • all_children() – 根对象的所有子对象
  • direct_children() –根对象的直接子对象

下面就使用acts_as_nested_set来生成一个Ext的Tree。

比如生成如下的树:

root
    |_ Child 1
    |  |_ Child 1.1
    |  |_ Child 1.2
    |_ Child 2
       |_ Child 2.1
       |_ Child 2.2

先来看一下对上面的树的一个图形化的解释:

 

这图还是比较清除的,请理解横线中的1到14这些数字,对应这个树,我们可能会有下面的数据:

 

这个也就是SQL脚本中的的lft和rgt的解释

 

 

1.创建Rails工程:

    rails ExtTree

2.安装act_as_nested_set:

    ruby script/plugin install acts_as_nested_set

3.下载ext,解压下载后的压缩包并拷贝到ExtTree工程的public目录(public/ext)

 

4.创建模型对象:

    ruby script/generate resource Category parent_id:integer lft:integer rgt:integer text:string

5.给模型对象Category加入acts_as_nested_set:

class Category < ActiveRecord::Base
  acts_as_nested_set
end

6.下面在CategoriesController中加入index方法,让它来转到index.html页面,并且为EXT TREE来生成JSON数据:

class CategoriesController < ApplicationController
  def index(id = params[:node])
    respond_to do |format|
      format.html # render static index.html.erb
      format.json { render :json => Category.find_children(id) }
    end
  end
end

 index方法有一个参数id,用来接收一个树的节点的id,我们就可以通过一个id来查找该节点的子节点。

7.实现CategoriesController中的find_children方法:


#首先先得到树的根节点,再根据传过来的id找到根的子节点
def self.find_children(start_id = nil)
    start_id.to_i == 0 ? root_nodes : find(start_id).direct_children
end
#如果parent_id为空,则为树的根节点
def self.root_nodes
    find(:all, :conditions => 'parent_id IS NULL')
end

 

到这里,已经实现了基本的树形结构,但却还有一个问题,如果是树叶节点,既没有子节点的节点,图标应该显示为"-" ,不应该再能够伸展了,Ext Tree中提供的示例中给出的JSON数据中有一个leaf的属性,如果为true,则为树叶节点,如果为false,则为树枝节点,所以,我们还需要让我们生成的JSON数据用来leaf来标识树枝节点与树叶节点,在Category.rb中添加如下代码:

def leaf
    unknown? || children_count == 0
end

def to_json_with_leaf(options = {})
    self.to_json_without_leaf(options.merge(:methods => :leaf))
end

alias_method_chain :to_json, :leaf

 对于alias_method_chain,需要先说一下Ruby中的alias_method方法,在Ruby中有这样的用法:

 

alias_method :old_method_name :new_method_name

 它同alias很类似,但只能用法方法。

在Ruby中,可以使用方法链的手段来实现mix-in,如果想要用new_method来override old_method方法,就可以这样使用:

alias_method :old_method_name :new_method_name
alias_method :new_method_name :old_method_name
 而在Rails中,提供了一个更强大的方法:alias_method_chain。

 

下面是index.html.erb文件:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
    <title>Rails Ext Tree</title>
    <%= stylesheet_link_tag "../ext/resources/css/ext-all.css" %>
    <%= javascript_include_tag :defaults %>
    <%= javascript_include_tag "../ext/adapter/prototype/ext-prototype-adapter.js" %>
    <%= javascript_include_tag "../ext/ext-all.js" %>
</head>
<body>
<div id="category-tree" style="padding:20px"></div>
<% javascript_tag do -%>
    Ext.onReady(function(){     
        root = new Ext.tree.AsyncTreeNode({
        text: 'Invisible Root',
        id:'0'
    });
   
    new Ext.tree.TreePanel({
        loader: new Ext.tree.TreeLoader({
            url:'/categories',
            requestMethod:'GET',
            baseParams:{format:'json'}
        }),
        renderTo:'category-tree',
        root: root,
        rootVisible:false
    });
    
    root.expand();
    });
<% end -%>
</body>
</html>
 

添加测试数据:

root = Category.create(:text => 'Root')

root.add_child(c1 = Category.create(:text => 'Child 1'))
c1.add_child(Category.create(:text => 'Child 1.1'))
c1.add_child(Category.create(:text => 'Child 1.2'))

root.add_child(c2 = Category.create(:text => 'Child 2'))
c2.add_child(c21 = Category.create(:text => 'Child 2.1'))
c2.add_child(c21 = Category.create(:text => 'Child 2.2'))
 

最后的显示效果:

 

 

 

 

  • 大小: 3.2 KB
  • 大小: 4 KB
  • 大小: 2.3 KB
   发表时间:2008-04-26  
楼主~ 没写全吧 我实现不了 ???
0 请登录后投票
   发表时间:2008-04-27  
yangtao309 写道
楼主~ 没写全吧 我实现不了 ???

什么叫没有写全?你有什么问题吗?
0 请登录后投票
   发表时间:2008-04-27  

def to_json_with_leaf(options = {})
    self.to_json_without_leaf(options.merge(:methods => :leaf))
end

self.to_json_without_leaf
这个方法找不到啊? 可以的话 能把源代码发我吗? yangtao309@gmail.com
0 请登录后投票
   发表时间:2008-05-01  
抱歉我是从.NET来的对MySQL和rails都不熟悉
问题1 MYSQL支持varchar做主键,但是当我在那个varchar做主键的表里,在建一个自增的int列就报错,
there can be only one auto column and it must be defined as a tree
汗!

一般不是有些公司很奇怪,不仅一个表里需要一个sn就是int自增那种,还要一个自己生成的类似于guid的列,例如商品编号之类的,如"productA002",我想把这个productA002这列设为主键,另外在来一个自增列作为sn,这样的方式在SQL里行,MySQL里不行.

我自己也不明为啥需要2个列,没想通.

问题2 activerecord act as tree问题 是否可以将你的例子里的
id int(11) unsigned not null auto_increment,  
parent_id int(11),  
这2列替换成我刚才说的那个productA002 也就是非主键那个列作为tree的关系列
0 请登录后投票
   发表时间:2008-05-02  
gakaki 写道
抱歉我是从.NET来的对MySQL和rails都不熟悉
问题1 MYSQL支持varchar做主键,但是当我在那个varchar做主键的表里,在建一个自增的int列就报错,
there can be only one auto column and it must be defined as a tree
汗!

一般不是有些公司很奇怪,不仅一个表里需要一个sn就是int自增那种,还要一个自己生成的类似于guid的列,例如商品编号之类的,如"productA002",我想把这个productA002这列设为主键,另外在来一个自增列作为sn,这样的方式在SQL里行,MySQL里不行.

我自己也不明为啥需要2个列,没想通.

问题2 activerecord act as tree问题 是否可以将你的例子里的
id int(11) unsigned not null auto_increment,  
parent_id int(11),  
这2列替换成我刚才说的那个productA002 也就是非主键那个列作为tree的关系列

对不起,我看了几遍,都看不懂你说什么,可不可以表达清楚一些,不好意思。
0 请登录后投票
   发表时间:2008-05-02  
将数据库换成这样
create table nested_objects (  
  id int(11) unsigned not null auto_increment,  
  tree_id varchar(32),               --作为treeid
  parent_tree_id varchar(32),
  lft int(11),  
  rgt int(11),  
  name varchar(32),
  primary key (id)  
);

是不是就没法映射了.

还有是在MYSQL里 用varchar做主键,在建一个自增int列行不行
0 请登录后投票
   发表时间:2008-05-05  
最近在自学RoR,看了LZ的帖子很受用
不过在数据库的设计上的lft和rgt的设计有些疑问,以下说说我的想法,如有不对的地方谢谢批评指正。
(事先申明,不是很熟悉Rails的acts_as_nested_set,对它的实现原理不了解。对于我下面的疑问,也许acts_as_nested_set已经有了很好的解决方案,我也只不过提提建议罢了。)
(因为我是做Java的,所以例子都是实际开发的Java代码,SSH的架构。好在rails区里以前的Java开发者众多,所以我的代码大家应该都能看的懂吧。)

lft和rgt这两个字段看上去是用来定义一个类型边界的。如果需要查找某个特定类型及其子类的话,则先查找该类型的lft和rgt,然后再 (lft > ? and rgt < ?) 获得其子类。
这种设计在数据结构完备的情况下能准确的统计出所有的子类。但是我要动态的增加Child子类,或者改变Child的隶属关系的时候,就需要对数据库表中所有数据的lft和rgt做出调整。比如我要在Child 1中增加一个Child 1.3。相应的 Root、Child 1、Child 2、Child 2.1、Child 2.2 的lft和rgt都要做相应的变化。
也许acts_as_nested_set可以通过先 delete from category; 后 insert into values(?,?,?,?); 的方式进行全类的维护,但是如果别的类有对Category的引用(即外键)。这样的隶属关系不是会产生混乱了吗?况且如果数据库真的建立了外键的话,也不允许 delete from category; 操作的。

所以我的考虑是用一个level字段代替lft和rgt字段,level字段维护着Category实例的层级关系。
例如,将root的level定义为1(这个在数据库或程序中可配),那么Child 1的level就为1|${id}(其中'|'为Level分层标记,${id}表示当前数据Id或其它可唯一标识的字段值),假定为1|2,同理Child 1.1的level就是1|2|3。
类结构如下所示:使用的是annotation的hibernate
@Entity
public class Category implements Serializable {

	/** Level分层标记 */
	public static final String LEVEL_SPLIT = "|";

	@Id
	@GeneratedValue
	private Integer id;
	
	/** 名称 */
	private String name;

	/** level */
	private String level;
	
	/** 删除标记 */
	private Boolean delFlag;

	/** 下级的类别 */
	@OneToMany(fetch = FetchType.LAZY, mappedBy="parent")
	@OrderBy("id")
	private List<Category> children = new ArrayList<Category>();

	/** 上级的类别 */
	@ManyToOne
	@JoinColumn(name = "category_id")
	private Category parent;
	
	// 省略所有 getter/setter 方法...
}

维护后数据库中的数据如下:



查找某个特定类型及其子类,只需要获得当前类型的level值,然后查找 (level like 'xxxx%')即可。如下所示:
Rails中需要重写Category中类似find的方法。
	public List<Category> getCategoryList(Integer startId) {
		Category root = crudDao.get(Category.class, startId);	// crudDao继承HibernateDaoSupport,并封装了HibernateTemplate的操作,下同
		String hql = "from Category where level like ? order by id desc";
		return crudDao.query(hql, root.getLevel + Category.LEVEL_SPLIT + "%");
	}



新增或修改Category的方法。则需要先查找当前父类,再重新维护当前类的level属性值即可。如下所示:
Rails中需要重写Category中的add_child方法
	public void saveCategory(Category category) {
		Category parent = category.getParent();
		// 判断Category的父类有没有设置
		if (parent == null || parent.getId() == null || parent.getId() == 0) {
			throw new ParentNotFoundException(Category.class); 
		}
		// 重新设置父类关系
		parent = crudDao.get(Category.class, parent.getId());	// crudDao继承HibernateDaoSupport,并封装了HibernateTemplate的操作,下同
		category.setParent(parent);
		
		if (category.getId() == null || category.getId() == 0) {
			crudDao.save(category);
			category.setLevel(parent.getLevel() + Category.LEVEL_SPLIT + category.getId());
			crudDao.update(category);
		} else {
			Category oldCategory = crudDao.get(Category.class, category.getId());
			BeanUtils.copyProperties(category, oldCategory, new String[]{"id", "children"});
			oldCategory.setLevel(parent.getLevel() + Category.LEVEL_SPLIT + oldCategory.getId());
			crudDao.update(oldCategory);
		}
	}

使用Rails的ActiveRecord,以上代码可以写的更简练


页面显示,我使用的是纯javascript的dtree。代码如下所示:
<body class="dtree">
<p><a href="javascript:categoryTree.openAll();">open all</a> | <a href="javascript:categoryTree.closeAll();">close all</a></p>
<script type="text/javascript">
	var dtreeImgPath = "${ctx}/script/dtree/img/";
	categoryTree = new dTree('categoryTree');
	<c:forEach var="category" items="${list}">
	<c:choose>
		<c:when test="${category.parent == null or category.parent == null or category.parent.id == 0}">
			categoryTree.add(${category.id},-1,'${category.name }',"javascript:doSelect('${category.id}','${category.name }')");
		</c:when>
		<c:otherwise>
			categoryTree.add(${category.id},${category.parent.id},'${category.name }',"javascript:doSelect('${category.id}','${category.name }')");
		</c:otherwise>
	</c:choose>
	</c:forEach>
	document.write(categoryTree);
</script>
</body>


显示的效果如下:
  • 描述: Category 显示效果图
  • 大小: 9.1 KB
  • 描述: Category 数据图
  • 大小: 14 KB
0 请登录后投票
   发表时间:2008-05-05  
bocar 写道
最近在自学RoR,看了LZ的帖子很受用
不过在数据库的设计上的lft和rgt的设计有些疑问,以下说说我的想法,如有不对的地方谢谢批评指正。

RoR的这个设计对于大数据量tree结构是最好的,比如一些常见的查询都可以避免循环或者递归抓取了:
--查询某个parent的所有子节点
SELECT * WHERE lft IS BETWEEN parent.lft AND parent.rgt

--统计某个节点的子节点数目
(right - left - 1)/2

--获取某个节点的所有父节点
SELECT * WHERE node.lft IS BETWEEN lft AND rgt

而且通过在lft, rgt上设置索引能够达到最好的查询效率。

RoR是通过callback来实现新加/删除/移动节点后,更新对应的所有节点lft/rgt值,这个更新也只是一句update语句而已,不需要循环调用。在Java中我们也可以这样设计,比如利用Hibernate的event机制。

你说的加level也是一种常见方法,但是like查询效率很低,不是合适大数据量的一种设计,而且需求如果要求节点是可以变更父子关系的,那么更新level也是一件麻烦的事情。记得很早以前JavaEye讨论过相关的tree数据库设计,文章中总结了多种方法,对于优缺点都分析得很清楚,我再找找看。
6 请登录后投票
   发表时间:2008-05-05  
nested tree有2个缺点比较明显:
1、单纯的nested tree无法取直系子节点,还是要借助parent,不过这不是什么大问题
2、插入及更新效率低,而且为了保证更新不出问题,会进行锁表,这样在更新频繁的场合并发能力就差


0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics