spring.jpa.open-in-viewとは?OSIVの既定値trueと起動時警告の消し方・falseの判断基準
Spring Bootアプリを起動すると、何も設定していないのに spring.jpa.open-in-view is enabled by default... というWARNがログに流れます。OSIV(Open Session In View)が黙って有効になっていることの通知です。ただし警告文が要求しているのは「明示的に設定せよ」だけで、trueとfalseのどちらにすべきかは教えてくれません。既定値の根拠から、警告の発火条件、falseにした直後に噴き出すLazyInitializationExceptionの手当てまでを扱います。
まとめ
spring.jpa.open-in-view は、JPAの永続化コンテキスト(EntityManager)をHTTPリクエストの処理全体に束縛するかどうかのスイッチです。未設定なら有効、つまり既定値はtrue。Spring Boot 4.1系と3.5系のどちらの現行ソースでも変わりません。起動時のWARNが出るのは「値が未設定のとき」だけで、spring.jpa.open-in-view=true と明示すれば挙動は一切変わらないまま警告だけが消えます。警告を消したい一心でtrueを書き足すのは、問題を隠す操作です。
falseにするかは、ビュー層やJSON変換の時点で遅延ロードに頼っているコードがあるかで決めてください。OSIVを切ると LazyInitializationException が表に出ますが、新しい不具合ではありません。これまでOSIVが覆い隠していた取得設計の穴が可視化されただけです。JOIN FETCH、@EntityGraph、DTOプロジェクションのいずれかで、必要なデータをトランザクション内に取り切る形へ書き換えていきます。
spring.jpa.open-in-viewの実体と、効く範囲の限界
既定値trueの根拠はmatchIfMissing=trueの条件式
このプロパティの既定値は、ドキュメントの記述ではなく自動構成の条件式が決めています。Spring Bootの JpaBaseConfiguration に入れ子で定義された JpaWebConfiguration には、次の条件が付いています。
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(WebMvcConfigurer.class)
@ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class, OpenEntityManagerInViewFilter.class })
@ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class)
@ConditionalOnBooleanProperty(name = "spring.jpa.open-in-view", matchIfMissing = true)
protected static class JpaWebConfiguration {
最後の matchIfMissing = true が既定値の正体です。プロパティが存在しない場合も条件が成立し、OpenEntityManagerInViewInterceptor がBeanとして登録されます。この記述はSpring Boot 4.1.0(2026年6月10日リリース)のタグと3.5系ブランチで同一でした。@ConditionalOnBooleanProperty 自体はSpring Boot 3.4で導入された注釈なので、3.3以前でソースを照合するときは @ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view", havingValue = "true", matchIfMissing = true) を探してください。
登録される OpenEntityManagerInViewInterceptor はSpring Framework側のクラスで、preHandle で EntityManager を生成して TransactionSynchronizationManager.bindResource でスレッドへ束縛し、afterCompletion で解除してクローズします。postHandle は空実装。つまりコントローラに入る前から、レスポンスを書き終えた後まで、同じ永続化コンテキストが生き続けます。
ここでのSessionはHTTPセッションではなく永続化コンテキスト
Open Session In Viewという名前のSessionは、Hibernateの Session、JPAでいう EntityManager を指します。ログイン状態を保持するHTTPセッション(HttpSession)とは無関係です。
この取り違えは実害を生みます。セッションタイムアウトを短くしてもセッションIDを再生成しても、OSIVの挙動は1ミリも変わりません。逆にOSIVを無効化してもログイン状態には影響しません。spring.jpa.open-in-view の文脈で出てくる「セッションが長く開いたまま」は、1次キャッシュを抱えた永続化コンテキストがリクエスト終端まで残るという意味です。JPAとHibernateの関係が曖昧なら、Java ORM(O/Rマッパー)の種類と選び方|Hibernate・MyBatis・jOOQを比較【2026年版】で仕様と実装の分担を先に押さえておくとよいでしょう。
Servletスタック限定:WebFlux・バッチ処理が対象外になる条件
上の条件式には @ConditionalOnWebApplication(type = Type.SERVLET) と @ConditionalOnClass(WebMvcConfigurer.class) が付いています。Spring WebFluxだけを使うリアクティブアプリでは、この自動構成は最初から成立しません。trueにしてもfalseにしても何も起こらない、ということです。@ConditionalOnMissingBean と @ConditionalOnMissingFilterBean も効くため、自前で OpenEntityManagerInViewInterceptor かサーブレットフィルタ版の OpenEntityManagerInViewFilter を登録しているアプリでも、このプロパティは無視されます。
同じことが @Scheduled のバッチ、メッセージリスナー、テストコードなどHTTPリクエスト以外の入口すべてに当てはまります。同じロジックを複数の入口へ露出する設計なら、OSIVに依存しない書き方へ寄せておかないと入口ごとに挙動が割れます。
起動時WARNが出る条件と、消し方の分岐
警告の発火条件はgetOpenInView()のnull判定
Bean定義メソッドの中身を見ると、警告の発火条件は明快です。
@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
if (this.jpaProperties.getOpenInView() == null) {
logger.warn("spring.jpa.open-in-view is enabled by default. "
+ "Therefore, database queries may be performed during view "
+ "rendering. Explicitly configure spring.jpa.open-in-view to disable this warning");
}
return new OpenEntityManagerInViewInterceptor();
}
JpaProperties の openInView フィールドは Boolean のnull許容型で、未設定ならnullのままです。判定しているのは値がtrueかどうかではなく、開発者が意思表示をしたかどうかでした。この警告はspring-bootのissue #7107(2016年10月5日起票、2017年10月30日クローズ、マイルストーン2.0.0.M6)で入りました。起票時の要求は「OSIVは広くアンチパターンとみなされている以上、既定で有効にすべきではなくオプトインにすべきだ」で、既定値そのものは変えず通知だけ足す決着になっています。
現場でよく見る誤った回避策が、ロガーごと黙らせるやり方です。ロガー名は内部クラスの JpaWebConfiguration、パッケージはSpring Boot 4.x系が org.springframework.boot.jpa.autoconfigure、3.5以前が org.springframework.boot.autoconfigure.orm.jpa です。ログレベルで潰せば確かに静かになりますが、消えるのは警告だけで、判断した記録も一緒に消えます。
trueを明示する場合とfalseにする場合の線引き
選択肢は事実上3つで、それぞれ意味が違います。
| 設定 | OSIV | 起動時WARN | 意味 |
|---|---|---|---|
| 未設定 | 有効 | 出る | 未判断のまま既定に乗っている |
| true を明示 | 有効 | 出ない | 有効のまま使うと決めた |
| false | 無効 | 出ない | 取得プランを自前で管理すると決めた |
書き方はプロパティ1行です。application.properties なら上、application.yml なら下になります。
spring.jpa.open-in-view=false
spring:
jpa:
open-in-view: false
falseへ倒すべきなのは、レスポンスをJSONで返すAPIサーバで、レイテンシやコネクションプールの逼迫を実測で問題視している場合です。逆にtrueのままでよいのは、Thymeleafでサーバサイドレンダリングしていて画面数が限られ、同時接続も想定内に収まっている管理画面のような領域。既存の大規模アプリで遅延ロード依存箇所を洗い出す工数が取れないなら、無理に倒す必要はありません。falseにして壊れる箇所が本番で初めて露出する事故のほうが、コネクション占有時間より高くつきます。切り替えるなら新規プロジェクトの初日か、統合テストで全エンドポイントを踏める体制ができてからです。
OSIV有効時の永続化コンテキスト寿命と接続リースの実像
リクエスト終端まで生き残る永続化コンテキストと1次キャッシュの滞留
サービス層のトランザクションがコミットしても、EntityManager は閉じません。閉じるのは afterCompletion、つまりレスポンスを書き終えた後です。その間、読み込んだエンティティはすべて1次キャッシュに滞留します。
一覧画面で数千件のエンティティを読み、DTOへ詰め替えて返す処理を考えてください。詰め替えが終わってもマネージド状態のエンティティは解放されず、リクエストが終わるまでヒープに残ります。issue #47547の起票者が挙げた被害も、コネクションプールの枯渇(connection pool starvation)と並んで「セッションの寿命が延びることによるメモリリーク」でした。
「接続を握りっぱなし」という説明の精度
OSIVの解説では「リクエストの間ずっとDB接続を占有する」と書かれることが多いのですが、実装の既定と正確には一致しません。Hibernateの hibernate.connection.handling_mode の既定値は DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION、公式javadocの定義は「接続は必要になった時点で取得され、各トランザクションの完了後に解放される」です(Hibernate 5.2以降)。解放はセッションのクローズではなくトランザクションの完了に紐付きます。
一方で取得の遅延は既定では効きません。hibernate.connection.provider_disables_autocommit が既定falseのため、Hibernateは取得した接続に Connection#setAutoCommit(boolean) を呼びに行き、その分だけ取得が前倒しされるからです。加えてビュー層の遅延ロードはトランザクションの外側で走るので、接続の取得・解放とコミットがリクエストの後半にも散らばります。Vlad Mihalcea氏はこの全体を「データベース接続がUI描画フェーズを通じて保持され、接続のリース時間が延び、コネクションプールの輻輳によって全体のトランザクションスループットが制限される」と表現し、さらに「UI描画フェーズから発行される追加の文はすべてauto-commitモードで実行される。auto-commitは1文ごとにトランザクションログをディスクへフラッシュしなければならないためデータベースサーバに負荷をかける」と指摘しています。
実際のリース時間はHibernateの設定と接続プールの実装で変わります。伝聞で決めず、HikariCPのメトリクスで測ってください。Micrometer経由なら hikaricp.connections.usage が1接続あたりの利用時間、hikaricp.connections.acquire が取得待ち時間、hikaricp.connections.pending が接続を待っているスレッド数です。OSIVを切る前後でこの3つを比べれば、自分のアプリで効果があるかを数字で判断できます。
ビュー描画とJSON変換で走るN+1クエリ
OSIVはテンプレートエンジンを使う画面だけの話ではありません。@RestController がエンティティをそのまま返す場合、JacksonによるJSON変換もOSIVの守備範囲で動きます。変換中に遅延ロードのコレクションへ触れれば、そこでSQLが発行されます。
親エンティティを100件返すAPIが各要素の子コレクションをJSONへ含めると、SQLは1+100本になります。これが典型的なN+1で、しかもサービス層のログには現れず、コントローラのメソッドが返った後に起きるため原因が追いにくい。ORM全般で共通する構造なので、対策の考え方はDjangoとRailsにおけるN+1問題解決法の比較と考察で整理した内容がそのまま使えます。そもそもこの経路でエンティティを直接シリアライズする設計自体が、OSIVとは別に見直し対象です。エンティティの構造変更がAPIレスポンスの互換性を壊します。
falseへ切り替えた直後のLazyInitializationException対策
JOIN FETCHによる一括取得と、Hibernate版で変わる制約
最も直接的な手当てが、JPQLの join fetch です。関連を同じSQLで取得するので、トランザクションを抜けた後に触っても初期化済みです。
@Query("select p from Parent p join fetch p.children where p.status = :status")
List<Parent> findWithChildren(@Param("status") Status status);
ここで distinct を書いていないのは意図的です。Hibernate 6.0の移行ガイドは「Hibernate ORM 6以降、子コレクションをjoin fetchする際に同一の親エンティティ参照を除外する目的で distinct をJPQLやHQLに使う必要はなくなった」と明記し、あわせて「distinct は常にSQLクエリへ渡される」としています。惰性で付けるとSQL側に SELECT DISTINCT がそのまま出て、フェッチした全列のソートやハッシュを無駄に走らせるだけです。Hibernate 5世代の作法として広まった書き方なので、コピー元の年式に注意してください。
ページングとの併用は、同梱しているHibernateの版で結論が変わります。Spring Boot 3.5.16はHibernate 6.6.53.Final、4.1.0は7.4.1.Finalを同梱しており(各spring-boot-dependenciesで実測)、6.6系まではコレクションフェッチとページングを併用すると全件を読んでJVM側でページングし、HHH90003004 firstResult/maxResults specified with collection fetch; applying in memory の警告とともに大量データで破綻しました。Hibernate 7.4の移行ガイドは「limitはSQLクエリの一部として処理されるようになった。以前の挙動に戻すにはクエリヒント org.hibernate.limitInMemory を設定する」としており、4.1系ではこの制約が解消しています。
複数コレクションの同時フェッチには別の制約があります。MultipleBagFetchException のjavadocの定義は「複数のbagを同時にフェッチしようとしたことを示す例外」で、対象はbag、すなわち @OrderColumn を持たない List です。両方を Set にすれば例外は避けられますが、今度は結果がデカルト積になります。件数が伸びる関連は、1本のクエリに詰め込まず複数クエリへ割るのが実務解です。
@EntityGraphによる取得プランの用途別出し分け
クエリ本体は変えずに取得プランだけを差し替えたいなら、Spring Data JPAの @EntityGraph を使います。
@EntityGraph(attributePaths = {"children"})
List<Parent> findByStatus(Status status);
同じエンティティに対して、用途ごとに違う取得プランを持てるのが利点です。一覧では子を取らず、詳細では取る、という出し分けがリポジトリのメソッド単位で書けます。エンティティ側に FetchType.EAGER を書いてしまうと全ユースケースに影響が及ぶので、取得の粒度はマッピングではなくクエリ側で決めてください。@EntityGraph を含むSpring Bootのアノテーションの役割分担はSpring Bootの主要なアノテーション一覧|DI・MVC・JPA・バリデーション・AOPを役割別に整理にまとめています。
DTOプロジェクションによる遅延ロードの排除
本命はこれです。エンティティを返さずDTOへ直接射影すれば、遅延ロードという概念自体が消えます。
@Query("select new com.example.dto.ParentSummary(p.id, p.name, count(c)) "
+ "from Parent p left join p.children c group by p.id, p.name")
List<ParentSummary> findSummaries();
Vlad Mihalcea氏も「DTOプロジェクションはアプリケーション開発者に必要なデータセットだけを取得させ、LazyInitializationException の影響を受けない」として、OSIVの代替ではなくOSIVが存在しなくてよい状態への移行手段として推奨しています。型安全にクエリを組み立てたい場合はQueryDSLとは?Spring Bootでの導入手順と5.1.0・OpenFeign版7.5の選び方の導入手順が使えます。
3つのうちどれを主軸にするかで迷うなら、参照系はDTOプロジェクション、更新のためにエンティティが要る場面だけ @EntityGraph か join fetch、と決め打ちするのが実務では扱いやすい配分です。境界をどこに引くかは、サービスクラスとは?Javaでの役割・書き方とコントローラーとの違いで扱っているサービス層の責務の切り方と同じ問題になります。
既定値trueの今後:Spring Boot 4.0・4.1でも据え置きという結論
「いずれ既定値がfalseになるなら、それまで待つ」という判断を検討しているなら、直近の結論を知っておいてください。2025年10月11日、issue #47547「Reconsider disabling ‘spring.jpa.open-in-view’ by default for Spring Boot 4.0」が起票されました。9年前の#7107から状況が変わっていないこと、コネクションプールの枯渇とセッション寿命の延長によるメモリリークを繰り返し踏んできたこと、プロパティ名がWeb層限定であることを表さないため同じコードが呼び出し元によって挙動を変えることが理由です。
クローズは2025年10月15日、ステータスは not_planned、ラベルは「status: declined」。Phil Webb氏のコメントはこうです。「本日議論した。spring.jpa.open-in-view の既定設定はいずれ見直したいが、4.0.0-RC1がこれほど近い時期に変更を加える準備はできていない。すべてを考慮して、現状のままにする」。
見直す意思はチームにある、しかし4.0でも4.1でも据え置かれた。待つ理由はありません。将来この既定が反転したとき挙動の変化を被るのは、値を明示していないアプリだけです。Spring Boot 4系への移行を検討中なら、Spring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説【2026年最新】で他の破壊的変更と併せて確認しておくと一度で片付きます。
よくある質問
spring.jpa.open-in-view の既定値は何ですか?
trueです。プロパティを書かなければ有効になります。根拠は JpaBaseConfiguration.JpaWebConfiguration に付いた @ConditionalOnBooleanProperty(name = "spring.jpa.open-in-view", matchIfMissing = true) で、プロパティが存在しない場合も条件が成立して OpenEntityManagerInViewInterceptor が登録されます。Spring Boot 4.1系と3.5系の現行ソースで同一の記述でした。
「spring.jpa.open-in-view is enabled by default…」の警告はどう消しますか?
application.properties に spring.jpa.open-in-view の値を明示すれば消えます。この警告は値がtrueかどうかではなく、値が未設定(null)かどうかで発火するためです。ただしtrueを明示した場合、消えるのは警告だけでOSIVは有効なまま。ログを静かにする目的だけでtrueを書くと、判断を先送りしたことが記録に残らなくなります。falseにするかtrueで確定するかを決めてから書いてください。
OSIVとOpen Session In Viewは同じものですか?
同じです。OSIVはOpen Session In Viewの略で、JPAを使う文脈ではOpen EntityManager In View(OEMIV)とも呼ばれます。Spring Bootが登録するクラス名も OpenEntityManagerInViewInterceptor です。ここでのSessionはHibernateの Session(JPAの EntityManager)を指し、ログイン状態を保持するHTTPセッションとは関係ありません。
spring.jpa.open-in-view=false にすれば必ず速くなりますか?
なりません。falseにすると、遅延ロードで散発的に発行されていたSQLがなくなる代わりに、必要なデータをトランザクション内で取り切る記述が要ります。取得プランを書き換えないままfalseにした場合、速くなるのではなく LazyInitializationException で落ちるだけです。効果を判断するなら、切り替え前後で hikaricp.connections.usage(接続の利用時間)と hikaricp.connections.pending(接続待ちスレッド数)を比較してください。
WebFluxやバッチ処理でも spring.jpa.open-in-view は効きますか?
効きません。この自動構成には @ConditionalOnWebApplication(type = Type.SERVLET) が付いており、Servletスタックのアプリでしか成立しないためです。@Scheduled のバッチ、メッセージリスナー、テストコードからサービスメソッドを呼ぶ経路もOSIVの外側にあります。Webからは動くコードがスケジューラからは落ちる、という現象の原因はこれです。