1. 數據庫設計
我們在工作中經常會用到樹型結構的數據,比如公司的部門結構,倉庫物品的分類等。一般這些樹的結構,都是任意層級的,而非固定的幾層結構。此時,我們就要用到樹形的數據結構。以下,將會以部門樹爲例進行描述。
數據庫表結構: |
部門Id----departmentId |
部門Url----url |
部門名稱----departmentName |
父級部門Id----superDepartmentId |
一個公司的部門信息,一定是要先將其持久化到數據庫中的。因爲部門的結構是一種無限層級的樹結構,因此,我們在設計數據庫時,在部門表的部門Id和部門名稱這兩個字段的基礎上,要增加兩個字段,父部門Id和部門的Url鏈接。
父部門Id:該字段用於在進行查詢時使用,如果一個部門A的父部門Id爲部門B的部門Id,我們則會將部門A,看作部門B的多個直接子部門之一,依次類推。
部門Url:如果我們有一個部門A,部門A的父部門爲部門B,部門B的父部門爲部門C,則部門Url則爲 部門CId_部門BId_部門AId。這個字段一般用來查詢一個部門的所有下屬子部門(包括非直接子部門),在樹結構中不做使用。
2. 從數據庫獲取部門樹
a. Model類FrameworkTree
在類FrameworkTree中,有多個屬性,這裏不一一列舉,只對重要的幾個屬性進行描述
String Id----實際上爲部門的Url,但是因爲使用的前端框架爲easyui的Tree,所以在這裏id實際上是url。
String thisId----該Model所代表的部門的實際Id
String text----在easyui中,進行顯示的文本,此處填充的爲部門名
List<FrameworkTree> children----該對象存儲的爲這個部門的所有子部門對象。
b. Dao層Sql語句:
<!-- 根據父部門Id查詢部門列表 -->
<select id="getLowerBySuperId" parameterType="com.mdoa.framework.model.FrameworkTree"
resultType="com.mdoa.framework.model.FrameworkTree">
SELECT
department_id AS thisId,
url AS id,
department_name AS text
FROM
framework_department
WHERE
super_department_id = #{thisId,jdbcType=CHAR}
AND alive_flag = '1'
ORDER BY
create_time DESC
</select>
因爲我們所使用的特殊數據庫結構,這裏的Sql語句僅僅根據父級部門Id來查詢該父級部門的所有直系子部門,所以我們要將這些部門以樹的形式全部查出來,就需要使用遞歸的形式來進行查詢。
c. Service層處理
/**
* 爲單例化的部門結構注入結構
* 部門結構從數據庫中進行獲取
*/
public List<FrameworkTree> injectFrameworkDepartment(FrameworkTree superDepartment){
//根據父級部門Id查詢該部門下的下一級子部門
List<FrameworkTree> departments = departmentDao.getLowerBySuperId(superDepartment);
//遞歸調用,查詢每一層的子部門的下一層級子部門,並設置進部門中
for(FrameworkTree department : departments){
department.setChildren(this.injectFrameworkDepartment(department));
}
//返回所有的部門信息
return departments;
}
我們在Service層中,採用的是遞歸的形式,首先需要傳入一個FrameworkTree對象,該對象爲我們在使用時的父級部門模型。
d. Controller層處理
我們在來看一下controller層啓動遞歸的處理辦法
FrameworkTree superDepartment = new FrameworkTree();
superDepartment.setText("XXXX有限公司");
superDepartment.setId("0000");
superDepartment.setThisId("0000");
superDepartment.setChildren(departmentService.injectFrameworkDepartment(superDepartment));
在Controller層中,我們new了一個根級別的部門,這個部門的部門Id,是所有部門最初級別的父部門,相當與java中的Object。我們利用這個最初節點,來對Service層中的遞歸進行啓動,啓動遞歸後,經過代碼的遞歸,我們可以獲得到一個所有部門的樹結構,這個樹結構,反饋給前端的easyui後,easyui就可以直接進行解析了。
3. 部門樹的單例化
在這個部門樹中,存在着一個重大的問題,那就是這個樹本身在創建的時候,是需要多次對數據庫進行請求的。每當在數據庫中找到一個部門以後,就需要重新調用dao層方法,對這個部門的所有子部門進行查詢。因此,我們需要對這個部門樹進行單例化。
Controller層完整代碼
/**
* 獲取公司的部門結構信息,如果單例化的部門結構信息是空的,則調用service層中的方法,爲單例化的對象注入結構
* @return 部門信息json
*/
@RequestMapping("getFramework.do")
public String getFramework(){
try{
Gson gson = new Gson();
if(FrameWorkConstant.frameworkDepartments != null){
String json = gson.toJson(FrameWorkConstant.frameworkDepartments);
return json;
}else{
synchronized(this){
if(FrameWorkConstant.frameworkDepartments != null){
String json = gson.toJson(FrameWorkConstant.frameworkDepartments);
return json;
}
FrameworkTree superDepartment = new FrameworkTree();
superDepartment.setText("XXXX有限公司");
superDepartment.setId("0000");
superDepartment.setThisId("0000");
superDepartment.setChildren(departmentService.injectFrameworkDepartment(superDepartment));
FrameWorkConstant.frameworkDepartments = new LinkedList<FrameworkTree>();
FrameWorkConstant.frameworkDepartments.add(superDepartment);
String json = gson.toJson(FrameWorkConstant.frameworkDepartments);
return json;
}
}
}catch(Exception e){
e.printStackTrace();
return Constant.SERVER_ERROR_CODE;
}
}
單例化部門樹結構的時候,只需要進行判斷是否已經創建了這個靜態對象即可,如果已經創建了對象,則直接返回該對象,如果未創建,則創建一個對象。
我們在這裏需要注意的一個問題,在啓動服務器後,在第一次獲取部門信息的時候,如果出現並發現象,可能依然會出新兩次創建對象賦值的情況,因此,我們要使用synchronized將部分代碼進行鎖定。鎖定的代碼中,需要重新判斷是否爲空。
4. 部門的增刪改查處理
在進行對部門的增刪改查時,因爲我們所獲取的部門結構是位於內存中的單例部門,而不是數據庫中的部門,所以我們在對數據庫進行增刪改查操作時,也需要對單例的部門樹進行處理。比如修改部門名稱時,要先修改部門的數據庫中的名稱,再修改單例部門樹中的數據。
在對單例部門樹中的數據進行尋址的代碼如下:
/**
* 通過Url獲取樹節點
* @param url
* @return
*/
public FrameworkTree getTargetByUrl(String url, List<FrameworkTree> targets){
FrameworkTree child = null;
for(FrameworkTree target:targets){
if (StringUtil.isInclude(url, target.getId())) {
if(url.equals(target.getId())){
return target;
}else{
child = getTargetByUrl(url, target.getChildren());
}
break ;
}
}
return child;
}
在我們擁有了通過節點Url來尋找節點的方法以後,對部門的修改與刪除就變得簡單化了。比如,我們需要將這個節點刪除,則可以設置爲null,需要修改部門名稱,則修改節點中的text,即可實現數據庫中的信息與單例樹的信息一致。
5. 節點拖動
當我們需要進行節點拖動時(如現有部門A,父部門爲B,將A修改至部門C下),依然要對數據庫進行處理,然後再對單例的樹進行處理。要注意的是,在拖動後,該節點的所有子節點的Url也需要進行修改。
Service層拖動代碼
/**
* 更改部門的父部門,在前端通過拖動的方式修改
*/
public void moveDepartment(String startNodeUrl, String endNodeUrl,HttpServletRequest reuqest){
String newUrl = endNodeUrl + "_" +StringUtil.getIdFromUrl(startNodeUrl);
String superDeptId = StringUtil.getIdFromUrl(endNodeUrl);
HashMap<String ,String> params = new HashMap<String, String>();
params.put("newUrl",newUrl);
params.put("superDeptId",superDeptId);
params.put("startNodeUrl",startNodeUrl);
UserInfo userInfo = getUser(reuqest);
params.put("updateUserId", userInfo.getUserId());
params.put("updateUserName", userInfo.getUserName());
if(!departmentDao.updateDepartmentUrl(params)){
throw new RuntimeException("更改部門Url失敗");
}
params.put("startNodeUrl", "'" + startNodeUrl + "%'");
departmentDao.updateChildDepartmentUrl(params);
//獲取所移動的節點的原始父節點
FrameworkTree oldParent = this.getTargetByUrl(
startNodeUrl.substring(0, startNodeUrl.lastIndexOf("_")), FrameWorkConstant.frameworkDepartments);
//尋找原始父節點下的所移動的節點
for(int i = 0; i < oldParent.getChildren().size() ; i++){
FrameworkTree target = oldParent.getChildren().get(i);
if(target.getId().equals(startNodeUrl)){
//尋找新的位置的父節點
FrameworkTree newParent = this.getTargetByUrl(endNodeUrl, FrameWorkConstant.frameworkDepartments);
//添加到新的父節點下
newParent.getChildren().add(target);
//替換Url爲新的Url
target.setId(newUrl);
//獲取所有移動的節點的子節點
List<FrameworkTree> childs = this.getAllChildren(target);
//獲取新的Url
for(FrameworkTree child : childs){
//替換所有的舊父節點Url爲新的父節點url
child.getId().replace(startNodeUrl, newUrl);
}
//從舊的父節點下移除目標子節點
oldParent.getChildren().remove(target);
break ;
}
}
}
數據庫處理sql如下:
<!-- 根據部門的Url來修改部門的父級Url和父級部門Id -->
<update id="updateDepartmentUrl" parameterType="java.util.HashMap">
UPDATE framework_department
SET
super_department_id = #{superDeptId},
url = #{newUrl},
update_time = NOW(),
update_user_id = #{updateUserId},
update_user_name = #{updateUserName}
WHERE
url = #{startNodeUrl}
</update>
<!-- 修改數據庫中被拖動的部門節點的所有子節點的Url -->
<update id="updateChildDepartmentUrl" parameterType="java.util.HashMap">
UPDATE framework_department
SET
url = REPLACE(url,#{startNodeUrl},#{newUrl}),
update_time = NOW(),
update_user_id = #{updateUserId},
update_user_name = #{updateUserName}
WHERE
url LIKE (${startNodeUrl})
</update>
獲取一個節點的所有子節點方法:
/**
* 通過節點的目標,獲取該節點的所有子節點
*/
public List<FrameworkTree> getAllChildren(FrameworkTree target){
List<FrameworkTree> childs = target.getChildren();
for(FrameworkTree child: childs){
childs.addAll(getAllChildren(child));
}
return childs;
}