検証環境
- mybatis 3.4.5
- java 1.8.0.25
ハマったこと
DBに以下のようなデータが入っているときに
SQL で FULL OUTER JOIN すると、以下のようになる。
これに対応する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>
理想は、以下のようにidがnullのオブジェクトがまとまること。
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のオブジェクトが2つに分割されてしまった。
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)
])
原因
MyBatis は、ResultSetの1行単位でオブジェクトを生成する。ResultMapがネストしている場合(1:Nにマッピングする場合)は、ResultMapごとにオブジェクトの生成を繰り返す。
参考: DefaultResultSetHandlerのソースコード
生成したオブジェクトは、CacheKeyオブジェクトをKeyにしてマップに保存する。このCachedKeyオブジェクトを識別するときのキーがresultMapのidに指定したフィールドの値。
ResultSetの1行ごとの処理時に、idが既にキャッシュにある場合は、そのオブジェクトを取り出して使う。このときは、行の実行結果は最終的なメソッドの戻り値には含まれない。(ただし、キャッシュされている値と戻り値のオブジェクトは共有されたミュータブルなオブジェクトのため、Shelfオブジェクトのbooksフィールドに対する変更が戻り値に反映される。)
ここで、idがnullの場合は NullCachedKey が利用され、キャッシュされない。
idがnullの場合は生成されたオブジェクトがキャッシュに入らないので、次の行でも、新しくShelfオブジェクトを生成する。
上記処理のreturnをまとめると最終的な戻り値のになり、以下が返ってくる。
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=[])