- 前言
作為電商網站,必然要有商品類目表,以便商品分類檢索。而設計商品類目表,又是一件特別繁雜的事情。一件商品可能有多個類目來檢索出來,比如蘋果手機,可以從品牌檢索也可以從手機檢索。一個類目對應多個商品,比如手機下對應了多款屬於手機類的商品。而類目是一個多叉樹結構,類似於文件夾的結構。通常大電商都是把類目整理好了放在cache上,就不用頻繁訪問數據庫和整理排序了。
- 個人類目標的結構設計
參考了一下網上無限級分類的表設計,簡化模型為一個表內存在父子級從屬關系。
表中的isRoot可以去掉,只要parent為0就認為是根節點就好了。而為什么不存子節點而是存父節點,因為子節點的映射類,生成整理的時候比存父節點要復雜一些,當然程序寫的不復雜而是這個問題需要花費的時間就會多一些。
表設計好了之后,就需要查詢數據並且整理出他們的從屬關系。為了減少數據庫的查詢次數,我采用了全部查詢,在內存中生成多叉樹的形式。節點類的定義想法非常直接,它存在唯一識別:id,真實信息:name,以及它的子樹:List or Map。
/* * 樹型存儲 */ class CategoryTree{ private long id; private String name; private List<CategoryTree> children; public CategoryTree(){ children = new ArrayList<>(); } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<CategoryTree> getChildren() { return children; } public void setChildren(List<CategoryTree> children) { this.children = children; } } /* * 哈希表存儲 */ class CategoryMap{ private long id; private String name; private Map<Long,CategoryMap> children; public CategoryMap(){ children = new HashMap<>(); } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Map<Long, CategoryMap> getChildren() { return children; } public void setChildren(Map<Long, CategoryMap> children) { this.children = children; } }
在對亂序數組生成三級n叉樹這個過程中,最快的方式是用map。在生成過程中,只能一級級生成,因為沒有父節點不可能有子節點跟隨這個邏輯的存在。
//集合存儲 public void test1(){ tool.connection2MYSQL(); Connection conn = tool.getCon(); String sql = "select * from category"; Statement stm = null; List<category> clist = new ArrayList<>(); try { stm = conn.createStatement(); ResultSet rs = stm.executeQuery(sql); while(rs.next()){ category c = new category(); c.setId(rs.getLong("id")); c.setName(rs.getString("name")); c.setParentId(rs.getLong("parent_id")); c.setIsRoot(rs.getInt("is_root")); clist.add(c); } tool.closeConn(conn); } catch (SQLException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } /* * 檢查集合 */ // for(category ca:clist){ // System.out.println("id: "+ca.getId()+" name: "+ca.getName()+" parentId: "+ca.getParentId()+" isRoot: "+ca.getIsRoot()); // } /** * 邏輯嘗試===== */ List<CategoryTree> roots = new ArrayList<>(); List<CategoryTree> second = new ArrayList<>(); List<CategoryTree> third = new ArrayList<>(); //一次遍歷 添加根節點 int i = 0; while(i != clist.size()-1){ if(clist.get(i).getParentId() == 0){ CategoryTree ct = new CategoryTree(); ct.setId(clist.get(i).getId()); ct.setName(clist.get(i).getName()); roots.add(ct); clist.remove(i); }else i++; } //二次遍歷 添加二級節點 for(int j=0;j<roots.size();j++){ i = 0; while(i < clist.size()){ if(clist.get(i).getParentId() == roots.get(j).getId()){ CategoryTree ct = new CategoryTree(); ct.setId(clist.get(i).getId()); ct.setName(clist.get(i).getName()); roots.get(j).getChildren().add(ct); second.add(ct);//用空間換 clist.remove(i); }else i++; } } //三次遍歷 添加三級節點 for(int j=0;j<second.size();j++){ i = 0; while(i < clist.size()){ if(clist.get(i).getParentId() == second.get(j).getId()){ CategoryTree ct = new CategoryTree(); ct.setId(clist.get(i).getId()); ct.setName(clist.get(i).getName()); second.get(j).getChildren().add(ct); third.add(ct);//用空間換 clist.remove(i); }else i++; } } for(category ca:clist){ System.out.println("id: "+ca.getId()+" name: "+ca.getName()+" parentId: "+ca.getParentId()+" isRoot: "+ca.getIsRoot()); } for(CategoryTree ct:roots){ System.out.println("id: "+ct.getId()+" name: "+ct.getName()); { for(CategoryTree ct1:ct.getChildren()) { System.out.println("二級 id: "+ct1.getId()+" name: "+ct1.getName()); for(CategoryTree ct2:ct1.getChildren()) System.out.println("三級 id: "+ct2.getId()+" name: "+ct2.getName()); } } }/** * 邏輯嘗試===== */ }
我對每一級的節點做了一個額外的存儲,在第三級生成的時候簡化一個循環,也就是n平方的復雜度。
使用map生成的話,僅在生成這個過程,就可以把問題化簡成n的復雜度。因為Map“知道”自己存儲的對象的id,而List要通過遍歷才知道它的自己存的元素的id。
public void test2(){ tool.connection2MYSQL(); Connection conn = tool.getCon(); String sql = "select * from category"; Statement stm = null; List<category> clist = new ArrayList<>(); try { stm = conn.createStatement(); ResultSet rs = stm.executeQuery(sql); while(rs.next()){ category c = new category(); c.setId(rs.getLong("id")); c.setName(rs.getString("name")); c.setParentId(rs.getLong("parent_id")); c.setIsRoot(rs.getInt("is_root")); clist.add(c); } tool.closeConn(conn); } catch (SQLException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } /** * 邏輯嘗試===== */ Map<Long,CategoryMap> rootMap = new HashMap<>(); Map<Long,CategoryMap> secondMap = new HashMap<>(); //遍歷一級 int i = 0; while(i < clist.size()){ if(clist.get(i).getParentId() == 0){ CategoryMap cm = new CategoryMap(); cm.setId(clist.get(i).getId()); cm.setName(clist.get(i).getName()); rootMap.put(cm.getId(),cm); clist.remove(i); }else i++; } //遍歷二級 i = 0; while (i < clist.size()) { if (rootMap.get(clist.get(i).getParentId()) != null) { CategoryMap cm = new CategoryMap(); cm.setId(clist.get(i).getId()); cm.setName(clist.get(i).getName()); rootMap.get(clist.get(i).getParentId()).getChildren().put(cm.getId(), cm); secondMap.put(cm.getId(), cm); clist.remove(i); } else i++; } //遍歷三級 i = 0; while (i < clist.size()) { if (secondMap.get(clist.get(i).getParentId()) != null) { CategoryMap cm = new CategoryMap(); cm.setId(clist.get(i).getId()); cm.setName(clist.get(i).getName()); secondMap.get(clist.get(i).getParentId()).getChildren().put(cm.getId(), cm); clist.remove(i); } else i++; } // for (Map.Entry<Long, CategoryMap> entry : rootMap.entrySet()) { // System.out.println("Key = " + entry.getKey() + ", id : " + entry.getValue().getId()+" name : "+entry.getValue().getName()); // for (Map.Entry<Long, CategoryMap> entry1 : entry.getValue().getChildren().entrySet()){ // System.out.println("二級 Key = " + entry1.getKey() + ", id : " + entry1.getValue().getId()+" name : "+entry1.getValue().getName()); // for (Map.Entry<Long, CategoryMap> entry2 : entry1.getValue().getChildren().entrySet()){ // System.out.println("三級 Key = " + entry2.getKey() + ", id : " + entry2.getValue().getId()+" name : "+entry2.getValue().getName()); // } // } // // } JSONArray json = new JSONArray(); for (CategoryMap entry : rootMap.values()) { JSONObject job1 = new JSONObject(); job1.put("id", entry.getId()); job1.put("name", entry.getName()); JSONArray joa1 = new JSONArray(); // System.out.println("id : " + entry.getId()+" name : "+entry.getName()); for (CategoryMap entry1 : entry.getChildren().values()){ JSONObject job2 = new JSONObject(); job2.put("id", entry1.getId()); job2.put("name", entry1.getName()); JSONArray joa2 = new JSONArray(); // System.out.println("二級 id : " + entry1.getId()+" name : "+entry1.getName()); for (CategoryMap entry2 : entry1.getChildren().values()){ JSONObject job3 = new JSONObject(); job3.put("id", entry2.getId()); job3.put("name", entry2.getName()); joa2.add(job3); // System.out.println("三級 id : " + entry2.getId() + " name : "+entry2.getName()); } job2.put("chird", joa2); joa1.add(job2); } job1.put("chird", joa1); json.add(job1); } for(int k=0;k<json.size();k++){ JSONObject jo = json.getJSONObject(k); System.out.println(jo.toString()); } /** * 邏輯嘗試===== */ }
最后的生成json的時候,仍然需要三次方的復雜度,我在考慮如何能在整理過程中順帶生成json,現在還沒做出來,不過優化成n應該還是有機會的。
另外,遍歷源數據的時候,把抽掉的節點remove掉也是一種減少重復遍歷的方式。
最后生成的結果如同預期。
連接成功
{"chird":[{"chird":[{"name":"襯衣","id":16}],"name":"七匹狼","id":6},{"chird":[{"name":"運動服","id":17}],"name":"阿迪達斯","id":7}],"name":"男裝","id":1}
{"chird":[{"chird":[{"name":"毛衣","id":18}],"name":"zara","id":8},{"chird":[{"name":"包包","id":19}],"name":"普拉達","id":9}],"name":"女裝","id":2}
{"chird":[{"chird":[{"name":"筆記本電腦","id":20}],"name":"dell","id":10},{"chird":[{"name":"台式電腦","id":21}],"name":"lenovo","id":11}],"name":"電腦","id":3}
{"chird":[{"chird":[{"name":"note","id":22}],"name":"三星","id":12},{"chird":[{"name":"iPhone","id":23}],"name":"蘋果","id":13}],"name":"手機","id":4}
{"chird":[{"chird":[{"name":"第一版","id":24}],"name":"Java程序設計","id":14},{"chird":[{"name":"第三版","id":25}],"name":"C++程序設計","id":15}],"name":"圖書","id":5}
- 總結
對於表設計和這個問題的解決不知道有沒有更加規范一點的方式,還需要繼續探討。對於我們項目來說,這個方法已經可以適應了,商品分類我打算用個分類表來做映射,類目表葉子節點的id和商品的id存入,檢索的時候根據葉子節點id檢索所有商品。本次問題,在於對亂序數組轉化為多叉樹的模型建立,解決了這個問題,對轉化思維又有了進一步的提高。(在這里其實問題的階並不重要,因為這是有窮集合,類目表的條目基本不會超過幾百,考慮的最多的就是訪問量的問題,因為小電商不會做cache,每一次訪問都生成三級樹,所以本次問題重在如何用更好的運算模型去解決同一個問題)
- 追加
關於三級關系樹的運用,我做了一個小的地域管理頁面,與類目樹是一樣的結構。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@include file="/common.jsp" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <script type="text/javascript" src="js/jquery.js" ></script> <script type="text/javascript" src="js/jquery.form.js"></script> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>地域信息</title> <style> .chosen{ background:blue; } .select{ width:30px; } </style> </head> <body> <div> <div style="margin-left:20px;float:left;"> <h2>一級類別</h2> <h3>當前:${current1.name}</h3> <form action="letao_DevelopmentModelArea_addArea" id="addForm1"> 編碼:<input size="8" name="code"> name:<input size="8" name="name"> <input size="8" name="parent" type="hidden" value="0"> <br/> </form> <br/> <button class="add1">添加</button> <table style="border: 2px solid BLACK;"> <thead> <tr> <th>選擇</th> <th>id</th> <th>name</th> <th>編碼</th> <th>操作</th> </tr> </thead> <s:iterator id="r1" value="#rank1"> <s:if test="#current1.id == #r1.id"> <tr class="chosen" type="${ r1.id}"> <td><button class="select" id="${ r1.id}" type="1">選擇</button></td> <td>${ r1.id}</td> <td><input value ="${ r1.name}" id="name${ r1.id}" size="8" ></td> <td><input value ="${ r1.code}" id="code${ r1.id}" size="8"></td> <td><button type="${ r1.id}" class="update">更新</button> | <button type="${ r1.id}" class="del">刪除</button></td> </tr> </s:if> <s:else> <tr type="${ r1.id}"> <td><button class="select" id="${ r1.id}" type="1">選擇</button></td> <td>${ r1.id}</td> <td><input value ="${ r1.name}" id="name${ r1.id}" size="8" ></td> <td><input value ="${ r1.code}" id="code${ r1.id}" size="8"></td> <td><button type="${ r1.id}" class="update">更新</button> | <button type="${ r1.id}" class="del">刪除</button></td> </tr> </s:else> </s:iterator> </table> </div> <div style="margin-left:20px;float:left;"> <h2>二級類別</h2> <h3>上級:${current1.name}</h3> <form action="letao_DevelopmentModelArea_addArea" id="addForm2"> 編碼:<input size="8" name="code"> name:<input size="8" name="name"><input size="8" name="parent" type="hidden" value="${current1.id}"> <br/> </form> <br/> <button class="add2">添加</button> <table style="border: 2px solid BLACK;"> <thead> <tr> <th>選擇</th> <th>id</th> <th>name</th> <th>編碼</th> <th>操作</th> </tr> </thead> <s:iterator id="r2" value="#rank2"> <s:if test="#current2.id == #r2.id"> <tr class="chosen" type="${ r2.id}"> <td><button class="select" id="${ r2.id}" type="2">選擇</button></td> <td>${ r2.id}</td> <td><input value ="${ r2.name}" id="name${ r2.id}" size="8"></td> <td><input value ="${ r2.code}" id="code${ r2.id}" size="8"></td> <td><button type="${ r2.id}" class="update">更新</button> | <button type="${ r2.id}" class="del">刪除</button></td> </tr> </s:if> <s:else> <tr type="${ r2.id}"> <td><button class="select" id="${ r2.id}" type="2">選擇</button></td> <td>${ r2.id}</td> <td><input value ="${ r2.name}" id="name${ r2.id}" size="8"></td> <td><input value ="${ r2.code}" id="code${ r2.id}" size="8"></td> <td><button type="${ r2.id}" class="update">更新</button> | <button type="${ r2.id}" class="del">刪除</button></td> </tr> </s:else> </s:iterator> </table> </div> <div style="margin-left:20px;float:left;"> <h2>三級類別</h2> <h3>上級:${current1.name}->${current2.name}</h3> <form action="letao_DevelopmentModelArea_addArea" id="addForm3"> 編碼:<input size="8" name="code"> name:<input size="8" name="name"> <input size="8" name="parent" type="hidden" value="${current2.id }"> <br/> </form> <br/> <button class="add3">添加</button> <table style="border: 2px solid BLACK;"> <thead> <tr> <th>id</th> <th>name</th> <th>編碼</th> <th>操作</th> </tr> </thead> <s:iterator id="r3" value="#rank3"> <tr> <td>${ r3.id}</td> <td><input value ="${ r3.name}" id="name${ r3.id}" size="8"></td> <td><input value ="${ r3.code}" id="code${ r3.id}" size="8"></td> <td><button type="${ r3.id}" class="update">更新</button> | <button type="${ r3.id}" class="del">刪除</button></td> </tr> </s:iterator> </table> </div> </div> <form action="letao_DevelopmentModelArea_updateInfo" method="post" id="updateForm" style="display:none;"> <input id="hideId" type="hidden" name="id" type="hidden"> <input id="updateCode" type="hidden" name="code"> <input id="updateName" type="hidden" name="name"> </form> <form action = "letao_DevelopmentModelArea_listArea" method="post" id="listForm"> <input id="firstRankId" type="hidden" name="firstRankId" value="${current1.id }"> <input id="secondRankId" type="hidden" name="secondRankId" value="${current2.id }"> </form> </body> <script> $('.update').click(function(e){ var dataId = $(e.target).attr("type"); $("#hideId").val(dataId); $("#updateCode").val($("#code"+dataId).val()); $("#updateName").val($("#name"+dataId).val()); $('#updateForm').ajaxSubmit({ type:'post', dataType: 'json', success: function (data) { if(data.status==1){ alert(data.info); location.reload(); }else{ alert(data.info); } }, error: function (XMLResponse) { //alert(XMLResponse.responseText); var ss =JSON.stringify(XMLResponse); alert('操作失敗!'+ss); } }); }); $('.del').click(function(e){ var dataId = $(e.target).attr("type"); $.getJSON("letao_DevelopmentModelArea_deleteInfo?id=" + dataId, function(data) { if (data.status == 1) { alert(data.info); location.reload(); } else { alert(data.info); } }); }); $('.add1').click(function(){ $('#addForm1').ajaxSubmit({ type:'post', dataType: 'json', success: function (data) { if(data.status==1){ alert(data.info); location.reload(); }else{ alert(data.info); } }, error: function (XMLResponse) { //alert(XMLResponse.responseText); var ss =JSON.stringify(XMLResponse); alert('操作失敗!'+ss); } }); }); $('.add2').click(function(){ $('#addForm2').ajaxSubmit({ type:'post', dataType: 'json', success: function (data) { if(data.status==1){ alert(data.info); location.reload(); }else{ alert(data.info); } }, error: function (XMLResponse) { //alert(XMLResponse.responseText); var ss =JSON.stringify(XMLResponse); alert('操作失敗!'+ss); } }); }); $('.add3').click(function(){ $('#addForm3').ajaxSubmit({ type:'post', dataType: 'json', success: function (data) { if(data.status==1){ alert(data.info); location.reload(); }else{ alert(data.info); } }, error: function (XMLResponse) { //alert(XMLResponse.responseText); var ss =JSON.stringify(XMLResponse); alert('操作失敗!'+ss); } }); }); $('.select').click(function(e){ if($(e.target).attr("type") === "1"){ $('#firstRankId').val($(e.target).attr("id")); }else if($(e.target).attr("type") === "2"){ $('#secondRankId').val($(e.target).attr("id")); } $('#listForm').submit(); }); </script> </html>
查詢用的sql語句使用in來構造。頁面控制使用一級當前id和二級當前id作為請求參數刷新頁面。
action的設計也是一般操作數據庫的形式,不過列表作為頁面展示,而其他作為REST接口。
package com.dijing.letao.action; import java.util.List; import com.dijing.letao.DJActionSupport; import com.dijing.letao.NoNeedLogin; import com.dijing.letao.dao.AreaDao; import com.dijing.letao.model.Headquarters.Area; import com.dijing.server.web.DJAction; import com.dijing.server.web.action.JsonResult; import com.opensymphony.xwork2.ActionContext; /** * 地區開發模式校准控制器 */ public class DevelopmentModelAreaAction extends DJActionSupport{ private long id; private String name; private long parent; private long firstRankId; private long secondRankId; private long code; /** * letao_DevelopmentModelArea_listArea * @return * @throws Exception */ @NoNeedLogin public String listArea() throws Exception { return executePage(() -> { AreaDao aDao = new AreaDao(); if(firstRankId == 0 && secondRankId == 0){ List<Area> rank1 = aDao.findByParent(0); List<Area> rank2 = null; List<Area> rank3 = null; if(rank1.size() > 0){ rank2 = aDao.findByParent(rank1.get(0).getId()); ActionContext.getContext().put("current1", rank1.get(0)); } if(rank2.size() > 0){ rank3 = aDao.findByParent(rank2.get(0).getId()); ActionContext.getContext().put("current2", rank2.get(0)); } ActionContext.getContext().put("rank1", rank1); ActionContext.getContext().put("rank2", rank2); ActionContext.getContext().put("rank3", rank3); return DJAction.SHOW; }else if(firstRankId != 0 && secondRankId == 0){ List<Area> rank1 = aDao.findByParent(0); List<Area> rank2 = null; List<Area> rank3 = null; if(rank1.size() > 0){ rank2 = aDao.findByParent(rank1.get(0).getId()); ActionContext.getContext().put("current1", aDao.findByById(firstRankId)); } if(rank2.size() > 0){ rank3 = aDao.findByParent(rank2.get(0).getId()); ActionContext.getContext().put("current2", rank2.get(0)); } ActionContext.getContext().put("rank1", rank1); ActionContext.getContext().put("rank2", rank2); ActionContext.getContext().put("rank3", rank3); return DJAction.SHOW; }else if(firstRankId != 0 && secondRankId != 0){ System.out.println("==================="); List<Area> rank1 = aDao.findByParent(0); List<Area> rank2 = null; List<Area> rank3 = null; if(rank1.size() > 0){ rank2 = aDao.findByParent(rank1.get(0).getId()); ActionContext.getContext().put("current1", aDao.findByById(firstRankId)); } if(rank2.size() > 0){ rank3 = aDao.findByParent(aDao.findByById(secondRankId).getId()); ActionContext.getContext().put("current2", aDao.findByById(secondRankId)); } ActionContext.getContext().put("rank1", rank1); ActionContext.getContext().put("rank2", rank2); ActionContext.getContext().put("rank3", rank3); return DJAction.SHOW; }else return DJAction.FAILURE; }); } /** * letao_DevelopmentModelArea_updateInfo * @return * @throws Exception */ @NoNeedLogin public String updateInfo() throws Exception { return execute(() -> { if(name == null || code == 0 || id == 0) return ok(JsonResult.jsonResultError("請求為空")); AreaDao aDao = new AreaDao(); Area a = aDao.findByById(id); a.setName(name); a.setCode(code); if(a.update()) return ok(JsonResult.jsonResultSuccess("更新成功:")); else return ok(JsonResult.jsonResultError("更新失敗")); }); } /** * letao_DevelopmentModelArea_deleteInfo * @return * @throws Exception */ @NoNeedLogin public String deleteInfo() throws Exception { if(id == 0) return ok(JsonResult.jsonResultError("請求為空")); AreaDao aDao = new AreaDao(); Area a = aDao.findByById(id); if(a == null) return ok(JsonResult.jsonResultError("條目不存在")); List<Area> son1 = aDao.findByParent(a.getId()); if(son1.size() >0){ List<Area> son2 = aDao.findByParentList(son1); for(Area s:son2) s.delete(); } for(Area s:son1) s.delete(); a.delete(); return ok(JsonResult.jsonResultSuccess("刪除成功")); } /** * letao_DevelopmentModelArea_addArea * @return * @throws Exception */ @NoNeedLogin public String addArea() throws Exception { return execute(() -> { if(name == null) return ok(JsonResult.jsonResultError("請求為空")); if(parent >0){ AreaDao aDao = new AreaDao(); if(aDao.findByParent(parent) == null) ok(JsonResult.jsonResultError("父級不存在 添加不合法")); } Area a = new Area(); a.setName(name); a.setCode(code); a.setParent(parent); if(a.save()) return ok(JsonResult.jsonResultSuccess("添加成功:")); else return ok(JsonResult.jsonResultError("添加失敗,已存在")); }); } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public long getParent() { return parent; } public void setParent(long parent) { this.parent = parent; } public long getFirstRankId() { return firstRankId; } public void setFirstRankId(long firstRankId) { this.firstRankId = firstRankId; } public long getSecondRankId() { return secondRankId; } public void setSecondRankId(long secondRankId) { this.secondRankId = secondRankId; } public long getCode() { return code; } public void setCode(long code) { this.code = code; } }
dao層查詢的方法,也是很簡單的單表查詢。
public List<Area> findByParent(long parent) { return mysql.queryListSql(model, " where parent="+parent); } public List<Area> findByParentList(List<Area> list){ StringBuilder sb = new StringBuilder(); sb.append("where parent in ("); sb.append(list.get(0).getId()); for(int i=1;i<list.size();i++){ sb.append(","); sb.append(list.get(i).getId()); } sb.append(")"); return mysql.queryListSql(model, sb.toString()); }