【原創】無快取資料庫下,部門樹結構處理--轉載請註明出處
1. 資料庫設計
我們在工作中經常會用到樹型結構的資料,比如公司的部門結構,倉庫物品的分類等。一般這些樹的結構,都是任意層級的,而非固定的幾層結構。此時,我們就要用到樹形的資料結構。以下,將會以部門樹為例進行描述。
資料庫表結構: |
部門Id----departmentId |
部門Url----url |
部門名稱----departmentName |
父級部門Id----superDepartmentId |
一個公司的部門資訊,一定是要先將其持久化到資料庫中的。因為部門的結構是一種無限層級的樹結構,因此,我們在設計資料庫時,在部門表的部門
父部門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;
}