实现web树

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现web树相关的知识,希望对你有一定的参考价值。

在我的博客《Java Web基础入门第四十四讲 数据库表的设计》中,用一个商品分类表来说明怎样去设计一个无限极分类的表。如果我们要使用这样的一个无限极分类的表来实现web树,不可避免就要递归,如果树的层次很深,那么递归的次数就会很多,这时候极容易导致内存溢出。这样的表虽然理论上可以保存无限极的分类,但是在实际开发里面是不行的,因为树的层次过多,那么递归的次数就会过多,这样很容易导致内存溢出,又因为我们的计算机内存是有限的。那么该如何设计数据库表来实现web树呢?

设计实现web树的数据库表

我们可以将商品分类表设计成如下的树状结构:
在这里插入图片描述
再将其改造成如下样式:
在这里插入图片描述
树状节点的特点:

  1. 每一个节点都有一个左右值;
  2. 如果右值-左值=1,则代表当前节点为叶子节点;
  3. 如果右值-左值>1,则代表当前节点有孩子节点,值在左右值之间的所有节点,即为当前结点的所有孩子节点。

根据以上树状结构,我们可以使用如下SQL建表语句来设计数据库表:

create table category
(
    id varchar(40) primary key,
    name varchar(100),
    lft int,
    rgt int
);

并向category表中插入一些数据:

insert into category values('1','商品',1,18);
insert into category values('2','平板电视',2,7);
insert into category values('3','冰箱',8,11);
insert into category values('4','笔记本',12,17);
insert into category values('5','长虹',3,4);
insert into category values('6','索尼',5,6);
insert into category values('7','西门子',9,10);
insert into category values('8','thinkpad',13,14);
insert into category values('9','dell',15,16);

这时就会产生一个问题:为了在页面中显示树状结构,需要得到所有结点,以及每个结点在树中的层次。解决思路如下:

  1. 要得到结点的层次,就是看节点有几个父亲,例如长虹有2个父亲,则它所在层次就为2;

  2. 如何知道每一个节点有几个父亲呢?这个表有个特点,父亲和孩子都在同一个表中,为得到父亲所有的孩子,可以把这张表想像成两张表,一张表用于保存父亲,一张表用于保存孩子,如下所示:

    select * from category parent,category child;
    
  3. 父亲下面的孩子有个特点,它的左值>父亲的左值,并且<父亲的右值,如下所示:

    select * from category parent,category child where child.lft>=parent.lft and child.rgt<=parent.rgt;
    

    以上语句会得到父亲下面所有的孩子。

  4. 对父亲所有孩子的姓名进行归组,然后使用count统计函数,这时就会知道合并了几个孩子,合并了几个孩子姓名,这个孩子就有几个父亲,从而知道它所在的层次。

    select child.name,count(child.name) depth from category parent,category child where child.lft>=parent.lft and child.rgt<=parent.rgt group by child.name;
    
  5. 最后根据左值排序即可。

    select child.name,count(child.name) depth from category parent,category child where child.lft>=parent.lft and child.rgt<=parent.rgt group by child.name order by child.lft;
    

实现web树

现在我们来写代码实现web树。

创建MVC架构的Web项目

在Eclipse中新建一个day17_tree的Web项目,导入项目所需要的开发包(jar包)以及创建项目所需要的包,在Java开发中,架构的层次是以包的形式体现出来的。
在这里插入图片描述
在这里插入图片描述
以上就是根据此项目的实际情况创建的包,可能还需要创建其他的包,这个得根据项目的实际需求来定了。
由于我们在应用程序中加入了DBCP连接池,所以还应在类目录下加入DBCP连接池的配置文件:dbcpconfig.properties。该配置文件的内容如下:

#连接设置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/day17
username=root
password=yezi

#<!-- 初始化连接 -->
initialSize=10

#最大连接数量
maxActive=50

#<!-- 最大空闲连接 -->
maxIdle=20

#<!-- 最小空闲连接 -->
minIdle=5

#<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 -->
maxWait=60000


#JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;] 
#注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
connectionProperties=useUnicode=true;characterEncoding=utf8

#指定由连接池所创建的连接的自动提交(auto-commit)状态。
defaultAutoCommit=true

#driver default 指定由连接池所创建的连接的只读(read-only)状态。
#如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
defaultReadOnly=

#driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
#可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
defaultTransactionIsolation=READ_COMMITTED

创建好的项目架构如下图所示:
在这里插入图片描述

分层架构的代码编写

分层架构的代码也是按照【域模型层(domain)】→【数据访问层(dao、dao.impl)】→【业务逻辑层(service、service.impl)】→【表现层(web.controller、web.UI、web.filter、web.listener)】→【工具类(util)】→【测试类(junit.test)】的顺序进行编写的。

开发domain层

在cn.liayun.domain包下创建一个Category类,该类的具体代码如下:

package cn.liayun.domain;

public class Category {
	
	private String id;
	private String name;
	private int lft;
	private int rgt;
	private int depth;
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getLft() {
		return lft;
	}
	public void setLft(int lft) {
		this.lft = lft;
	}
	public int getRgt() {
		return rgt;
	}
	public void setRgt(int rgt) {
		this.rgt = rgt;
	}
	public int getDepth() {
		return depth;
	}
	public void setDepth(int depth) {
		this.depth = depth;
	}
	
}

开发数据访问层(dao、dao.impl)

在开发数据访问层之前,先在cn.liayun.utils包下创建一个获取数据库连接的工具类(JdbcUtils),该工具类的代码为:

package cn.liayun.utils;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;

public class JdbcUtils {
	
	private static DataSource ds;
	
	static {
		try {
			Properties prop = new Properties();
			InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
			prop.load(in);
			
			BasicDataSourceFactory factory = new BasicDataSourceFactory();
			ds = factory.createDataSource(prop);
		} catch (Exception e) {
			throw new ExceptionInInitializerError(e);
		}
	}
	
	public static DataSource getDataSource() {
		return ds;
	}
	
	
	public static Connection getConnection() throws SQLException {
		return ds.getConnection();
	}
	
}

温馨提示:以上工具类里面没有必要提供release()方法,因为我们是使用DBUtils操作数据库,即调用DBUtils的update()和query()方法操作数据库,它操作完数据库之后,会自动释放掉连接。
接着,在cn.liayun.dao包下创建一个CategoryDao类,该类的具体代码如下:

package cn.liayun.dao;

import java.util.List;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import cn.liayun.domain.Category;
import cn.liayun.utils.JdbcUtils;

public class CategoryDao {
	public List<Category> getAll() {
		try {
			QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());
			String sql = "select child.id,child.name,child.lft,child.rgt,count(child.name) depth from category parent,category child where child.lft >= parent.lft and child.rgt <= parent.rgt group by(child.name) order by child.lft";
			List<Category> list = runner.query(sql, new BeanListHandler<Category>(Category.class));
			return list;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		
	}
}

照理说,开发完数据访问层,一定要对程序已编写好的部分代码进行测试。但是我们有信心认为以上代码不会有任何问题,这点自信都没有,搞鬼啊!

开发service层(service层对web层提供所有的业务服务)

在cn.liayun.service包下创建一个BusinessService类,该类的具体代码如下:

package cn.liayun.service;

import java.util.List;

import cn.liayun.dao.CategoryDao;
import cn.liayun.domain.Category;

public class BusinessService {
	public List<Category> getAllCategory() {
		CategoryDao dao = new CategoryDao();
		return dao.getAll();
	}
}

同理,开发完业务逻辑层,一定要对程序已编写好的部分代码进行测试,但是我们有信心认为业务逻辑层的代码没有任何问题,所以我们略过测试这一步。

开发web层

实现web树

在cn.liayun.web.controller包中创建一个ListTreeServlet,它用于处理实现web树的请求,该Servlet的具体代码如下:

package cn.liayun.web.servlet;

import java.io.IOException;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import cn.liayun.domain.Category;
import cn.liayun.service.BusinessService;

@WebServlet("/ListTreeServlet")
public class ListTreeServlet extends HttpServlet {
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		BusinessService service = new BusinessService();
		List<Category> list = service.getAllCategory();
		request.setAttribute("list", list);
		request.getRequestDispatcher("/listtree.jsp").forward(request, response);
	}
	
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}

}

接下来就要创建显示web树的界面(listtree.jsp)了,该页面的创建有一点麻烦,需要我们从网上下载一个名称为xtree117的树控件,将我们需要的xtree.js文件、xtree.css文件、images文件夹拷贝到我们的项目中,如下图所示:
在这里插入图片描述
关于这个树控件怎么使用,可以参考其文档xtree/usage.html。在这里我就不详细讲解其使用方法了,下面我直接给出listtree.jsp页面的内容:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script src="${pageContext.request.contextPath }/js/xtree.js"></script>
<link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath }/css/xtree.css">
</head>
<body>
	<script type="text/javascript">
		<c:forEach var="c" items="${list }">
			<c:if test="${c.depth == 1 }">
				var tree = new WebFXTree('${c.name}');
			</c:if>
			
			<c:if test="${c.depth == 2 }">
				var node${c.depth} = new WebFXTreeItem('${c.name}');//node2
				tree.add(node${c.depth});
			</c:if>
			
			<c:if test="${c.depth > 2 }">
				var node${c.depth} = new WebFXTreeItem('${c.name}');//node3
				node${c.depth-1}.add(node${c.depth}); /* node2.add(node3); */
			</c:if>
		</c:forEach>
		
		document.write(tree);
	</script>
</body>
</html>

至此,实现web树的整个项目就圆满完成了,下面我们来测试一把,测试结果如下:
在这里插入图片描述
可能出现的问题:当我们在浏览器中输入url地址http://localhost:8080/day17_tree/ListTreeServlet访问服务器,显示web树时,文件夹图片有可能显示不出来,那么一定是图片的路径写错了。xtree.js文件里面引用图片采用的是相对路径,那么相对路径相对的是谁呢?相对的是http://localhost:8080/day17_tree这个目录,该目录下是有images目录的,所以我们显示web树是没有任何问题的。

给web树动态添加节点

例如,现在要给冰箱这个节点再添加一个海尔子节点,用图来表示即为:
这里写图片描述
上图就说明了动态地给web树添加节点的原理。现在我们写代码来实现动态地给web树添加节点的功能。
要实现该功能,需要修改JavaBean——Category.java的代码,Category类修改后的代码为:

public class Category {
	
	private String id;
	private String name;
	private int lft;
	private int rgt;
	private int depth;
	
	private List<Category> parents = new ArrayList<Category>(); // 记住该节点的所有父节点
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getLft() {
		return lft;
	}
	public void setLft(int lft) {
		this.lft = lft;
	}
	public int getRgt() {
		return rgt;
	}
	public void setRgt(int rgt) {
		this.rgt = rgt;
	}
	public int getDepth() {
		return depth;
	}
	public void setDepth(int depth) {
		this.depth = depth;
	}
	public List<Category> getParents() {
		return parents;
	}
	public void setParents(List<Category> parents) {
		this.parents = parents;
	}
	
}

修改数据访问层CategoryDao.java的代码,改后的代码如下:

public class CategoryDao {
	
	public List<Category> getAll() {
		
		try {
			QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());
			String sql = "select child.id,child.name,child.lft,child.rgt,count(child.name) depth from category parent,category child where child.lft>=parent.lft and child.rgt<=parent.rgt group by child.name order by child.lft";
			List<Category> list = (List<Category>) runner.query(sql, new BeanListHandler(Category.class));
			
			return list;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		
	}
	
	public Category getCategory(String id) {
		
		try {
			QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());
			// 查出节点的基本信息
			String sql = "select * from category where id=?";
			Category category = (Category) runner.query(sql, id, new BeanHandler(Category.class));
			
			// 查出节点的父节点,填充list集合
			sql = "select * from category where lft < ? and rgt > ? order by lft";
			Object[] params = {category.getLft(), category.getRgt()};
			List<Category> list = (List<Category>) runner.query(sql, params, new BeanListHandler(Category.class));
			category.setParents(list);
			
			return category;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

	}

	public void addCategory(Category child) {
		
		try {
			QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());
			String sql = "insert into category(id,name,lft,rgt) values(?,?,?,?)";
			Object[] params = {child.getId(), child.getName(), child.getLft(), child.getRgt()};
			runner.update(sql, params);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		
	}

	public void insertUpdate(int rgt) {
		
		try {
			QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());
			String sql1 = "update category set lft=lft+2 where lft>=?";
			String sql2 = "update category set rgt=rgt+2 where rgt>=?";
			
			runner.update(sql1, rgt);
			runner.update(sql2, rgt);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		
	}
	
}

修改业务层BusinessService类的代码,改后的代码为:

public class BusinessService {
	
	private CategoryDao dao = new CategoryDao();
	
	public List<Category> getAllCategory() {
		return dao.getAll();
	}
	
	public Category getCategory(String id) {
		return dao.getCategory(id);
	}

	public void addCategory(String parent_id, String name) {
		Category parent = dao.getCategory(parent_id); // 得到要在哪个父节点下添加子节点
		
		// 创建要添加的子节点
		Category child = new Category();
		child.setName(name);
		child.setId(UUID.randomUUID().toString());
		child.setLft(parent.getRgt());
		child.setRgt(child.getLft() + 1);
		
		dao.insertUpdate(parent.getRgt());
		dao.addCategory(child);
		
	}
}

在WebRoot根目录下新建网站首页页面——index.jsp。
这里写图片描述
index.jsp页面内容如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>网站首页(采用分帧技术)</title>
</head>
<frameset cols="15%,*">
  	<frame src="${pageContext.request.contextPath}/ListTreeServlet" name="left">
  	<frame src="" name="right">
</frameset>
</html>

修改显示树的页面listtree.jsp,页面改后的内容如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>

<!-- The xtree script file -->
<script src="${pageContext.request.contextPath }/js/xtree.js"></script>

<!-- Modify this file to change the way the tree looks -->
<link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath }/css/xtree.css">

<script type="text/javascript">
	<c:forEach var="c" items="${list }">
		<c:if test='${c.depth==1 }'> 
			var tree = new WebFXTree('${c.name }');
			tree.action = "${pageContext.request.contextPath }/ViewCategoryServlet?id=${c.id }";
			tree.target = "right";
		</c:if>
		
		<c:if test='${c.depth==2 }'> 
			var node${c.depth } = new WebFXTreeItem('${c.name }'); // node2
			tree.add(node${c.depth });
			node${c.depth}.action = "${pageContext.request.contextPath }/ViewCategoryServlet?id=${c.id }";
			node${c.depth}.target = "right";
		</c:if>
		
		<c:if test='${c.depth>2 }'> 
			var node${c.depth } = new WebFXTreeItem('${c.name }'); // node3
			node${c.depth-1 }.add(node${c.depth }); 	// node2.add(node3)
			node${c.depth}.action = "${pageContext.request.contextPath }/ViewCategoryServlet?id=${c.id }";
			node${c.depth}.target = "right";
		</c:if>
	</c:forEach>
	document.write(tree);
</script>
</head>
<body>
	
</body>
</html>

在cn.itcast.web.controller包中创建一个Servlet——ViewCategoryServlet,用于处理显示web树节点详细信息的请求。
这里写图片描述
ViewCategoryServlet的具体代码如下:

public class ViewCategoryServlet extends HttpServlet {

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		
		String id = request.getParameter("id");
		BusinessService service = new BusinessService();
	 	Category category = service.getCategory(id);
	 	
	 	request.setAttribute("c", category);
	 	request.getRequestDispatcher("/addcategory.jsp").forward(request, response);

	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}

}

在WebRoot根目录下新建一个添加分类的页面——addcategory.jsp。
这里写图片描述
addcategory.jsp页面的内容为:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>添加子类别</title>
</head>
<body>
	<br/>
	
	您当前所在的位置:&nbsp;&nbsp;&nbsp;&nbsp;
	<c:forEach var="parent" items="${c.parents }">
		${parent.name } >>> 
	</c:forEach>
	${c.name }
	
	<br/><br/>
	分类id:${c.id }&nbsp;&nbsp;&nbsp;&nbsp;
	分类名称:${c.name }
	
	<form action="${pageContext.request.contextPath }/AddCategoryServlet" method="post">
		<input type="hidden" name="pid" value="${c.id }"> <!-- 添加子节点的父节点id -->
		<input type="text" name="name">
		<input type="submit" value="添加子类">
	</form>
</body>
</html>

在cn.itcast.web.controller包中创建一个Servlet——AddCategoryServlet,用于处理添加web树节点的请求。
这里写图片描述
AddCategoryServlet的具体代码如下:

public class AddCategoryServlet extends HttpServlet {

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		
		try {
			request.setCharacterEncoding("UTF-8");
			
			String parent_id = request.getParameter("pid");
			String name = request.getParameter("name");
			
			BusinessService service = new BusinessService();
			service.addCategory(parent_id, name);
			
			request.setAttribute("message", "添加成功!!!");
		} catch (Exception e) {
			e.printStackTrace();
			request.setAttribute("message", "添加失败!!!");
		}
		request.getRequestDispatcher("/message.jsp").forward(request, response);

	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}

}

最后在WebRoot根目录下新建一个全局消息显示页面——message.jsp。
这里写图片描述
message.jsp页面的内容为:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	${message }
</body>
</html>

至此,给web树动态地添加节点的功能,我们就已经实现了,小伙伴们,还不来快试试!!!
这里写图片描述

以上是关于实现web树的主要内容,如果未能解决你的问题,请参考以下文章

十条jQuery代码片段助力Web开发效率提升

十条jQuery代码片段助力Web开发效率提升

二叉查找树简单实现

PHP必用代码片段

线段树详解

JAVA WEB代码片段