Spring Boot API サーバーに JPA ではなく MyBatis を選んだ理由
はじめに
Spring Boot でREST APIを作るとき、多くのチュートリアルは Spring Data JPA を使います。
私も最初はJPAで実装しようとしました。
しかし、顧客向けDVDレンタルAPIの実装にあたり、MyBatisを選びました。
この記事は「JPAが悪い」という話ではなく、「なぜそのケースではMyBatisの方が合っていたか」の話です。
JPAでつまずいた場面
DVDレンタルのDBは、元々PostgreSQLのサンプルDB dvdrental をベースにしています。
テーブルは約15個あり、リレーションが複雑に絡み合っています。
film ←→ film_actor ←→ actor
film ←→ film_category ←→ category
inventory → film
rental → inventory → film
payment → rental
顧客向けAPIで「作品一覧(カテゴリ付き)」を返すエンドポイントを作ろうとしたとき、JPAでこんな問題が起きました。
問題1:N+1クエリ
@ManyToMany でfilmとcategoryを繋ぐと、filmを1件取得するたびにcategoryを取得するクエリが走ります。
1,000件の作品があれば、1,001本のSQLが実行されます。
// これが1000件あると1001本のSQLになる
List<Film> films = filmRepository.findAll();
films.forEach(f -> f.getCategories()); // ← ここでN+1
@EntityGraph や fetch join で回避できますが、記述が複雑になります。
問題2:集計クエリが書きにくい
「カテゴリ別の作品数」「在庫が残っている作品の一覧」など、GROUP BYやサブクエリが必要な集計は、JPQLで書くと可読性が下がります。
// JPQLでGROUP BYを書くとこうなる
@Query("SELECT c.name, COUNT(fc) FROM FilmCategory fc " +
"JOIN fc.category c GROUP BY c.name ORDER BY COUNT(fc) DESC")
List<Object[]> countByCategory();
返り値が List<Object[]> になるので、型安全性が失われます。
MyBatisを選んだ理由
理由1:SQLをそのまま書ける
MyBatisはSQLをXMLまたはアノテーションで直接書けます。
<!-- FilmMapper.xml -->
<select id="findAllWithCategory" resultType="PublicFilmSummary">
SELECT
f.film_id,
f.title,
c.name AS category_name,
f.length,
f.description
FROM film f
LEFT JOIN film_category fc ON f.film_id = fc.film_id
LEFT JOIN category c ON fc.category_id = c.category_id
ORDER BY f.title
</select>
書いたSQLがそのまま実行されます。動作が予測しやすく、デバッグが簡単です。
理由2:複雑な集計クエリが自然に書ける
<select id="findFilmsInStock" resultType="PublicFilmSummary">
SELECT DISTINCT
f.film_id,
f.title,
COUNT(i.inventory_id) AS stock_count
FROM film f
JOIN inventory i ON f.film_id = i.film_id
LEFT JOIN rental r ON i.inventory_id = r.inventory_id
AND r.return_date IS NULL
WHERE r.rental_id IS NULL
GROUP BY f.film_id, f.title
HAVING COUNT(i.inventory_id) > 0
</select>
JPAのJPQLで同じことを書くと、かなり難解になります。
理由3:既存のDBに乗る場合はSQLが正確
dvdrental は既存のDBです。テーブル名・カラム名がすでに決まっています。
JPAの @Column(name = "...") アノテーションを全フィールドに付けるより、
SQLに直接書いた方がミスが少ない。
MyBatisのセットアップ
<!-- pom.xml -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
// Mapperインターフェース
@Mapper
public interface PublicFilmMapper {
List<PublicFilmSummary> findAllWithCategory();
Optional<PublicFilmDetail> findById(int filmId);
}
# application.yml
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true # snake_case → camelCase 自動変換
JPAを使うべきケース
MyBatisが正解ではなく、ケースバイケースです。
| ケース | 向いている技術 |
|---|---|
| 単純なCRUDが中心 | JPA(Spring Data JPA) |
| 複雑なJOIN・集計が多い | MyBatis |
| 既存DBに乗る | MyBatis |
| ゼロからDB設計する | JPA |
| 型安全なクエリビルダーが欲しい | jOOQ |
まとめ
- DVDレンタルの顧客向けAPIは複雑なJOINと集計が多かったため、MyBatisを選択した
- MyBatisはSQLをそのまま書けるため、既存DBへの乗りが良く、動作の予測が容易
- N+1問題を気にせず、書いたSQLがそのまま実行されることがデバッグのしやすさに繋がった
- 「どちらが優れているか」ではなく「何を作るかで選ぶ」のが正解
このシリーズの記事マップ
→ dvdrental 管理アプリと対になるエンドユーザー向けDVDレンタルアプリを作っている話 — Vue 3 + Spring Boot の全体構成と記事マップ