表題の通り。
想定される利用シーンは、例えば以下が考えられます。
- 結合数が多くなりすぎるため性能の懸念があるため表結合できない
- 他システムなど、データベースが別のため表結合できない
以下のようなテーブルを例に考えます。
Employee | Department | SubDepartment |
---|---|---|
id PK | id PK | employee_id FK |
name | name | department_id FK |
mail_address | ||
department_id FK | ||
department_name |
// 社員Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmployeeEntity {
private int id;
private String name;
private String mailAddress;
private int departmentId;
private String departmentName;
// 兼務組織
private List<DepartmentEntity> subDepartments;
}
// 所属組織Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DepartmentEntity {
private int id;
private String name;
}
以下のような構造体を取得したいとき、MyBatisの機能のみで1発で取得することは、調べた限りではできなさそうです。
{
"1" : {
"id" : 1,
"name" : "John",
"mailAddress" : "john@example.com",
"departmentId" : 1,
"departmentName" : "Planning",
"subDepartments" : [ ]
},
"2" : {
"id" : 2,
"name" : "Ken",
"mailAddress" : "ken@example.com",
"departmentId" : 1,
"departmentName" : "Planning",
"subDepartments" : [ {
"id" : 2,
"name" : "Legal"
}, {
"id" : 3,
"name" : "Investor Relations"
} ]
}
}
そこで、MappingHelperクラスを仲介し、上記の構造を取得できるようにします。
データベースからはMappingHelperのリストとして情報を取得し、MappingHelper#toMapを利用してMapに変換します。
キーと値のデータ型は、発行するクエリのインタフェースを定義するときに指定できるように総称型にしておきます。
package com.example.domain.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MappingHelper<K, V> {
private K key;
private V value;
public static <K, V> Map<K, V> toMap(List<MappingHelper<K, V>> list) {
if (list == null) {
return Collections.emptyMap();
}
return list.parallelStream().collect(Collectors.toMap(MappingHelper::getKey, MappingHelper::getValue));
}
}
このMappingHelperクラスを利用して、以下のようなMyBatisのMapperを作ります。
package com.example.domain.repository;
import com.example.domain.model.EmployeeEntity;
import com.example.domain.model.MappingHelper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@Mapper
public interface EmployeeRepository {
List<MappingHelper<Integer, EmployeeEntity>> findAllByIds(@Param("ids") List<Integer> ids);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.domain.repository.EmployeeRepository">
<select id="findAllByIds" resultMap="employeeMapHelper">
SELECT
e.id employeeId,
e.name employeeName,
e.mail_address mailAddress,
d.id departmentId,
d.name departmentName,
sd.id subDepartmentId,
sd.name subDepartmentName
FROM
employee e
LEFT JOIN
department d
ON
e.department_id = d.id
LEFT JOIN (
SELECT
d2.id,
d2.name,
sd2.employee_id
FROM
sub_department sd2
INNER JOIN
department d2
ON
d2.id = sd2.department_id
WHERE
sd2.employee_id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
) sd
ON
e.id = sd.employee_id
WHERE
e.id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
<resultMap id="employeeMapHelper" type="MappingHelper">
<id property="key" column="employeeId" javaType="String"/>
<association property="value" resultMap="employeeMap"/>
</resultMap>
<resultMap id="employeeMap" type="EmployeeEntity">
<id property="id" column="employeeId"/>
<result property="name" column="employeeName"/>
<result property="mailAddress" column="mailAddress"/>
<result property="departmentId" column="departmentId"/>
<result property="departmentName" column="departmentName"/>
<collection property="subDepartments" ofType="DepartmentEntity">
<id property="id" column="subDepartmentId"/>
<result property="name" column="subDepartmentName"/>
</collection>
</resultMap>
</mapper>
以上を利用して以下のように処理を実行すると、目的の構造体を取得できます。
package com.example.web;
import com.example.domain.model.EmployeeEntity;
import com.example.domain.model.MappingHelper;
import com.example.domain.repository.EmployeeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequiredArgsConstructor
public class SandboxRestController {
private final EmployeeRepository employeeRepository;
@RequestMapping("/mybatisToMap")
public ResponseEntity<Map> mybatisToMap() {
List<MappingHelper<Integer, EmployeeEntity>> mapperList = employeeRepository.findAllByIds(Arrays.asList(1, 2));
Map<Integer, EmployeeEntity> employeeMap = MappingHelper.toMap(mapperList);
return ResponseEntity.ok().body(employeeMap);
}
}
以上で、MyBatisを利用してキーを識別子、値をEntityとするMapを取得することができました。
表結合できないときにJavaでゴリゴリと実装をするときに役立つ・・・かもしれません。
サンプルコード:https://github.com/tnemotox/sandbox
参考:https://stackoverflow.com/questions/36400538/mybatis-resultmap-is-hashmapstring-object
追記:18/6/14
JavaならわざわざMyBatisでがんばらなくても、List取得してStreamでがんばればよいのでは・・・
Listが欲しい場合はあるので、そのクエリの取得結果から1発で作れるじゃないか。
Map<Integer, EmployeeEntity> employeeMap = employees.stream().collect(toMap(EmployeeEntity::getEmployeeId, e -> e, (e1, e2) -> e1);
ま、何かの役に立つ・・かもしれない・・・