MyBatisとは
MyBatisは、JavaからRDBへアクセスしてデータの授受を行うフレームワークの1つです。
動作条件によって動的に変わるSQL、ストアドプロシージャの実行、そして高度なマッピングに対応しています。
SQLの定義や作成は、XMLやJavaのアノテーションで定義します。
SpringBoot(SpringMVC)と組みあわせて使う
SpringBootと組み合わせて使う場合は、SpringBoot用のstarterライブラリがあります。こちらを導入するのが簡単でオススメです。
MyBatisにはデフォルトで実行結果をキャッシュする機構が用意されている
MyBatisにはクエリの検索結果をキャッシュする機構が用意されており、デフォルトでは有効です。これにより同一トランザクション内で同じクエリを実行すると、SQLを実行することなくキャッシュした結果を返します。
これにより同じクエリを何度か実行する実装の場合にはデータベースへの通信をせずに実行するのでパフォーマンスは良くなることが期待できます。
しかし1つのトランザクション内で、同じクエリを投げるときに起こること
繰り返しになりますが、1つのトランザクション内で同じ条件の同じクエリを実行すると、MyBatisはデフォルトで実行結果をキャッシュに保持した内容を返します。同じ結果を返すとは、実際にはデータベースに問い合わせすることなく、MyBatisが同じ結果を返します。
この 同じ結果を返す に気を付けるべきところがあります。
クエリの結果を受け取ったあとに編集するケースは良くあるけれど
具体的な例をあげます。
商品の検索結果を格納する以下のクラスがあるとします。このクラスは検索した結果を加工して表示用にするため、ミューテータメソッド1 も用意します。今回は Lombok の @Dataで記述は省略します。
@Data
public class ItemResult {
private ProductId productId;
private ProductName productName;
}
この検索結果を返すMyBatis用の実装インタフェースを ItemMapperとします。
public interface ItemMapper {
ItemResult getItemResult(ProductId productId);
}
実際にこの検索処理を実行した結果を受け取るRepositoryクラスは、例えば以下のようになるでしょう。
@AllArgsConstructor
@Repository
public class ItemRepository {
ItemMapper itemMapper;
public ItemResult getItemResult(ProductId productId) {
return itemMapper.getItemResult(productId);
}
}
今回の例ではServiceクラスは経由せず、Controllerから直接Repositoryを呼びます。
Controllerで表示用に2回検索し、片方の検索結果を表示用に加工します。
@AllArgsConstructor
@Controller
public class ItemSearchController {
ItemRepository itemRepository;
@GetMapping
public ModelAndView display(ModelAndView mnv, ItemForm form) {
ProductId productId = form.productId(); // formクラスから検索条件を取り出す実装をしておきます。
ItemResult searchResult = itemRepository.getItemResult(productId);
mnv.addObject("searchResult", searchResult);
ItemResult editResult = itemRepository.getItemResult(productId);
editResult.setProductName(ProductName.of("変更後")); // メソッドの引数で名前を変更する
mnv.addObject("editResult", editResult);
return mnv;
}
}
この結果はどうなるでしょうか?
実は、searchResult と editResult の両方とも、ProductName の値は 「変更後」になります。
これはMyBatisで検索した結果を「キャッシュ」している、具体的には 同じオブジェクトを返します。
toString() メソッドを呼ぶと、同一のオブジェクトIDを返します。
つまり、
ItemResult searchResult = itemRepository.getItemResult(productId);
ItemResult editResult = itemRepository.getItemResult(productId);
このように2つに分けたところで同じオブジェクトを扱いますので、片方を修正すれば、もう片方も修正されます。
キャッシュを意識しないためにすること
回避方法は2つあります。どちらかを採択しましょう。オススメは後者のイミュータブルです。
- クエリ単位でキャッシュを無効化する
- イミュータブルにする
キャッシュを無効化する
検索のクエリにキャッシュの属性 useCache を追加し、そして無効化 false します。
<select id="...." useCache="false">
SELECT .....
</select>
イミュータブルにする
ItemResultクラスの値を後から変更できないようにします。ミューテータメソッド1で変更できるのをやめ、値を変更したいならば改めて新しいオブジェクトを作り直すよう実装します。
やや冗長にも見えますが、事故が起きにくい実装です。
@Getter
public class ItemResult {
ItemResult() {
// MyBatis用にデフォルトコンストラクタを定義
}
public ItemResult(ProductId productId, ProductName productName) {
// @AllArgsConstructorで省略も可能
this.productId = productId;
this.productName = productName;
}
private ProductId productId;
private ProductName productName;
}
@AllArgsConstructor
@Controller
public class ItemSearchController {
ItemRepository itemRepository;
@GetMapping
public ModelAndView display(ModelAndView mnv, ItemForm form) {
ProductId productId = form.productId(); // formクラスから検索条件を取り出す実装をしておきます。
ItemResult searchResult = itemRepository.getItemResult(productId);
mnv.addObject("searchResult", searchResult);
ItemResult editResult = itemRepository.getItemResult(productId);
mnv.addObject("editResult", new ItemResult(editResult.getProductId(), ProductName.of("変更後")));
return mnv;
}
}
まとめ
MyBatisのキャッシュ機能と、その注意点を紹介しました。キャッシュは手軽にパフォーマンスを稼げる機能ですが、思いもよらないところでキャッシュされて不具合を生み出すこともあります。安全に使っていきましょう。