概述
在做背景網站(平台/系統)業務開發時,經常遇到層級概念。比如我最近在全權負責(開發+測試+産品)的一款資料産品就有分類的層級概念,層級有3層;另外産品有資料集、圖表、看闆、組合看闆、資料推送等功能點(概念),這些功能點名稱都有層級的概念。
舉個例子:建立一個一級分類(最頂級)資料集。背景知識:資料集其實就是多段SQL,SQL裡面可以有删表後建表的語句(drop then create table),那我可以在這個SQL裡面建立一個最基礎的表(table),隻不過SQL的最後一個子句必須得是查詢字句(資料集概念展現點)。然後我可以再建立一個二級分類的資料集,然後這個資料集的SQL可以使用一級分類資料集SQL裡面的表(table),查詢這個table,用來做圖表。三級分類類推。
上圖中,以
/
形式拼接傳回多級分類名稱,并給出層級的實作,參考附錄。
分類表設計:
create table category(
category_id bigint auto_increment primary key,
category_name varchar(100) not null,
type int(1) not null comment '1:資料集 2:圖表 3:看闆 4:組合看闆',
isactive tinyint(1) default 1 not null comment '邏輯删除',
parent_id bigint null comment '父級id'
);
資料準備:
INSERT INTO category (category_id, category_name, type, isactive, parent_id) VALUES (869, '圖表分類A', 2, 1, 898);
INSERT INTO category (category_id, category_name, type, isactive, parent_id) VALUES (882, '圖表分類B', 2, 1, 869);
INSERT INTO category (category_id, category_name, type, isactive, parent_id) VALUES (888, '圖表分類1', 2, 1, 898);
INSERT INTO category (category_id, category_name, type, isactive, parent_id) VALUES (898, '圖表分類', 2, 1, null);
圖表的表設計:
create table widget (
widget_id bigint auto_increment primary key,
widget_name varchar(100) not null comment '圖表名稱',
category_id bigint not null comment '分類id',
isactive tinyint(1) default 1 not null comment '邏輯删除字段'
);
問題
如何選擇一級分類時,查詢下面的二級以及三級分類呢?具體來說,查詢條件裡面指定圖表的一級分類ID,如何查詢其下的二級和三級分類的圖表?即所謂的MySQL級聯(父子)查詢。
實作
在構思實作方案前先Google一下,發現級聯查詢有兩種情況:自下向上和自上向下。
自下向上
即:給定子級查詢父級。每個子級肯定隻有一個父級,實作起來相對容易些。這裡直接給出實作SQL:
SELECT category_id
FROM (
SELECT @r AS _id,
(
SELECT @r := parent_id
FROM category
WHERE category_id = _id
) AS parent_id,
@l := @l + 1 AS lvl
FROM (SELECT @r := 893, @l := 0) vars,
category h
WHERE @r <> 0
) T1
JOIN category T2 ON T1._id = T2.category_id;
自上向下
即:給定父級查詢全部子級。由于父級含有若幹個子級,每個子級又有若幹個子級,即形成一顆樹的概念。
MySQL實作
通過SQL不好實作。可以通過定義函數的方式,不建議使用函數。
Java實作
實體類定義:
@Data
public class DashboardCategory {
private Long categoryId;
private String categoryName;
private Integer type;
private Boolean isactive;
private Long parentId;
/**
* 非DB字段,表示第幾級
*/
private Integer level;
}
CategoryServiceImpl.java
實作類,由于最多隻有3級,故而可以兩層for循環嵌套實作,參考下面的附錄,業務代碼在儲存分類時有個數限制。是以for循環嵌套情況下,性能絕對不是問題:
/**
* 根據分類ID查詢子級分類ID
*
* @param categoryId 分類ID
* @return 清單形式
*/
public List<Long> getChildIds(Long categoryId) {
List<DashboardCategory> categoryList = categoryMapper.getCategoryListByParentId(categoryId);
if (CollectionUtils.isEmpty(categoryList)) {
return Lists.newArrayList(categoryId);
}
List<Long> result = Lists.newArrayList(categoryId);
for (DashboardCategory it : categoryList) {
result.add(it.getCategoryId());
List<DashboardCategory> sonCategoryList = categoryMapper.getCategoryListByParentId(it.getCategoryId());
for (DashboardCategory item : sonCategoryList) {
result.add(item.getCategoryId());
}
}
return result;
}
CategoryMapper.java
接口定義:
List<DashboardCategory> getCategoryListByParentId(Long parentId);
CategoryMapper.xml
mapper定義:
<select id="getCategoryListByParentId" resultType="com.xy.cloudiview.common.model.DashboardCategory">
SELECT category_id categoryId, parent_id parentId FROM category
WHERE isactive = 1 AND parent_id = #{parentId}
</select>
附錄
儲存分類
@Value("${category.level.one:15}")
private Integer levelOne;
@Value("${category.level.two:10}")
private Integer levelTwo;
@Value("${category.level.three:10}")
private Integer levelThree;
public String saveCategory(JSONObject jsonObject) {
try {
DashboardCategory dashboardCategory = new DashboardCategory();
Long levelOneId = jsonObject.getLong("levelOneId");
Long levelTwoId = jsonObject.getLong("levelTwoId");
Integer type = jsonObject.getInteger("type");
if (levelOneId == null && levelTwoId != null) {
return JSONObject.toJSONString(ServiceUtil.returnError("非法情況:一級為空,二級不為空"));
}
// 一級分類ID為空,則建立一級分類,先判斷一級分類個數
if (null == levelOneId) {
int categoryCount = categoryMapper.selectFirstLevelCategoryCount(type);
if (categoryCount >= levelOne) {
return JSONObject.toJSONString(ServiceUtil.returnError(String.format("一級分類不得超過%d個!", levelOne)));
}
// 分類名重複校驗
List<DashboardCategory> list = categoryMapper.getCategoryListByCondition(jsonObject);
if (CollectionUtils.isNotEmpty(list)) {
return JSONObject.toJSONString(ServiceUtil.returnError("一級分類名不能重複"));
}
// 注意加上else
} else if (null == levelTwoId) {
// 一級分類ID不為空,二級分類ID為空,則建立二級分類,先判斷所選一級分類下已有二級分類個數
int categoryCount = categoryMapper.selectCategoryCountByParentId(levelOneId, type);
if (categoryCount >= levelTwo) {
return JSONObject.toJSONString(ServiceUtil.returnError(String.format("二級分類不得超過%d個!", levelTwo)));
}
List<DashboardCategory> list = categoryMapper.getCategoryListByCondition(jsonObject);
if (CollectionUtils.isNotEmpty(list)) {
return JSONObject.toJSONString(ServiceUtil.returnError("二級分類名不能重複"));
}
dashboardCategory.setParentId(levelOneId);
}
// 一級二級分類ID都不為空,則建立一個三級分類,父類ID,為二級分類ID
if (null != levelOneId && null != levelTwoId) {
int categoryCount = categoryMapper.selectCategoryCountByParentId(levelTwoId, type);
if (categoryCount >= levelThree) {
return JSONObject.toJSONString(ServiceUtil.returnError(String.format("三級分類不得超過%d個!", levelThree)));
}
List<DashboardCategory> list = categoryMapper.getCategoryListByCondition(jsonObject);
if (CollectionUtils.isNotEmpty(list)) {
return JSONObject.toJSONString(ServiceUtil.returnError("三級分類名不能重複"));
}
dashboardCategory.setParentId(levelTwoId);
}
dashboardCategory.setUserId(jsonObject.getString("userId"));
dashboardCategory.setCategoryName(jsonObject.getString("categoryName"));
dashboardCategory.setUpdateUserName(jsonObject.getString("updateUserName"));
dashboardCategory.setType(type);
int num = categoryMapper.insertSelective(dashboardCategory);
if (num > 0) {
return JSONObject.toJSONString(ServiceUtil.returnSuccess());
} else {
return JSONObject.toJSONString(ServiceUtil.returnError("添加分類失敗!"));
}
} catch (Exception e) {
logger.error("saveCategory error:{}", e.toString());
return JSONObject.toJSONString(ServiceUtil.returnError(e.getMessage()));
}
}
查詢分類
categoryMapper.getCategoryById(id);
,根據主鍵,即category_id查詢,省略代碼。
public String getCategoryList(JSONObject jsonObject) {
try {
// 分頁
PageHelper.startPage(jsonObject.getInteger("pageNo"), jsonObject.getInteger("pageSize"));
// 代碼省略
List<DashboardCategory> list = categoryMapper.getCategoryList(jsonObject);
list.forEach(x -> {
x.setCategoryName(this.getParentCategoryById(x.getCategoryId()).getT2());
if (x.getParentId() == null) {
x.setLevel(1);
} else if (this.isLevelTwo(x)) {
x.setLevel(2);
} else {
x.setLevel(3);
}
});
PageInfo<DashboardCategory> pageInfo = new PageInfo<>(list);
return JSONObject.toJSONString(ServiceUtil.returnSuccessData(pageInfo));
} catch (Exception e) {
logger.error("getCategoryList error:{}", e.toString());
return JSONObject.toJSONString(ServiceUtil.returnError(e.getMessage()));
}
}
/**
* tuple.t1 為分類ID
* tuple.t2 為分類名稱
*/
public Tuple<String, String> getParentCategoryById(Long categoryId) {
DashboardCategory result = categoryMapper.getCategoryById(categoryId);
Long parentId = result.getParentId();
Tuple<String, String> tuple = new Tuple<>();
// 當父級ID為空時,此時為一級分類
if (null == parentId) {
tuple.setT1(Collections.singletonList(categoryId).toString());
tuple.setT2(result.getCategoryName());
return tuple;
} else {
DashboardCategory parentResult = categoryMapper.getCategoryById(parentId);
// 表明parentResult是一級分類,result為二級分類
if (null == parentResult.getParentId()) {
tuple.setT1(Arrays.asList(parentResult.getCategoryId(), categoryId).toString());
tuple.setT2(parentResult.getCategoryName().concat("/").concat(result.getCategoryName()));
return tuple;
} else {
// 用parentResult的parentId當做categoryId去查詢資訊,lastResult為一級,parentResult為二級,result為三級
DashboardCategory lastResult = categoryMapper.getCategoryById(parentResult.getParentId());
tuple.setT1(Arrays.asList(lastResult.getCategoryId(), parentResult.getCategoryId(), categoryId).toString());
tuple.setT2(lastResult.getCategoryName().concat("/").concat(parentResult.getCategoryName()).concat("/").concat(result.getCategoryName()));
return tuple;
}
}
}
/**
* 判斷是否是二級
*/
private boolean isLevelTwo(DashboardCategory item) {
if (item.getParentId() == null) {
return false;
}
DashboardCategory po = categoryMapper.getCategoryById(item.getParentId());
if (po == null) {
return false;
} else {
return po.getParentId() == null;
}
}
@Data
public class Tuple<T1, T2> {
private T1 t1;
private T2 t2;
}