合併多對多關係並輸出成JSON
例如要列出每個使用者有哪些群組身分
情境
-
例如有"使用者"與"群組",他們是多對多關係,存在一張DB的中間表
- 一個使用者可以有多個群組身分,一個群組中也能存在多個使用者
-
今天想查詢以"使用者"出發,要列出每個使用者的群組身分有哪些,返回JSON像這樣
[
{"userId":1,"groups":[1,2,3]},
{"userId":2,"groups":[2,3]},
{"userId":3,"groups":[3]}
]
模擬資料
// 模擬資料
List<User> list = new ArrayList<>();
list.add(new User(1, 1));
list.add(new User(1, 2));
list.add(new User(1, 3));
list.add(new User(2, 2));
list.add(new User(3, 3));
list.add(new User(2, 3));
方法一
- 建一個
UserVo
public class UserVo {
Integer userId;
List<Integer> groups;
}
- 用
stream().filter
操作
List<UserVo> result = new ArrayList<>();
for (User user : list) {
UserVo userVo = null;
// 如果Id已經存在
Optional<UserVo> matched = result.stream().filter(e -> user.getUserId().equals(e.getUserId())).findFirst();
if (matched.isPresent()) {
userVo = matched.get();
List<Integer> groups = new ArrayList<>(userVo.getGroups());
groups.add(user.getGroup());
userVo.setGroups(groups);
} else {
userVo = UserVo.builder()
.userId(user.getUserId())
.groups(Arrays.asList(user.getGroup())).build();
result.add(userVo);
}
}
System.out.println("result = " + result);
方法二
- 用Map
Map<Integer, Set<Integer>> idGroupsMap = new HashMap<>();
for (User user : list) {
if (idGroupsMap.containsKey(user.getUserId())) {
// 如果Id已經存在
idGroupsMap.get(user.getUserId()).add(user.getGroup());
} else {
idGroupsMap.put(user.getUserId(), new HashSet<>(Arrays.asList(user.getGroup())));
}
}
System.out.println(idGroupsMap); // 純數值
// 不借助Vo生成有序的JSON
JSONArray ja = new JSONArray();
idGroupsMap.forEach((k, v) -> {
JSONObject jo = new JSONObject(new LinkedHashMap<String, Object>());
jo.putOpt("userId", k);
jo.putOpt("groups", v);
ja.add(jo);
});
System.out.println(ja);
System.out.println("JSON=" + JSONUtil.toJsonPrettyStr(ja));
踩坑記錄
- 我在邊吃過一個虧,就是單純遍歷
idGroupsMap
,然後判斷k是否存在,若有就把v拿出來,add資料,然後再用同k放回去,問題就出在最後這個put - 一直報錯
null
,我就覺得很奇怪,開debug
看每個值都有阿,怎麼到put那步就報null - 後來才想明白,我取出的v實際上是取得引用,我add是沒問題,但是在put的時候,由於key已經存在,所以這個put是執行覆蓋,他底層做的事就是把本來的v直接變成null,然後放入新的v
- 所以我根本是自找麻煩,如果像這種只是附加到尾部的直接add就好
- 如果v需要多步驟大改可以取出v然後做一個clone,運算完,將clone作為新的v放進去才行
- 相較之下就很麻煩,不如用
computeIfPresent()
idGroupsMap.computeIfPresent(user.getUserId(), (k, v) -> {
v.add(user.getGroup());
return v;
});
方法三
- 流式操作,用
Collectors.mapping
- 最優雅,且如果資料量龐大時可以用parallelStream來並行處理(但是小心順序與線程安全問題)
Map<Integer, Set<Integer>> userMap =
list.stream().collect(
Collectors.groupingBy(
User::getUserId,
Collectors.mapping(
User::getGroup,
Collectors.toSet()
)
)
);
List<Map<String, Object>> result =
userMap.entrySet().stream()
.map(entry -> {
Map<String, Object> map = new HashMap<>(2);
map.put("userId", entry.getKey());
map.put("groups", entry.getValue());
return map;
})
.collect(Collectors.toList());
小結
- 方法二、三更好一點,用Map、Set本身可以防止重複的key,並且不需要額外造一個
Vo
更省資源 - 另外我發現
Hutool
的JSONArray
就很奇怪,如果純用LinkedHashMap
作為JsonNode
,那我put的順序最後輸出時又會亂掉,但是用JSONObject(new LinkedHashMap<String, Object>());
他就能維持住put的順序 - 後來覺得
jackson-databind
可能還是好用一點,直接用LinkedHashMap
構成他就會照順序
List<Object> ja = new ArrayList<>();
idGroupsMap.forEach((k, v) -> {
HashMap<String, Object> jo = new LinkedHashMap<>();
jo.put("userId", k);
jo.put("groups", v);
ja.add(jo);
});
System.out.println("JSON = " + new ObjectMapper().writeValueAsString(ja));
- 如果有POJO也可以直接註釋順序
@JsonPropertyOrder({"name", "phoneNumber","email", "salary", "id" })
public class Employee {
...
- 但是要注意用
jackson
還原泛型時使用TypeReference
不要引錯包,我曾經在這邊找問題找超久…
import com.fasterxml.jackson.core.type.TypeReference;
上次修改於 2022-04-10