樹形結構是典型的遞歸結構,常見於代碼中,但是代碼比較通用,所以記之備忘。實現方式有多種,此僅僅是一種比較好理解的方式,不適合於數據量太大的情況,如果數據量太大,請使用分批次查詢的方式。
形成一個樹形結構大部分情況下是基於一張表,然後通過一個字段parent_id指向其父級id即可。
CREATE TABLE `some_model` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(32) NOT NULL COMMENT '名字',
`level` int(11) NOT NULL DEFAULT '1' COMMENT '層級,方便前端展示',
`parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=40 DEFAULT CHARSET=utf8 COMMENT='具備層級關係';
對應的JPA 的Entity如下,其中設置一個displayName方便前端形成層級效果。
@Entity
@Table(name = "some_model")
public class SomeModel implements Serializable {
private static final String PREFIX = "┣";
private static final String SEPERATOR = " ";
public static final int LEVEL_1 = 1;
public static final int LEVEL_2 = 2;
public static final int LEVEL_3 = 3;
public static final int LEVEL_4 = 4;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
/**
* 展示方便形成層級的名字,就是前綴+name
*/
@Transient
private String displayName;
/**
* 層級,最高級爲1
*/
private int level;
/**
* 所屬上級
*/
@Column(name = "parent_id")
private int parentId;
@Transient
public List<SomeModel> children;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDisplayName() {
StringBuilder builder = new StringBuilder();
int l = level<=1 ? 0 : level;
for (int i = 0; i < l; i++) {
builder.append(SEPERATOR);
}
return builder.append(PREFIX).append(name).toString();
}
.....省略getter、setter
}
首先,設置每個model的children,可以是個森林(可以包含多個頂級節點)
public List<SomeModel> tree() {
List<SomeModel> all = someModelRepository.findAll();
if(CollectionUtil.isEmpty(all)){
return Collections.emptyList();
}
final Map<Integer, List<SomeModel>> map = childrenGroupingBy(all);
///List<SomeModel> topList = someModelRepository.findAllByLevelEquals(1);
///List<SomeModel> topList = someModelRepository.findAllByParentIdEquals(0);
///parentId=0的就是頂級的
List<SomeModel> topList = map.get(0);
if (CollectionUtil.notEmpty(topList)) {
topList.forEach(o -> setChildren(o, map));
}
return null == topList ? Collections.emptyList() : topList;
}
/**
* 所有的形成id和下級之間的映射,通過 parentId
*/
public Map<Integer, List<SomeModel>> childrenGroupingBy(List<SomeModel> all) {
return all.stream().collect(Collectors.groupingBy(SomeModel::getParentId));
}
private void setChildren(SomeModel someModel, final Map<Integer, List<SomeModel>> map) {
List<SomeModel> childList = map.get(someModel.getId());
if (CollectionUtil.notEmpty(childList)) {
someModel.setChildren(childList);
childList.forEach(o -> setChildren(o, map));
}
}
然後,將該樹扁平化,頁面循環此列表展示displayName即可
/**
* 樹形結構扁平化,頁面展示形成樹形結構{@link SomeModel#getDisplayName()}
* 先展示自己,再展示自己的下級
* @param tree 具備children的一個列表
*/
public List<SomeModel> flatten(List<SomeModel> tree){
List<SomeModel> list = new LinkedList<>();
for (SomeModel someModel : tree) {
list.add(someModel);
//孩子扁平化
List<SomeModel> children = someModel.getChildren();
if(CollectionUtil.isEmpty(children)){
continue;
}
List<SomeModel> flatten = flatten(children);
list.addAll(flatten);
someModel.setChildren(null);
}
return list;
}
效果如下:
另外,提供獲取某個的下級,子子孫孫的方法。
/**
* 獲取我所有的下級,不包括自己,兒子、孫子、無窮盡
* @param someModel 哪一個
* @param includeMySelf 是否包含自己
*/
public List<SomeModel> getMyChildren(SomeModel someModel, boolean includeMySelf) {
if(null == someModel){
return Collections.emptyList();
}
List<SomeModel> all = someModelRepository.findAll();
Map<Integer, List<SomeModel>> integerListMap = childrenGroupingBy(all);
LinkedList<SomeModel> someModels = new LinkedList<>();
if(includeMySelf){
someModels.add(someModel);
}
fillMyChildren(someModels , someModel , integerListMap);
return someModels;
}
/**
* @param result 裝結果的list,一般傳入一個初始化的list
* 獲取我所有的下級,不包括自己,兒子、孫子、無窮盡
*/
private void fillMyChildren(List<SomeModel> result , SomeModel someModel, Map<Integer, List<SomeModel>> map){
if(null == someModel){
return;
}
List<SomeModel> myChild = map.get(someModel.getId());
if(CollectionUtil.isEmpty(myChild)){
return;
}
result.addAll(myChild);
myChild.forEach(o-> fillMyChildren(result, o, map));
}