Java
MyBatis

MyBatisを利用してキーを識別子、値をEntityとするMapを取得する

表題の通り。

想定される利用シーンは、例えば以下が考えられます。

  • 結合数が多くなりすぎるため性能の懸念があるため表結合できない
  • 他システムなど、データベースが別のため表結合できない

以下のようなテーブルを例に考えます。

Employee Department SubDepartment
id PK id PK employee_id FK
name name department_id FK
mail_address
department_id FK
department_name
Entity(packageやimportは省略)
// 社員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に変換します。
キーと値のデータ型は、発行するクエリのインタフェースを定義するときに指定できるように総称型にしておきます。

MappingHelper.java
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を作ります。

EmployeeRepository.java
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);
}
EmployeeRepository.xml
<?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>

以上を利用して以下のように処理を実行すると、目的の構造体を取得できます。

SandboxRestController.java
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発で作れるじゃないか。

streamでlistをmapに
Map<Integer, EmployeeEntity> employeeMap = employees.stream().collect(toMap(EmployeeEntity::getEmployeeId, e -> e, (e1, e2) -> e1);

ま、何かの役に立つ・・かもしれない・・・