在Web開發中經常遇到樹形數據的操作,如菜單、組織機構、行政區(省、市、縣)等具有層級關係的數據。下面以行政區爲例說明樹形數據(層級關係數據)的存儲以及實現,效果如圖所示。
1 數據庫表結構設計
樹形數據一般通過父節點和子節點實現數據之間的層級關聯,層級關係在數據庫中主要通過主鍵和外鍵來實現。
--使用Oracle數據庫
--創建行政區表
create table TB_XZQ
(
code NUMBER not null, --行政區編碼,主鍵
parent_code NUMBER, --上級行政區編碼,如果沒有上級行政區,則爲空
name VARCHAR2(50) --行政區名稱
);
--設置CODE爲主鍵
alter table TB_XZQ add constraint PK_TB_XZQ primary key (CODE) using index;
--設置外鍵
alter table TB_XZQ
add constraint FK_TB_XZQ_PARENT_CODE foreign key (PARENT_CODE)
references TB_XZQ (CODE) on delete set null;
--插入行政區數據
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (420000, NULL, '湖北省');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (420100, 420000, '武漢市');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (420101, 420100, '市轄區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (420102, 420100, '江岸區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (420103, 420100, '江漢區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (420104, 420100, '礄口區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (420105, 420100, '漢陽區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (421000, 420000, '荊州市');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (421001, 421000, '市轄區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (421002, 421000, '沙市區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (421003, 421000, '荊州區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (430000, NULL, '湖南省');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (430100, 430000, '長沙市');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (430101, 430100, '市轄區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (430102, 430100, '芙蓉區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (430103, 430100, '天心區');
INSERT INTO TB_XZQ (CODE,PARENT_CODE,NAME) VALUES (430104, 430100, '嶽麓區');
插入後數據如下圖所示
2 樹形數據Java實現
通常情況下,從數據庫中讀取的數據需要轉換爲樹形結構。TreeNode是一個Java樹形數據實現類,通過靜態方法buildTree可以方便的把TreeNode構建成樹。
import java.util.ArrayList;
import java.util.List;
/**
* 樹節點,支持Ext、zTree等Web控件
*
* @author [email protected]
* @param <T> 樹節點的綁定數據類
*/
public class TreeNode<T> {
/**
* 樹節點id
* 爲了兼容多種情況,使用String類型
*/
private String id;
/**
* 樹節點上級id
*/
private String parentId;
/**
* 樹節點顯示文本
*/
private String text;
/**
* 樹節點名稱,內容和text一樣
* 該字段主要是爲了兼容Ext和zTree
*/
private String name;
/**
* 是否爲葉子節點
*/
private Boolean leaf = true;
private Boolean expanded = false;
private T nodeData;
/**
* 是否爲父節點,該字段和leaf重複,主要是爲了兼容Ext和zTree
*/
private Boolean isParent = false;
/**
* 子節點,如果沒有子節點,則列表長度爲0
*/
private List<TreeNode<T>> children = new ArrayList<TreeNode<T>>();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public String getText() {
return text;
}
public void setText(String text) {
this.name = text;
this.text = text;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
this.text = name;
}
public Boolean getExpanded() {
return expanded;
}
public void setExpanded(Boolean expanded) {
this.expanded = expanded;
}
public List<TreeNode<T>> getChildren() {
return children;
}
public void setLeaf(Boolean leaf) {
this.leaf = leaf;
this.isParent = !leaf;
}
public Boolean getLeaf() {
return this.leaf;
}
public Boolean getIsParent() {
return isParent;
}
public void setIsParent(Boolean isParent) {
this.isParent = isParent;
this.leaf = !isParent;
}
public T getNodeData() {
return nodeData;
}
public void setNodeData(T nodeData) {
this.nodeData = nodeData;
}
/**
* 把樹節點列表構造成樹,最後返回樹的根節點,如果傳入的列表有多個根節點,會動態創建一個根節點。
* @param nodes 樹節點列表
* @return 根節點
*/
public static <T> TreeNode<T> buildTree(List<TreeNode<T>> nodes){
if(nodes == null || nodes.size() == 0){
return null;
}
if(nodes.size() == 1){
return nodes.get(0);
}
//用來存放nodes裏面的頂級樹節點
//也就是把沒有父節點的節點都放到tops裏面去
List<TreeNode<T>> tops = new ArrayList<TreeNode<T>>();
boolean hasParent = false;
//第一次遍歷,獲取一個節點作爲子節點
for(TreeNode<T> child : nodes){
hasParent = false;
//當前節點child的父節點id
String pid = child.getParentId();
//如果pid不存在或爲空
//則當前節點爲頂級節點
if(pid == null || pid.equals("")){
//把當前節點添加到tops中作爲頂級節點
tops.add(child);
//跳過當前節點,進入下一輪
continue;
}
//遍歷nodes上的所有節點,判斷是否有child的父節點
for(TreeNode<T> parent : nodes){
String id = parent.getId();
//如果parent節點的id等於child節點的pid,則parent節點是child節點的父節點
if(id != null && id.equals(pid)){
//把child加到parent下
parent.getChildren().add(child);
parent.setLeaf(false);
//child節點有父節點
hasParent = true;
continue;
}
}
//如果child節點沒有父節點,則child是頂級節點
//把child添加到tops中
if(!hasParent){
tops.add(child);
}
}
TreeNode<T> root;
if(tops.size() == 1){
//如果頂級節點只有一個,該頂級節點是根節點
root = tops.get(0);
}else{
//如果頂級節點有多個,創建一個根節點,把頂級節點放到根節點下
root = new TreeNode<T>();
root.setLeaf(false);
root.setId("-1");
root.setName("root");
root.setParentId("");
root.getChildren().addAll(tops);
}
return root;
}
}
3 生成行政區樹
Dao(只列出主要代碼)
@Repository("xzqDao")
public class XzqDaoImpl implements XzqDao {
@Resource
private JdbcTemplate jdbcTemplate;
public List<XzqEntity> select() {
String sql = "SELECT CODE,PARENT_CODE,NAME FROM TB_XZQ";
//用Spring JDBC進行數據庫操作
return jdbcTemplate.query(sql, new RowMapper<XzqEntity>(){
public XzqEntity mapRow(ResultSet rs, int index) throws SQLException {
XzqEntity xzq = new XzqEntity();
xzq.setCode(rs.getInt("CODE"));
xzq.setName(rs.getString("NAME"));
xzq.setParentCode(rs.getInt("PARENT_CODE"));
return xzq;
}
});
}
}
Service(只列主要代碼)
@Service("xzqService")
public class XzqServiceImpl implements XzqService {
@Resource
private XzqDao xzqDao;
public TreeNode<XzqEntity> tree(){
List<XzqEntity> list = xzqDao.select();
List<TreeNode<XzqEntity>> nodes = new ArrayList<TreeNode<XzqEntity>>();
//把行政區類轉爲樹節點
for(XzqEntity xzq : list){
TreeNode<XzqEntity> node = new TreeNode<XzqEntity>();
//節點id
node.setId(xzq.getCode().toString());
//節點上級id
node.setParentId(xzq.getParentCode().toString());
node.setText(xzq.getName());
//把行政區類放到節點數據中,以備使用
node.setNodeData(xzq);
nodes.add(node);
}
return TreeNode.buildTree(nodes);
}
}
Controller(只列出主要代碼)
@Controller
@RequestMapping("/xzq")
public class XzqController {
@Resource
private XzqService xzqService;
/**
* 行政區樹,返回JSON格式
*
* @param response
*/
@RequestMapping("/tree.mvc")
public void tree(HttpServletResponse response) {
String json = "";
try {
json = JSON.toJSONString(xzqService.tree());
} catch (Exception e) {
e.printStackTrace();
}
//輸出JSON數據
//這裏直接通過response輸出JSON字符串
//Spring MVC也提供了輸出JSON數據的方法
// 設置編碼格式
response.setContentType("text/plain;charset=utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter out = null;
try {
out = response.getWriter();
out.write(json);
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ztree顯示行政區樹
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
String context = request.getContextPath();
%>
<!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">
<link type="text/css" rel="stylesheet" href="./lib/ztree/zTreeStyle.css" />
<script type="text/javascript" src="./lib/jquery/jquery-1.11.2.min.js"></script>
<script type="text/javascript" src="./lib/ztree/jquery.ztree.all-3.5.min.js"></script>
<title>行政區</title>
</head>
<body>
<!-- 行政區樹 -->
<ul class="ztree" id="xzqtree" style="width:180px;height:350px;margin:10px;border:1px solid blue;overflow:auto;"></ul>
<script type="text/javascript">
$(function(){
//獲取行政區數據
$.ajax({
url: './xzq/tree.mvc',
dataType: 'json'
}).done(function(data){
if(!data){
return;
}
//初始化行政區樹
$.fn.zTree.init($('#xzqtree'), {}, data);
});
});
</script>
</body>
</html>