はじめに
例えば、DBから親子の2テーブルをjoinして抽出した結果をキー毎に処理をしたいとき。
Listのまま処理してもいいけどキーごとにグルーピングした方が見た目きれいですよね?(個人的には可読性が上がるので好きです。性能的に問題が出た場合は仕方ないですが。)
今回はいったん横並びのListで一括取得した後、Map(キー:親テーブル、値:子テーブル)に変換してみます。
前提
ER
部署マスタ
- 部署id
- 部署名
社員マスタ
- 社員id
- 社員名
- 部署id(FK)
これを内部結合で取得するとこうなりますね。 ↓
select *
from 部署マスタ
inner join 社員マスタ using 部署id ;
取得結果のイメージ(List<結果>) ↓
部署 | 社員 |
---|---|
部署1 | 社員1 |
部署1 | 社員2 |
部署2 | 社員3 |
部署2 | 社員4 |
部署2 | : |
これを部署をキー、社員を値とした『Map<部署, List<社員>>』に変換することが今回の目的です。
実装
環境
- Java8
- Spring boot
- Lombok
作ったもの
- DemoService:メイン処理
- DbMapper:実際はMybatisなどのORマッパー。
- DbMapperImpl:テスト用に実装したDbMapper。
- Result:select_all.sqlの結果を格納する
- Department:部署テーブル
- Employee:社員テーブル
Serviceクラス
一応、Java7バージョンのlist2Map7メソッドと、Java8(StreamAPI)のlist2Map8メソッド2つ作成しました。
どちらも結果は同じはず。
やっぱりStreamAPIでの実装はコード量も少なく見やすい。
ビジネスロジックと関係ない実装はあまりしたくないですね。
頑張ればlist2Map8ももっと見やすくなるかも?でもServiceクラス以外に可読性の低いコードが増えそうですね。
package com.example.demo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class DemoService {
@Autowired
DbMapper mapper;
public void execute(String[] args) {
log.info("### START ###");
// select from DB
List<Result> list = mapper.select();
// 抽出した中身
list.forEach(System.out::println);
// List ⇒ LinkedHashMap
Map<Department, List<Employee>> map = list2Map8(list);
System.out.println();
// 変換結果を出力
for (Map.Entry<Department, List<Employee>> entry : map.entrySet()) {
System.out.println("Key:" + entry.getKey());
entry.getValue().forEach(e -> System.out.println(" Value:" + e));
}
log.info("### END ###");
}
/**
* List⇒Map変換(java7Ver).
*
* @param list
* @return
*/
private Map<Department, List<Employee>> list2Map7(List<Result> list) {
if (list.isEmpty()) {
return Collections.emptyMap();
}
Map<Department, List<Employee>> map = new LinkedHashMap<>();
// 前回のキー
Department prevDep = null;
List<Employee> tempList = null;
for (Result result : list) {
Department dep = result.getDepartment();
// キーが変わったら
if (!dep.equals(prevDep)) {
// 値リストを初期化
tempList = new ArrayList<>();
// 値が初期化された状態のマップに追加(mapに参照をセット)
map.put(dep, tempList);
// 今回のキーを前回キーとする
prevDep = dep;
}
// mapを参照しているtempListに値を追加
tempList.add(result.getEmployee());
}
return map;
}
/**
* List⇒Map変換(java8Ver).
*
* @param list
* @return
*/
private Map<Department, List<Employee>> list2Map8(List<Result> list) {
// List(値)⇒Map(キー、値)に変換
Map<Department, List<Employee>> ret = list.stream()
.collect(Collectors.groupingBy(Result::getDepartment,
LinkedHashMap::new, Collectors.mapping(Result::getEmployee, Collectors.toList())));
return ret;
}
}
取得順を保持するためLinkedHashMapを使用しています。
HashMapでは保持されません。
(コメントいただいて、変換メソッド改良しました。ありがとうございます!)
DbMapperインタフェース
実際はMybatisとかORマッパーを使う感じ。
package com.example.demo;
import java.util.List;
import org.springframework.stereotype.Component;
@Component
public interface DbMapper {
List<Result> select();
}
DbMapperImplクラス
デバッグ用データを返す。
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
@Component
public class DbMapperImpl implements DbMapper {
@Override
public List<Result> select() {
List<Result> list = new ArrayList<>();
list.add(getResult(1, 101));
list.add(getResult(1, 102));
list.add(getResult(2, 203));
list.add(getResult(3, 304));
list.add(getResult(3, 305));
list.add(getResult(3, 306));
return list;
}
private Result getResult(int did, int eid) {
Department department = new Department();
department.setDepartmentId(did);
department.setDepartmentName("システム" + did + "課");
Employee employee = new Employee();
employee.setEmployeeId(eid);
employee.setName("山田 " + eid + "郎");
employee.setDepartmentId(department.getDepartmentId());
Result result = new Result(department, employee);
return result;
}
}
Resultクラス
DBからのSELECT結果がまず格納されるオブジェクト。
package com.example.demo;
import lombok.Data;
@Data
public class Result {
private Department department;
private Employee employee;
public Result() {
}
public Result(Department department, Employee employee) {
this.department = department;
this.employee = employee;
}
}
Departmentクラス
部署マスタ。
Lombokの@Dataでhashcodeとequalsをオーバーライドしています。社員マスタも同様。
状況によりますが実際にこれがテーブルの1レコードを表す場合、オーバーライドするフィールドはPKだけでいいですね。その場合は@Dataは使用せずに自前で作成。LinkedHashMap(HashMap)使うのでオーバーライドは必須。
package com.example.demo;
import lombok.Data;
@Data
public class Department {
private Integer departmentId;
private String departmentName;
}
Employeeクラス
社員マスタ。
package com.example.demo;
import lombok.Data;
@Data
public class Employee {
private Integer employeeId;
private String name;
private Integer departmentId;
}
実行結果
ちゃんとキー(部署)ごとのマップに変換されていますね。
Result(department=Department(departmentId=1, departmentName=システム1課), employee=Employee(employeeId=101, name=山田 101郎, departmentId=1))
Result(department=Department(departmentId=1, departmentName=システム1課), employee=Employee(employeeId=102, name=山田 102郎, departmentId=1))
Result(department=Department(departmentId=2, departmentName=システム2課), employee=Employee(employeeId=203, name=山田 203郎, departmentId=2))
Result(department=Department(departmentId=3, departmentName=システム3課), employee=Employee(employeeId=304, name=山田 304郎, departmentId=3))
Result(department=Department(departmentId=3, departmentName=システム3課), employee=Employee(employeeId=305, name=山田 305郎, departmentId=3))
Result(department=Department(departmentId=3, departmentName=システム3課), employee=Employee(employeeId=306, name=山田 306郎, departmentId=3))
Key:Department(departmentId=1, departmentName=システム1課)
Value:Employee(employeeId=101, name=山田 101郎, departmentId=1)
Value:Employee(employeeId=102, name=山田 102郎, departmentId=1)
Key:Department(departmentId=2, departmentName=システム2課)
Value:Employee(employeeId=203, name=山田 203郎, departmentId=2)
Key:Department(departmentId=3, departmentName=システム3課)
Value:Employee(employeeId=304, name=山田 304郎, departmentId=3)
Value:Employee(employeeId=305, name=山田 305郎, departmentId=3)
Value:Employee(employeeId=306, name=山田 306郎, departmentId=3)
あとがき
Lombok便利ですよね。