JavaのORM(O/Rマッパー)は、JPA実装のHibernate、SQLを手で書くMyBatisやDoma、SQLをJavaの型で組み立てるjOOQの3系統に分かれます。どれを選ぶかで、SQLを誰が書くか、遅いクエリをどこで直すかが変わります。この記事では2026年10月15日時点の最新版とライセンスで主要製品を比較し、Spring Bootで使うときのスターターと版の組み合わせ、N+1問題で実際に何本のSQLが流れるかをHibernate 7.4.13で測った結果まで扱います。
まとめ:Java ORM選定の結論
- Spring Bootで新規開発するなら、まずSpring Data JPA(実装はHibernate)。spring-boot-starter-data-jpaを入れればHibernateが自動設定されます。
- SQLを自分で管理したい、既存の複雑なSQL資産があるならMyBatisかDoma。Domaは注釈処理でSQLファイルの有無とSQLテンプレートの記述をコンパイル時に検証します。
- SQLをJavaの型で守りたいならjOOQ。OracleやSQL Serverで使うなら商用ライセンスが前提です。
- JPAの暗黙の動作を避けたいならSpring Data JDBC、マッピング自体が要らないならJdbcClient。どちらも遅延ロードやキャッシュを持たないので、発行SQLが読めます。
- 最後に更新状況を確認する。Reladomoは2022年2月の18.1.0以降リリースがなく、Hibernateも7.1・7.3系はすでにサポートが切れています。
ORM(O/Rマッパー)とは|読み方と役割
ORMとO/Rマッパーの読み方と正式名称
ORMは「オーアールエム」、O/Rマッパー(ORMapper)は「オーアールマッパー」と読みます。正式名称はObject-Relational Mappingで、Javaのオブジェクトとリレーショナルデータベースの表を対応づける仕組みです。Javaは参照とコレクションでデータを持ち、RDBは行と外部キーで持ちます。この差を毎回ResultSetから手で詰め替える代わりに、対応関係を宣言で済ませるのがORMの役割です。言語を問わないORMの仕組みとメリット・デメリットはORMとは?O/Rマッパーの仕組み・メリットとデメリットをSQL比較と実行結果で解説で扱っています。
ORM・JPA・Spring Data JPAの関係
この3語はレイヤーが違います。
- ORM/O/Rマッパー:オブジェクトと表を対応づける技術の総称。
- JPA(Jakarta Persistence):JavaにおけるORMのAPI仕様。現行の正式版は3.2(2024年4月)で、4.0はマイルストーン版(4.0.0-M7)の段階です。仕様なので単体では動きません。
- Hibernate・EclipseLink・OpenJPA:JPA仕様の実装。実際に動くのはこちらです。
- Spring Data JPA:JPAの上に、インターフェースを宣言するだけでリポジトリ実装を生成する層を載せたもの。
「JPAを使う」と言うとき、裏では必ず何らかの実装が動いています。Spring Bootでspring-boot-starter-data-jpaを入れた場合はHibernateです。Spring Frameworkのspring-ormモジュールも名前は似ていますが、HibernateやJPAをSpringのトランザクション管理につなぐための連携部品で、それ自体はORMではありません。MyBatisはJPAの実装ではなく、SQLと結果マッピングを担う別系統のライブラリです。
Javaの「Mapper」が指す2つのもの:MyBatisとMapStruct
「Java mapper」「@Mapper」で調べると、別物の2つが混ざって出てきます。どちらも@Mapperという名前のアノテーションを持つためです。
| 項目 | MyBatisの@Mapper | MapStructの@Mapper |
|---|---|---|
| パッケージ | org.apache.ibatis.annotations | org.mapstruct |
| 対応づける相手 | SQLの結果とJavaオブジェクト | Javaオブジェクト同士 |
| 実装が作られる時点 | 実行時(プロキシ) | コンパイル時(注釈処理) |
| DBアクセス | する | しない |
MyBatis-Spring-Boot-Starterは公式ドキュメントのとおり、@Mapperが付いたインターフェースを既定で探してSQLマッパーとして登録します。一方のMapStructは、エンティティとDTOの詰め替えコードをコンパイル時に生成する注釈処理ツールで、ORMではありません(最新安定版は1.6.3)。Spring Bootで「object mapping」を探している場合、必要なのは多くの場合こちらです。
Javaの主要ORM・O/Rマッパー一覧(2026年10月時点)
実務で候補に挙がる9製品を、系統・最新版・ライセンスで並べます。最新版はMaven Centralで2026年10月15日に確認した正式リリース(RC・ベータ・マイルストーンを除く)です。機能より先に、リリース年月の列を見てください。
| 製品 | 系統 | 最新版(リリース) | ライセンス | SQLの書き方 |
|---|---|---|---|---|
| Hibernate ORM | JPA実装 | 7.4.13(2026-10) | Apache 2.0 | 自動生成+HQL |
| EclipseLink | JPA実装 | 5.0.2(2026-09) | EPL 2.0 / EDL 1.0 | 自動生成+JPQL |
| Apache OpenJPA | JPA実装 | 4.1.1(2025-05) | Apache 2.0 | 自動生成+JPQL |
| Spring Data JDBC | 軽量ORM | 4.1.1(2026-08) | Apache 2.0 | 自動生成+@Query |
| MyBatis | SQLマッパー | 3.5.19(2025-01) | Apache 2.0 | 手書き(XML・注釈) |
| Doma | SQLマッパー | 3.14.1(2026-10) | Apache 2.0 | 手書き(2WaySQL) |
| uroboroSQL | SQLマッパー | 1.0.12(2026-07) | MIT | 手書き(2WaySQL) |
| jOOQ | 型安全SQL DSL | 3.21.9(2026-09) | Apache 2.0+商用 | Javaで組み立て |
| Reladomo | 独自ORM | 18.1.0(2022-02) | Apache 2.0 | 独自Finder API |
JPA実装系(Hibernate・EclipseLink・OpenJPA)
エンティティクラスに@Entityや@Columnを付け、SQLはフレームワークが生成します。事実上の標準はHibernateです。Hibernateのリリース一覧では、最新安定系列が7.4(7.4.13.Final・2026年10月11日)、限定サポートが7.2と6.6で、7.0・7.1・7.3はすでにend-of-lifeです。7.0でJakarta Persistence 3.2対応・Java 17必須・Apache License 2.0への移行が入り、次の8.0はJakarta Persistence 4.0対応としてベータ版(8.0.0.Beta3)が出ています。
EclipseLinkはJakarta Persistence 3.2の互換実装の1つで、ライセンスはEPL 2.0とEDL 1.0(BSD-3-Clause)の選択式です。WebLogicなど既存環境がEclipseLink前提の場合に選ぶのが現実的で、Spring Bootと組み合わせる限り、Hibernate以外を選ぶ理由は多くありません。OpenJPAは2025年5月の4.1.1以降、リリースが出ていません。
SQLマッパー系(MyBatis・Doma・uroboroSQL)
SQLを開発者が書き、結果とオブジェクトの対応づけだけをライブラリが担います。厳密にはSQLマッパーですが、O/Rマッパーの候補として同じ土俵で比較されます。
MyBatisは国内のSIer案件で採用例が多く、XMLか注釈でSQLを定義します。本体の正式リリースは2025年1月の3.5.19が最新ですが、Spring Boot連携のmybatis-spring-boot-starterは4.1.0(2026年7月)が出ており、Spring Boot 4.1に対応しています。Doma(3.14.1・Java 17以上)は注釈処理を使い、SQLファイルが存在するかとSQLテンプレート内のディレクティブ(バインド変数など)の文法をコンパイル時に検証します(公式ドキュメントの doma.sql.validation オプションの説明)。SQLファイルの置き忘れやディレクティブの書き誤りがビルドで止まるのは、注釈処理による検証を持たないMyBatis本体との差です。ただしDBの列名やテーブル名の誤りまでは検出しないので、実DBに対するテストは別に要ります。旧版のDoma 2から移る場合の判断はDoma2とは?Java向けORMの仕組みとDoma 3への移行判断にまとめています。uroboroSQL(1.0.12・MIT・Java 11以上)はフューチャー製で、SQLファイルがそのままDBクライアントでも実行できる2WaySQLを採ります。
型安全SQL DSL系(jOOQ)
jOOQはDBスキーマからJavaコードを生成し、SQLをメソッドチェーンで書きます。存在しない列を参照すればコンパイルエラーになります。注意点はライセンスです。jOOQのライセンスページは、Open Source Editionが対応していない商用DBでは正しく動く見込みが低いとして商用ライセンスの購入を強く勧めています。必要数は「jOOQのコードを書く開発者のワークステーションごとに1つ」で、サーバー分は含まれます。PostgreSQLやMySQLだけのプロジェクトなら無償の範囲で完結します。
JPA以外の選択肢:Spring Data JDBCとJdbcClient
マッピングの自動化が不要なら、Spring標準のJdbcTemplateや、その流れるようなAPI版のJdbcClient(Spring Framework 6.1で追加)でSQLを直接投げれば足ります。こちらはORMではなく、SQL実行と結果の受け取りを手伝う部品です。JPAとJdbcClientの中間にあるのがSpring Data JDBCです。リポジトリの書き方はSpring Data JPAとほぼ同じで、公式READMEは「キャッシュ、遅延ロード、write behindなどJPAの多くの機能を提供しない」と明言し、自らを「シンプルで機能を絞った、考え方のはっきりしたORM」と位置づけています。永続化コンテキストを持たないので、どのメソッドがいつSQLを流すかがコードから読めます。READMEが「集約(Aggregate)のCRUD」を掲げるとおり、集約単位で保存する設計と相性がよく、JPAの暗黙の動作に悩まされてきたチームの逃げ道になります。
Java ORMの選び方:4つの判断軸
1. SQLの自動生成と手書きの選択
最初に決めるのはここです。Spring Data JPAなら単純なCRUDはリポジトリのインターフェースを宣言するだけで済みますが、生成されるSQLが見えにくく、遅いときの原因追跡にHibernateの挙動理解が要ります。MyBatis・Doma・uroboroSQLはSQLを書く手間が残る代わりに、発行するSQLを一文字単位で調整できます。実行計画を選ぶのはDBのオプティマイザーですが、結合順やインデックスを意識したSQLに書き換えて計画を確かめる作業は、SQLが手元にあるほうが速く回せます。集計SQLが本体になるバッチや帳票ではSQLマッパー系が有利です。
2. 標準準拠とDB固有機能のトレードオフ
JPAは仕様なので、実装を差し替えても@EntityやEntityManagerのコードはそのまま動き、DBの方言もHibernateが吸収します。逆に、ウィンドウ関数やDB固有のヒント句を多用するなら標準に寄せる利点は小さく、SQLを直接書ける方式かjOOQが向きます。
3. ライセンスと保守継続性
機能比較表では差が出ないのに、後から効いてくる軸です。jOOQは商用DBで有償になり、Reladomoはリリース一覧の最終版が2022年2月の18.1.0で、4年以上新しい版が出ていません。バイテンポラル(適用期間と処理期間の二重管理)という独自の強みはあっても、新規採用でこの更新頻度を許容できるかは慎重に判断すべきです。
4. Spring Bootとの組み合わせ
Spring Bootを使うなら、自動設定されるSpring Data JPAかMyBatisのスターターが第一候補になります。どのORMでも、コネクションプールの設定は共通の性能要因です。Spring Bootの既定であるHikariCPとは?既定値一覧とSpring Bootでの設定・タイムアウト対処【7.1.0】の設定も確認してください。遅いときは、コネクション取得の待ち時間とSQLの実行時間を分けて測り、待ち時間が長ければプール、実行時間が長ければ発行SQLを疑うのが近道です。
Spring BootでのORMの導入:スターターと管理されるHibernateの版
スターターの対応とBootが固定するHibernateの版
Spring BootではORMの版を自分で書かず、Bootの依存管理(BOM)に任せるのが基本です。そのため、使えるHibernateの機能はBootの版で決まります。Maven Centralのspring-boot-dependenciesで確認した組み合わせは次のとおりです。
| Spring Boot | Hibernate | jOOQ | MyBatisスターター |
|---|---|---|---|
| 4.1.1(2026-08) | 7.4.5.Final | 3.21.7 | 4.1.0 |
| 4.0.8 | 7.2.24.Final | 3.19.37 | 4.0.x |
| 3.5.16 | 6.6.53.Final | 3.19.35 | 3.0.x |
使うスターターはJPAならspring-boot-starter-data-jpa、Spring Data JDBCならspring-boot-starter-data-jdbc、jOOQならspring-boot-starter-jooq、MyBatisならmybatis-spring-boot-starterです。MyBatisのスターターだけはSpring Boot本体の管理外なので、版を明記します。
<!-- Spring Boot 4.1系でJPAを使う(版はBootの依存管理に任せる) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MyBatisを使う(Boot 4.1には4.1.0を合わせる) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
Boot 4系の変更点はSpring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違い【2026年10月】、各版のサポート期限はSpring Bootバージョン一覧とサポート期限(EOL)【2026年10月】最新版は4.1、4.0の無償サポートは12月末終了で確認できます。
Spring Data JPAのエンティティとリポジトリ
インターフェースを宣言するだけで実装が生成されるため、基本的なCRUDにコードは要りません。
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
// getter・setter は省略
}
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
@Query("select u from User u left join fetch u.department where u.name = :name")
List<User> findWithDepartment(@Param("name") String name);
}
findByNameはメソッド名から where name = ? のSQLが導出されます。findWithDepartmentは部署を同じSQLで取り、left join fetchなので部署未所属のユーザーも結果に残ります(join fetchにすると未所属のユーザーは結果から消えます)。条件が複雑になるとメソッド名が破綻するので、その場合は@QueryでJPQLを書きます。@ManyToOneに明示しているFetchType.LAZYの理由は、次の実測章で数字として示します。
JPAとMyBatisの使い分け
「JPAよりMyBatisが良い」と語られる場面は、実際には条件付きです。判断は次の対比で足ります。
| 観点 | JPA(Hibernate) | MyBatis |
|---|---|---|
| 単純なCRUD | コード量が最小 | SQLを都度書く |
| 複雑な集計・結合 | JPQLでは書きにくい | SQLそのまま |
| 実行されるSQL | 生成に依存 | 書いたSQLが流れる |
| 既存スキーマへの適合 | エンティティ設計が先行 | 合わせやすい |
| 学習で詰まる点 | 永続化コンテキスト | 動的SQLの記法 |
MyBatisでは、SQLをインターフェースに直接書けます。
@Mapper
public interface UserMapper {
@Select("SELECT id, name FROM users WHERE name = #{name}")
List<User> findByName(String name);
}
MyBatisが向くのは、スキーマを変更できないレガシーDB、SQLチューニングが要件に入る案件、SQLに強いメンバーが多いチームです。エンティティ中心にドメインを設計し、スキーマもアプリ側が主導できる新規開発では、JPAのほうがコードが薄くなります。検索条件が多い画面はMyBatis Dynamic SQLの使い方|if・where・foreachと2.0.0の型安全DSLを実行結果で解説、テーブルからのコード生成はMyBatis Generatorの使い方|2.0.0の設定・Maven実行・生成コードの選び方が参考になります。
両者は排他ではありません。JPAを基本にして、帳票やバッチの集計クエリだけMyBatisやjOOQに切り出す構成には理由があります。JPAでもDTOやrecordへの射影(プロジェクション)で集計結果を受け取れますが、ウィンドウ関数やDB固有の構文を多用する長いSQLは、JPQLやネイティブクエリの文字列で持つよりSQLファイルやjOOQのDSLで持つほうが読みやすく、レビューもしやすいからです。
既存システムのORMの置き換えや、JPAとSQLマッパーを併用する構成の設計から任せたい場合は、Javaでのフルスクラッチ開発として相談を受けています。
N+1問題の発行SQL本数をHibernate 7.4.13で実測
ORMを使うと起きやすい性能問題の代表がN+1問題です。親を1回のSELECTで取ったあと、関連を参照するたびに追加のSELECTが走ります。言葉で説明するより本数を見たほうが早いので、Hibernate 7.4.13.FinalとH2 2.5.252(Java 25)で、ユーザー100件・部署10件と、ユーザー100件・部署100件の2通りを用意し、HibernateのStatementInspectorで発行SQLを数えました。
取得方法別のSQL本数
| 取得方法 | 部署10件 | 部署100件 |
|---|---|---|
| EAGER(既定)+JPQLで全件 | 11本 | 101本 |
| EAGER(既定)+find(id)で1件だけ取得 | 1本 | 1本 |
| LAZY+ループで部署名を参照 | 11本 | 101本 |
| LAZY+join fetch | 1本 | 1本 |
| LAZY+fetchgraph(@EntityGraph相当) | 1本 | 1本 |
| LAZY+default_batch_fetch_size=16 | 2本 | 8本 |
find(id)の行だけはユーザー1件の取得で、ほかの行はユーザー100件を取得しています。読み取れることは3つあります。まず、追加SQLの本数は「ユーザー数」ではなく「参照される部署の種類数」で決まります。同じ部署は永続化コンテキストに1回載れば再取得されないためで、部署10件なら1+10本、部署100件なら1+100本でした。開発環境のテストデータで関連先の種類が少ないと、N+1は小さく見えて見逃されます。
次に、@ManyToOneの既定であるEAGERにしても防げません。HibernateがJOINでまとめて取ったのはfind(id)のときだけで、JPQLで全件取ると、LAZYでループした場合と同じ本数が流れました。EAGERは「必ず取る」という意味であって、「1本のSQLで取る」という意味ではありません。
最後に、hibernate.default_batch_fetch_sizeを16にすると、未取得の部署をIN句で16件ずつまとめて取るため、部署100件でも1+7本に収まりました。Spring Bootなら spring.jpa.properties.hibernate.default_batch_fetch_size=16 と1行書くだけで全エンティティに効くので、join fetchを書き漏らした箇所の保険になります。計測に使ったコードの要点だけを抜粋すると次のとおりです(LazyUserはLAZYの@ManyToOneで部署を持つエンティティ、sessionFactoryはStatementInspectorを登録して組み立てたもの)。
// 発行SQLを記録するStatementInspector
public class Rec implements StatementInspector {
public static final List<String> SQL = new ArrayList<>();
public String inspect(String sql) { SQL.add(sql); return sql; }
}
// hibernate.session_factory.statement_inspector に Rec を登録して実行
Rec.SQL.clear();
sessionFactory.inSession(s ->
s.createQuery("select u from LazyUser u", LazyUser.class)
.getResultList()
.forEach(u -> u.getDept().getName()));
System.out.println(Rec.SQL.size()); // 部署10件なら11
Django・Rails・Laravelでの対策や、ORMの外側で起きるN+1はN+1問題とは?発行クエリ数の実測と検出方法・ORM別の対策で扱っています。
Hibernate 7.4で変わったページングとコレクションのjoin fetch
部署に属するユーザー一覧のような@OneToManyのコレクションをjoin fetchしつつ、setMaxResults(3)でページングするクエリは、長くHibernateの落とし穴でした。同じクエリを7.2.25と7.4.13で実行すると、発行SQLが変わります。
- 7.2.25.Final:WARN「HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory」が出て、SQLにLIMITが付かず全行を取得し、JVM内で3件に絞る。
- 7.4.13.Final:警告なし。部署をサブクエリ内で「offset ? rows fetch first ? rows only」で3件に絞ってからユーザーを結合する、1本のSQLになる。
Hibernate 7.4のWhat’s Newはこの変更を、Hibernate 6と7.4より前の7系ではJVM内でlimitを適用していたため性能が悪かった問題が解決した、と説明しています(Sybase ASEを除く)。旧挙動に戻すクエリヒントはorg.hibernate.limitInMemoryです。Spring Bootでは4.1系(Hibernate 7.4.5)から恩恵を受け、4.0系(7.2.24)と3.5系(6.6.53)では従来どおりメモリ内で絞られます。ログにHHH90003004が出ているなら、Bootを4.1へ上げるか、IDだけを先にページングで取ってから関連を取る2段階のクエリに分けてください。
ORMを採用すべきでないケース
ORMの価値は「SQLを書かないこと」ではなく、ドメインモデルを中心に据えた開発で詰め替えコードを減らすことにあります。次のケースでは、JdbcClientやSQLマッパーを選ぶほうが正解です。
- 数十万件規模のバッチ更新:永続化コンテキストが読み込んだエンティティを抱え続け、メモリを圧迫します。バルクUPDATEかJDBCのバッチ実行が向きます。
- 集計・分析が中心の処理:GROUP BYやウィンドウ関数が主役なら、エンティティに写像する意味がありません。SQLで書き、DTOやrecordで受けます。
- スキーマを変更できず、正規化も崩れているDB:エンティティ設計が歪み、マッピング定義の保守がSQLを書くより高くつきます。
JPAを採用した場合も、Spring Bootの既定で有効なOpen Session in Viewは、ビュー描画中の遅延ロードでN+1を隠す原因になります。扱いはspring.jpa.open-in-viewとは?OSIVの既定値trueと起動時警告の消し方・falseの判断基準を参照してください。
よくある質問
ORMの読み方は?
「オーアールエム」と読みます。O/Rマッパー(ORMapper)は「オーアールマッパー」です。正式名称はObject-Relational Mappingで、オブジェクトとリレーショナルデータベースの表を対応づける技術を指します。
MyBatisはORMですか?
厳密にはSQLマッパーです。JPAのようにSQLを自動生成せず、開発者が書いたSQLの結果をオブジェクトへ対応づけます。ただしJavaのO/Rマッパーの選択肢としてはJPA実装と同じ土俵で比較されるため、実務上はORMの一種として扱われます。
JPAとORMの違いは?
ORMは技術の総称で、JPA(Jakarta Persistence)はJavaでORMを行うためのAPI仕様です。JPAは仕様なので単体では動かず、HibernateやEclipseLinkといった実装が必要です。
Spring BootのORMには何が使われていますか?
spring-boot-starter-data-jpaを入れた場合、JPA実装としてHibernateが自動設定されます。Spring Boot 4.1.1が管理する版は7.4.5.Finalです。MyBatisを使う場合はmybatis-spring-boot-starterを追加します。
Hibernateの最新バージョンは?
2026年10月15日時点の最新安定版は7.4.13.Final(2026年10月11日)です。7.2と6.6は限定サポート、7.0・7.1・7.3はend-of-lifeです。Jakarta Persistence 4.0に対応する8.0はベータ版の段階です。