Mybatisのキャッシュ利用について
Mybatisはデフォルトで、キャッシュの設定は有効になっています。同じ条件でselectを実行する際、キャッシュ参照となり、同じインスタンを返すことになります。実はここに大きな落とし穴があり、注意しないと、不思議なバグになる可能性があります。
Mybatisのサイトに、キャッシュについて、以下のような説明があります。
MyBatis は循環参照の解決やネストされたクエリのスピード向上のためにローカルキャッシュを使用します。 デフォルト(SESSION)では同一セッション内の全てのクエリ結果がキャッシュされます。localCacheScope に STATEMENT を設定した場合、ローカルキャッシュはステートメントごとに適用されます。言い換えると、同一 SqlSession に対する複数の呼び出しでデータが共有されることはありません。
ちなみに、mybatisのキャッシュは以下のような特徴があります。
・Mapper.xmlファイル内のすべてのselectステートメントの結果がキャッシュされます。
・Mapper.xmlファイル内のすべてのinsert、update、およびdeleteステートメントを実行したら、キャッシュをフラッシュします。
・キャッシュは定期的に更新されません (つまり、更新間隔はありません)。
不思議なバグ
普通にMybatisの同じ条件のselect文で取得したアイテムをArrayList に詰め込んでいるだけですが、なぜなら、追加するたびに、前にすでに追加された要素をすべて一番最後に追加された要素に上書きされます。その理由は、Mybatisは同じ条件でselectする際、キャッシュを利用して、同じインスタンが帰ってきたためでした。つまり、itemとitem2は同一のインスタンスを参照しています。
ArrayListに追加された要素はすべて参照のため、item2でnameを書き換える際に、実に、itemの参照も書き換えられたことになります。
//ArrayListを用意する
List<ABC> testList = new ArrayList<>();
//ArrayListに一つ目の要素を追加
ABC item = new ABC();
// MybatisのMapper実行し、DBからselectで取得する値を指定のエンティティに入れる
Map<String, Object> condition = new HashMap<>();
condition.put("condition1", "CONDITION1");
condition.put("condition2", "CONDITION2");
item = testMybatisMapper.selectItem(condition);
item.name = 'AAA';
testList.add(item);
//ArrayListに二つ目の要素を追加
ABC item2 = new ABC();
//1つ目の要素作成と同じselect条件でデータを取得する。
condition = new HashMap<>();
condition.put("condition1", "CONDITION1");
condition.put("condition2", "CONDITION2");
//★★★newしたitem2なのに、itemと同じインスタンス参照となった!★★★
item2 = testMybatisMapper.selectItem(condition);
item.name = 'BBB';
//ArrayListに追加された1つ目のitemも書き換えられ、
//二つ同じインスタンをListに詰め込んだ
testList.add(item2); ←Listにある二つ要素のnameは全部[BBB]となった!
解決策
Mybatisは特定のSQLを実行する際にキャッシュを利用しないように設定することができます。上記のselect文の場合は、対応するselect文のxmlに、以下ののように指定すれば、キャッシュの使用が禁止されます。
<select ... flushCache="false" useCache="true"/>
もう少し深掘り
個別のSQLにキャッシュを利用禁止する以外、グローバル範囲またはファイル範囲にキャッシュを利用禁止することもできます。
mybatis-config.xml に以下を追加すると、グローバルにキャッシュをオフになります。
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
コメント