如何将数据库文件结构分组为层级化JSON数组/对象?
从扁平文件记录构建层级化JSON结构
嘿,这个需求我太熟悉了!之前做文件管理系统的时候刚好遇到过一模一样的场景——从数据库拿的扁平数据要转成树形结构,还不能多次查库影响性能。结合你给出的排序规则和数据,我给你捋个清晰的实现思路和代码示例:
先明确现状和目标
你现在用这段Java代码生成了扁平的JSON数组:
// in this example, list is the returned data JSONArray jList1 = new JSONArray(); for (int a = 0; a < list.size(); a++){ Object[] fileObj = (Object[])list.get(a); String folder = (String)fileObj[0]; String filename = (String)fileObj[1]; JSONObject jObj1 = new JSONObject(); jObj1.put("path",folder); jObj1.put("filename",filename); jList1.add(jObj1); } response.getWriter().write(jList1.toJSONString());
输出的扁平结构是这样的:
[ { "path": "/a", "filename": "" }, { "path": "/a", "filename": "file1.png" }, { "path": "/a/b", "filename": "" }, { "path": "/a/b", "filename": "file2.png" }, { "path": "/a/b", "filename": "file3.png" }, { "path": "/a/c", "filename": "" }, { "path": "/a/c", "filename": "file4.jpg" }, { "path": "/", "filename": "file5.jpg" }, { "path": "/", "filename": "file6.jpg" } ]
而你想要的是层级化的树形结构:
[ { "path": "/a", "filename": "", "files": [ { "path": "/a/b", "filename": "", "files": [ { "path": "/a/b", "filename": "file2.png" }, { "path": "/a/b", "filename": "file3.png" } ] }, { "path": "/a/c", "filename": "", "files": [ { "path": "/a/c", "filename": "file4.jpg" } ] }, { "path": "/a", "filename": "file1.png" } ] }, { "path": "/", "filename": "file5.jpg" }, { "path": "/", "filename": "file6.jpg" } ]
数据库的排序规则已经帮了大忙:
order by (CASE WHEN path='/' THEN 1 ELSE 0 END), path, filename
这个排序保证了同级目录下文件夹(filename为空)在前、文件在后,而且所有非根路径的条目都排在根路径前面,这为我们顺序构建树形结构提供了基础。
核心实现思路
我们要做的就是一次遍历扁平数组,用一个映射表快速定位父文件夹节点,逐步把文件/子文件夹挂载到对应的父节点下,具体步骤:
- 用HashMap做文件夹映射:创建一个
Map<String, JSONObject>,键是文件夹的path,值是对应的JSON对象(包含files数组)。这样我们可以O(1)时间找到任意文件夹的节点,不用递归查找,性能拉满。 - 顺序处理每个条目:
- 如果是文件夹(
filename为空):- 创建带
files数组的JSON对象,存入映射表。 - 找出它的父路径(比如
/a/b的父路径是/a,根路径/没有父节点)。如果父文件夹在映射表里存在,就把当前文件夹加到父节点的files里;如果不存在(比如根目录的子文件夹,而根目录的文件夹还没被处理),就先把它作为根节点加入最终结果数组。
- 创建带
- 如果是文件(
filename不为空):- 创建对应的JSON对象。
- 直接从映射表里找它所在的文件夹(就是它的
path字段),找到的话就把文件加到该文件夹的files数组里;找不到的话(比如根目录下的文件,根目录没有文件夹条目),就直接加到最终结果数组里。
- 如果是文件夹(
- 收尾处理:因为排序规则的原因,根目录的文件夹条目会最后处理,这时候如果之前有子文件夹被当成根节点,我们可以把它们移到根文件夹的
files里(不过看你的示例,根目录没有文件夹条目,所以这一步可能不需要)。
具体代码实现(基于org.json库)
// 假设list是数据库查询返回的结果集合 JSONArray hierarchicalTree = new JSONArray(); // 文件夹映射表:key=文件夹path,value=对应的JSON节点(带files数组) Map<String, JSONObject> folderNodeMap = new HashMap<>(); for (int i = 0; i < list.size(); i++) { Object[] fileRecord = (Object[]) list.get(i); String path = (String) fileRecord[0]; String filename = (String) fileRecord[1]; JSONObject currentNode = new JSONObject(); currentNode.put("path", path); currentNode.put("filename", filename); if (filename.isEmpty()) { // 处理文件夹节点 currentNode.put("files", new JSONArray()); folderNodeMap.put(path, currentNode); String parentPath = getParentFolderPath(path); if (parentPath != null) { JSONObject parentFolder = folderNodeMap.get(parentPath); if (parentFolder != null) { // 父文件夹存在,挂载到父节点下 parentFolder.getJSONArray("files").put(currentNode); } else { // 父文件夹还未处理(比如根目录的子文件夹),先作为根节点加入结果 hierarchicalTree.put(currentNode); } } else { // 根目录文件夹,直接加入结果 hierarchicalTree.put(currentNode); } } else { // 处理文件节点 JSONObject parentFolder = folderNodeMap.get(path); if (parentFolder != null) { // 找到所属文件夹,挂载进去 parentFolder.getJSONArray("files").put(currentNode); } else { // 没有对应的文件夹节点(比如根目录下的文件),直接加入结果 hierarchicalTree.put(currentNode); } } } // 额外处理:如果根目录文件夹存在,把之前误加入根结果的子文件夹移进去 JSONObject rootFolder = folderNodeMap.get("/"); if (rootFolder != null) { // 遍历根结果数组,把属于根目录的子文件夹移到rootFolder的files里 JSONArray tempRoot = new JSONArray(); for (int i = 0; i < hierarchicalTree.length(); i++) { JSONObject node = hierarchicalTree.getJSONObject(i); if (node.getString("filename").isEmpty() && getParentFolderPath(node.getString("path")).equals("/")) { rootFolder.getJSONArray("files").put(node); } else { tempRoot.put(node); } } // 把根目录文件夹加入临时数组,替换原结果 tempRoot.put(rootFolder); hierarchicalTree = tempRoot; } response.getWriter().write(hierarchicalTree.toJSONString());
辅助方法getParentFolderPath:
private String getParentFolderPath(String currentPath) { if ("/".equals(currentPath)) { return null; // 根目录没有父文件夹 } int lastSlashPos = currentPath.lastIndexOf('/'); return lastSlashPos == 0 ? "/" : currentPath.substring(0, lastSlashPos); }
为什么这个方案可行?
- 利用现有排序:数据库的排序保证了我们处理文件夹的时候,除了根目录的文件夹,其他父文件夹都已经被处理并存入映射表了,避免了找不到父节点的问题。
- 一次遍历搞定:全程只遍历一次查询结果,没有额外的数据库查询,性能最优。
- 兼容各种场景:不管树形结构有多少层级,也不管根目录有没有文件夹条目,都能正确生成你想要的层级结构。
内容的提问来源于stack exchange,提问作者fanfavorite
相关产品推荐
相关产品推荐

