メニューの樹状構造データベース設計と検索方法

ソフトウェア開発では、樹状構造を必要とする場面がよくあります。例えば、ナビゲーションメニュー、組織構造図、商品カテゴリ階層などがあります。これらはすべて、親子のような階層構造を持っています。

そんな樹状構造を表すデータベース設計はどのようにしたらよいのでしょうか?今回はその設計と検索方法について探ります。

まず最初に、非リレーショナルデータベースでの保存は可能ですか?

肯定的に「可能」です。例えばMongoDBでは、一本の木全体をJSON形式で保存できます。しかし、条件検索がしにくいという欠点があります。もちろん、具体的な要件に依存しますが、要件を無視した設計はナンセンスです。

メニューのシーンでは一般的にリレーショナルデータベースを使用します。最終的な検索結果をキャッシングするのが一般的です。

一般的な方法は4種類あります。

  • 各レコードにparent_idを保存する方法
  • 各レコードにツリー経路を保存する方法
  • 各レコードにnleftとnrightを保存する方法
  • ツリー構造を表す専用の表を作成する方法

第一の方法:各レコードにparent_idを保存する方法

この方法はシンプルで直感的ですが、特定の節点の親や子の検索時にリカursive処理が必要になります。MySQLではストアドプロシージャーを使用する必要がありますが、これには手間がかかります。

ただし、2階層までのメニューであれば単純にセルフジョインで済みます。

第四の方法:専用の表で節点間の関係を保存する方法

CREATE TABLE `city`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(16),
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4;

CREATE TABLE `city_tree_path_info`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `city_id` int(11) NOT NULL,
  `ancestor_id` int(11) NOT NULL COMMENT '祖先ID',
  `level` tinyint(4) NOT NULL COMMENT 'レベル',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4;

この例では、city表が都市を表し、city_tree_path_info表が都市間の階層関係を表します。ancestor_idは親や祖父などのIDを示し、levelは現在のレコードがancestor_idに対してのレベルを示します。このようにして全体の階層関係を保存することができます。

そして、このような階層構造を構築するのに最適なのはJavaコードです。

Javaでのメニュー樹の生成

Menu.java

 1 package com.example.demo.model;
 2 
 3 import lombok.AllArgsConstructor;
 4 import lombok.Data;
 5 import lombok.NoArgsConstructor;
 6 
 7 import java.util.List;
 8 
 9 @AllArgsConstructor
10 @NoArgsConstructor
11 @Data
12 public class Menu {
13 
14     /**
15      * メニューID
16      */
17     private Integer id;
18 
19     /**
20      * 親メニューID
21      */
22     private Integer pid;
23 
24     /**
25      * メニュー名
26      */
27     private String name;
28 
29     /**
30      * メニュー識別子
31      */
32     private String code;
33 
34     /**
35      * メニューリンク
36      */
37     private String url;
38 
39     /**
40      * メニューアイコン
41      */
42     private String icon;
43 
44     /**
45      * 順番
46      */
47     private int sort;
48 
49     /**
50      * 子メニュー
51      */
52     private List<Menu> children;
53 
54     public Menu(Integer id, Integer pid, String name, String code, String url, String icon, int sort) {
55         this.id = id;
56         this.pid = pid;
57         this.name = name;
58         this.code = code;
59         this.url = url;
60         this.icon = icon;
61         this.sort = sort;
62     }
63 
64 }

Test.java

 1 package com.example.demo.model;
 2 
 3 import com.fasterxml.jackson.core.JsonProcessingException;
 4 import com.fasterxml.jackson.databind.ObjectMapper;
 5 
 6 import java.util.ArrayList;
 7 import java.util.Comparator;
 8 import java.util.List;
 9 import java.util.stream.Collectors;
10 
11 public class Hello {
12     public static void main(String[] args) throws JsonProcessingException {
13         List<Menu> allMenuList = new ArrayList<>();
14         allMenuList.add(new Menu(1, 0, "湖北", "HuBei", "/a", "a", 3));
15         allMenuList.add(new Menu(2, 0, "河南", "HeNan", "/b", "b", 2));
16         allMenuList.add(new Menu(3, 1, "宜昌", "YiChang", "/c", "c", 2));
17         allMenuList.add(new Menu(4, 2, "信阳", "XinYang", "/d", "d", 1));
18         allMenuList.add(new Menu(5, 1, "随州", "SuiZhou", "/e", "e", 1));
19         allMenuList.add(new Menu(6, 5, "随县", "SuiXian", "/f", "f", 2));
20         allMenuList.add(new Menu(7, 3, "枝江", "ZhiJiang", "/g", "g", 2));
21 
22         //  1級メニュー
23         List<Menu> parentList = allMenuList.stream().filter(e->e.getPid()==0).sorted(Comparator.comparing(Menu::getSort)).collect(Collectors.toList());
24         //  递归呼び出しで、全ての1級メニューに子メニューを設定
25         for (Menu menu : parentList) {
26             menu.setChildren(getChild(menu.getId(), allMenuList));
27         }
28 
29         ObjectMapper objectMapper = new ObjectMapper();
30         System.out.println(objectMapper.writeValueAsString(parentList));
31     }
32 
33     /**
34      * 递归検索で子メニューを取得
35      * @param id    現在のメニューID
36      * @param allList   検索対象メニュー一覧
37      * @return
38      */
39     public static List<Menu> getChild(Integer id, List<Menu> allList) {
40         //  子メニュー
41         List<Menu> childList = new ArrayList<>();
42         for (Menu menu : allList) {
43             if (menu.getPid().equals(id)) {
44                 childList.add(menu);
45             }
46         }
47 
48         //  子メニューに子メニューを設定
49         for (Menu nav : childList) {
50             nav.setChildren(getChild(nav.getId(), allList));
51         }
52 
53         //  順番を整える
54         childList = childList.stream().sorted(Comparator.comparing(Menu::getSort)).collect(Collectors.toList());
55 
56         if (childList.size() == 0) {
57 //            return null;
58             return new ArrayList<>();
59         }
60         return childList;
61     }
62 }

結果:

 1 [
 2     {
 3         "id":2,
 4         "pid":0,
 5         "name":"河南",
 6         "code":"HeNan",
 7         "url":"/b",
 8         "icon":"b",
 9         "sort":2,
10         "children":[
11             {
12                 "id":4,
13                 "pid":2,
14                 "name":"信阳",
15                 "code":"XinYang",
16                 "url":"/d",
17                 "icon":"d",
18                 "sort":1,
19                 "children":[]
20             }
21         ]
22     },
23     {
24         "id":1,
25         "pid":0,
26         "name":"湖北",
27         "code":"HuBei",
28         "url":"/a",
29         "icon":"a",
30         "sort":3,
31         "children":[
32             {
33                 "id":5,
34                 "pid":1,
35                 "name":"随州",
36                 "code":"SuiZhou",
37                 "url":"/e",
38                 "icon":"e",
39                 "sort":1,
40                 "children":[
41                     {
42                         "id":6,
43                         "pid":5,
44                         "name":"随县",
45                         "code":"SuiXian",
46                         "url":"/f",
47                         "icon":"f",
48                         "sort":2,
49                         "children":[]
50                     }
51                 ]
52             },
53             {
54                 "id":3,
55                 "pid":1,
56                 "name":"宜昌",
57                 "code":"YiChang",
58                 "url":"/c",
59                 "icon":"c",
60                 "sort":2,
61                 "children":[
62                     {
63                         "id":7,
64                         "pid":3,
65                         "name":"枝江",
66                         "code":"ZhiJiang",
67                         "url":"/g",
68                         "icon":"g",
69                         "sort":2,
70                         "children":[]
71                     }
72                 ]
73             }
74         ]
75     }
76 ]

参考文献:

https://www.cnblogs.com/w2206/p/10490208.html

https://www.cnblogs.com/mokingone/p/9109021.html

https://www.cnblogs.com/makai/p/12301707.html

https://www.cnblogs.com/zhifengge/p/6910881.html

タグ: データベース設計 Java 樹状構造 メニューシステム

9月9日 00:26 投稿