Tech Blog

Spring Boot API サーバーに JPA ではなく MyBatis を選んだ理由

Spring Boot MyBatis JPA Java

はじめに

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

@EntityGraphfetch 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 の全体構成と記事マップ

気軽にメッセージください

技術相談・ご感想・ご質問があればメッセージをお願いします。