新規のSpring Bootプロジェクトを始めるとき、ORマッパーの選定で悩むことってありますよね。JPAとMyBatis、どちらを選ぶべきなのか。チームメンバーから「どっちがいいの?」と聞かれて困った経験がある人も多いはずです。
本記事では、JPAとMyBatisの特性を比較しながら、プロジェクトの要件に応じた選定基準と併用パターンを解説します。
JPAとMyBatisの基本的な違い
まず両者の設計思想の違いを押さえておきましょう。
JPA はオブジェクト指向のアプローチです。Javaのエンティティクラスを中心に設計し、データベースとのマッピングは自動で行われます。JPAは仕様であり、Hibernateなどが実装を提供しています。
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
MyBatis はSQL中心のアプローチです。SQLを明示的に記述し、その結果をJavaオブジェクトにマッピングします。
<select id="findById" resultType="User">
SELECT id, name, email FROM users WHERE id = #{id}
</select>
この根本的な違いが、使い勝手や得意な領域に大きく影響します。
学習コストと習熟曲線
チームの技術レベルに応じて選ぶという視点も重要です。
MyBatisはSQLが書ければすぐに使えます。Mapper XMLにSQLを書いて、Javaインターフェースと紐付けるだけ。習得のハードルは低いですね。
一方、JPAはエンティティ設計、遅延ロード、永続化コンテキストといった独自の概念を理解する必要があります。初期の学習コストは高めですが、一度慣れればCRUD操作が驚くほど簡単になります。
チームがSQL得意ならMyBatis、オブジェクト指向設計に慣れているならJPAが扱いやすいでしょう。
シンプルなCRUD操作での比較
基本的なデータアクセスでどれくらい違うのか見てみましょう。
JPAの場合
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByStatus(String status);
}
Repositoryインターフェースを継承するだけで、基本的なCRUDメソッドが自動的に使えます。findByXxxのようなメソッド名規約で検索メソッドも自動生成されます。
MyBatisの場合
@Mapper
public interface UserMapper {
List<User> findByStatus(@Param("status") String status);
void insert(User user);
void update(User user);
void delete(Long id);
}
<mapper namespace="com.example.mapper.UserMapper">
<select id="findByStatus" resultType="com.example.model.User">
SELECT * FROM users WHERE status = #{status}
</select>
<insert id="insert">
INSERT INTO users (name, email, status)
VALUES (#{name}, #{email}, #{status})
</insert>
<update id="update">
UPDATE users
SET name = #{name}, email = #{email}, status = #{status}
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM users WHERE id = #{id}
</delete>
</mapper>
MapperインターフェースとXMLファイルの両方が必要です。SQLを明示的に書く分、記述量は増えますね。
定型的なCRUD操作が多いプロジェクトでは、JPAのコード削減効果が大きいです。
複雑なクエリでの比較
JOINや集計処理が多い画面ではどうでしょうか。
JPAでの複雑なクエリ
@Query("""
SELECT u FROM User u JOIN u.orders o
WHERE o.orderDate BETWEEN :start AND :end
GROUP BY u.id
HAVING SUM(o.amount) > :threshold
""")
List<User> findHighValueCustomers(
@Param("start") LocalDate start,
@Param("end") LocalDate end,
@Param("threshold") BigDecimal threshold
);
JPQLで記述できますが、複雑になると可読性が落ちます。CriteriaAPIを使うとさらに冗長になります。
MyBatisでの複雑なクエリ
<select id="findHighValueCustomers" resultType="User">
SELECT u.*, SUM(o.amount) as total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.order_date BETWEEN #{start} AND #{end}
GROUP BY u.id
HAVING SUM(o.amount) > #{threshold}
</select>
生のSQLが書けるので、複雑なクエリでも読みやすく保てます。動的SQLもMyBatisの得意分野です。
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE #{name}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
<if> や <choose> タグで条件分岐が直感的に書けます。<trim> や <foreach> を使えば、さらに柔軟な動的SQL構築が可能です。
レポート画面や分析機能など、複雑なSQLが必要な部分ではMyBatisの方が扱いやすいですね。
パフォーマンス特性の違い
JPAは便利な反面、N+1問題に注意が必要です。
List<User> users = userRepository.findAll();
for (User user : users) {
user.getOrders().size(); // ここで追加のクエリが発行される
}
Fetch戦略を適切に設定すれば回避できます。パフォーマンス最適化では詳しく解説しています。
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();
MyBatisは発行されるSQLを完全に制御できるため、予期しないクエリが走ることはありません。ただし、不適切なresultMap設定(ネストしたコレクションマッピング)では、N+1的な問題が起きる可能性もあります。
JPAには二次キャッシュなど高度なキャッシュ機能がありますが、設定が複雑になりがち。MyBatisのキャッシュはシンプルですが、デフォルトでは同一SqlSession内のみ有効で、分散環境での利用には制約があります。
保守性の観点
長期運用を考えると保守性も大事ですよね。
JPAはテーブル構造が変わったとき、エンティティクラスを修正すれば多くのクエリが自動追従します。カラム追加程度ならコード変更が最小限で済みます。
MyBatisはSQL変更が必要ですが、影響範囲は明確です。どのMapperを修正すればいいか一目瞭然なので、大規模な変更でも追跡しやすいです。
単体テストに関しては、MyBatisの方がモック化がシンプルです。JPAは永続化コンテキストの管理があるため、テスト用の設定がやや複雑になります。
実務的な使い分け基準
では、どう選べばいいのか。プロジェクトの特性で判断しましょう。
- CRUD中心の業務アプリ → JPA推奨。定型操作が多いほど生産性が上がります
- 複雑なレポート・分析画面 → MyBatis推奨。柔軟なSQL制御が活きます
- チームのSQL習熟度が高い → MyBatisが自然に扱えます
- スキーマ変更が頻繁 → JPAの自動追従が便利です
- 既存DBへの後付け実装 → MyBatisの柔軟性が役立ちます
絶対的な優劣はありません。要件とチームの特性で選ぶのが正解です。
JPAとMyBatisの併用パターン
実は、両方を同時に使うこともできます。Spring Bootなら設定も簡単です。
用途による使い分けが効果的です。
- 通常のCRUD操作 → JPA
- 複雑な検索・集計 → MyBatis
トランザクション管理は共通の PlatformTransactionManager で統一できるので、@Transactionalアノテーションで一貫して制御できます。
パッケージ分割で明示的に区別すると分かりやすいですね。
com.example.repository (JPA)
com.example.mapper (MyBatis)
Serviceレイヤーでは、CRUD操作にはJpaRepositoryを、レポート生成にはMapperを注入するといった使い分けができます。
併用時の設定例
具体的な設定を見てみましょう。
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
runtimeOnly 'com.h2database:h2'
}
application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
mybatis:
mapper-locations: classpath:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
ddl-autoの設定値は環境によって使い分けます。
- create - 起動時にテーブルを作り直す(開発初期)
- update - スキーマを自動更新(開発環境)
- validate - スキーマ検証のみ(ステージング・本番)
- none - 何もしない(Flywayなど別ツールで管理する場合)
本番環境では validate または none を推奨します。
設定クラス
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@MapperScan("com.example.mapper")
public class DataAccessConfig {
// DataSourceとTransactionManagerは自動設定される
}
basePackagesを明示することで、JPAとMyBatisのスキャン範囲が明確になり、競合を防げます。
MyBatisからJPA、JPAからMyBatisへの移行
既存プロジェクトで技術スタックを変更するケースもありますよね。
段階的移行が現実的です。
- 新機能からターゲット技術で実装開始
- 既存機能は並行稼働させる(併用期間)
- リスクの低い箇所から順次置き換え
- 十分なテストカバレッジを確保しながら進める
MyBatis → JPAへの移行
エンティティ設計とリレーションシップマッピングがポイントです。MyBatisで明示的に書いていたJOINを、JPAのリレーションシップ(@OneToMany、@ManyToOneなど)で表現します。
JPA → MyBatisへの移行
暗黙的なクエリの明示化が必要です。JPAが自動生成していたクエリを、Mapper XMLに書き出す作業が中心になります。遅延ロードで隠れていたN+1問題が、移行時に顕在化することもあるので注意が必要です。
どちらの場合も、まず並行稼働できる環境を整えてから、段階的に移行するのが安全です。
まとめ
JPAとMyBatis、どちらを選ぶべきかは要件次第です。
CRUD操作が中心ならJPAの生産性が光ります。複雑なSQLが必要ならMyBatisの制御性が役立ちます。
併用も現実的な選択肢です。それぞれの得意分野で使い分ければ、両方のメリットを享受できます。
まずは小規模な機能で試してみて、チームとの相性を確認するのがおすすめです。たとえばユーザーCRUDをJPAで、売上集計レポートをMyBatisで実装してみる、といった形ですね。
技術選定に絶対の正解はありません。プロジェクトとチームに合った選択をしましょう。