SIerだけど技術やりたいブログ

6年目のSIerのブログです

MyBatis 利用時に SQL で FULL OUTER JOIN するときの注意点

バージョン

  • mybatis 3.4.5
  • java8

注意点

DBに以下のようなデータが入っているときに、
f:id:kimulla:20180310145717j:plain

SQL で FULL OUTER JOIN すると、以下のようになる。
f:id:kimulla:20180310145728j:plain

これに対応するJavaのBeanを用意して、

@Data
public class Shelf {
    private Long id;
    private String name;
    private String position;
    private List<Book> books;
}
@Data
public class Book {
    private Long id;
    private String name;
}

以下のようなマッパー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.mybatissample.ShelfRepository">

  <resultMap id="shelfResult" type="com.example.mybatissample.Shelf">
    <id property="id" column="shelf_id"></id>
    <result property="name" column="shelf_name"></result>
    <result property="position" column="position"></result>
    <collection property="books" ofType="com.example.mybatissample.Book">
      <id property="id" column="book_id"></id>
      <result property="name" column="book_name"></result>
    </collection>
  </resultMap>

  <select id="findAll" resultMap="shelfResult">
      SELECT
        shelf_id, shelf_name, position, book_id, book_name
      FROM shelf
      FULL OUTER JOIN book USING (shelf_id)
      ORDER BY shelf_id
  </select>

</mapper>

これを実行すると、以下のようになってほしかった。

Shelf(id=1, name=本棚A, position=1F, books=[
  Book(id=1, name=ネコでもわかるJava), 
  Book(id=2, name=イヌでもわかるJava)
])
Shelf(id=2, name=本棚B, position=2F, books=[])
Shelf(id=null, name=null, position=null, books=[
  Book(id=3, name=サルでもわかるJava), 
  Book(id=4, name=キジでもわかるJava)
])

が、実際には、ShelfのidがnullのときにBeanが分割される。

Shelf(id=1, name=本棚A, position=1F, books=[
  Book(id=1, name=ネコでもわかるJava), 
  Book(id=2, name=イヌでもわかるJava)
])
Shelf(id=2, name=本棚B, position=2F, books=[])
Shelf(id=null, name=null, position=null, books=[
  Book(id=3, name=サルでもわかるJava)]
)
// ここが別のBeanにマッピングされる
Shelf(id=null, name=null, position=null, books=[
  Book(id=4, name=キジでもわかるJava)
])

原因

https://github.com/mybatis/mybatis-3/blob/master/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java#L865-L893


MyBatis は、ResultSetの1行単位でオブジェクトを生成する様子。ResultMapがネストしている場合(1:Nにマッピングする場合)は、ResultMapごとにオブジェクトの生成を繰り返す。

生成したオブジェクトは、CacheKeyオブジェクトをKeyにしてマップに保存する。このCachedKeyオブジェクトを識別するときのキーがresultMapのidに指定したフィールドの値。

各オブジェクトの関連が解決されたものが、最終的な戻り値の要素になる。

f:id:kimulla:20180310171026j:plain

idが既にキャッシュにある場合は、そのオブジェクトを取り出して使う。キャッシュから取り出したShelfオブジェクトと生成したBookオブジェクトをもとに、関連を解決する。

この行の実行結果は最終的なメソッドの戻り値には含まれない。ただし、ミュータブルなオブジェクトなのでShelfオブジェクトのbooksフィールドに対する変更は戻り値に反映される。

f:id:kimulla:20180310171035j:plain

idがnullの場合は NullCachedKey が利用され、キャッシュされない。

f:id:kimulla:20180310171057j:plain

キャッシュに入ってないので、新しくShelfオブジェクトを生成する。

f:id:kimulla:20180310171105j:plain

そうして、最終的に以下が返ってくる。

Shelf(id=1, name=本棚A, position=1F, books=[
  Book(id=1, name=ネコでもわかるJava), 
  Book(id=2, name=イヌでもわかるJava)
])
Shelf(id=2, name=本棚B, position=2F, books=[])
Shelf(id=null, name=null, position=null, books=[
  Book(id=3, name=サルでもわかるJava)]
)
Shelf(id=null, name=null, position=null, books=[
  Book(id=4, name=キジでもわかるJava)
])

解決策

設定でidがnullの場合にキャッシュを有効化することはできなさそう。

以下のようにidがnullだった場合は代わりの値に置き換えれば、近いことはできる。

    <select id="findAll" resultMap="shelfResult">
        SELECT
          CASE
             WHEN shelf_id IS NULL THEN -1
             ELSE shelf_id
          END,
          shelf_name, position, book_id, book_name
        FROM shelf
        FULL OUTER JOIN book USING (shelf_id)
        ORDER BY shelf_id
    </select>
Shelf(id=-1, name=null, position=1F, books=[
  Book(id=3, name=サルでもわかるJava), 
  Book(id=4, name=キジでもわかるJava)
])
Shelf(id=1, name=本棚A, position=2F, books=[
  Book(id=1, name=ネコでもわかるJava), 
  Book(id=2, name=イヌでもわかるJava)]
)
Shelf(id=2, name=本棚B, position=null, books=[])