1. HOME
  2. ブログ
  3. IT技術
  4. JPAの仕様を洗っていく

JPAの仕様を洗っていく

前提

話がややこしくなるので事前に説明しておくと、Javaを介したDB操作をする上で JPASpring Data JPA という似ている2つのワードが出てきます。簡単に違いを説明すると JPA はJavaオブジェクトとDBカラムのマッピングを仕様として定義したものです。その仕様(JPA)に則ったライブラリを提供しているのが Spring Data JPA になります。JPAそのままでもDBとやり取りすることは可能ですが、仕組みがかなりややこしく学習コストが高いです。そのため簡易的にJPAを使用できるようにしてくれるSpring Data JPAが用意されています。今回の記事ではJPAをメインに解説を進めつつ、Spring Data JPAにも触れていきたいと思います。

仕様確認に至った経緯

ここ数ヶ月で Spring Data JPA に触れる機会が増えたにも関わらず、動作の詳細について把握していなかったことが原因で実装が手間取ったことがありました。

具体的には下記のようにDBを更新する場面で、saveメソッドの実行時に裏でUPDATEのSQLが走る想定をしていたのですが、実際には別のタイミングでSQLは実行されていました。(詳細については後述)

そのため Spring Data JPA ないしはその大元にある JPA について根っこから概要を押さえる必要があると感じ、そのアウトプットも兼ねて本記事の投稿に至りました。

JPA

まずJPAの重要な要素として Entity  と Entity Manager があります。

EntityはDBのテーブルと1対1で対応するJavaオブジェクトです。コード上では @Entity がついているクラスが該当します。

Entity ManagerはEntityのライフサイクル管理を PersistenceContext という領域で行います。その際に使用することができるEntity Managerのメソッドとして主に以下のものがあります。

persist引数にエンティティを渡すとそのエンティティをPersistenceContextに格納し、管理状態として永続化する。(INSERT)

find

管理状態のエンティティの中から、引数で渡したIDに紐づくエンティティを返却する。(SELECT)
merge引数で指定したエンティティの状態をPersistenceContext内にあるエンティティに反映する。(UPDATE)
remove​引数で指定したエンティティの状態を削除状態にする。(DELETE)
flush上記メソッドで行ったエンティティへの変更をDBに反映する。このメソッドを実行して初めてPersistenceContext内のエンティティとDBの状態が同期される。
detachPersistenceContextで管理しているエンティティを削除し管理対象から外します。removeと似ていますがライフサイクル的には削除状態にはならず "分離状態" として扱われます。※分離状態のエンティティに対する変更はDBに反映されません(管理対象外のため)
contain引数で指定したエンティティがPersistenceContext内に存在するかチェックする。
refresh管理状態のエンティティにDBの情報を反映させる。 ※管理状態でないエンティティを指定した場合例外が発生します。
clearPersistenceContextをクリアし、全ての管理状態のエンティティを分離状態にする。

例としてUserテーブルにレコードをINSERTする場合のコードを用意しました。

※今回あえてcommitメソッドを書いていますがJPAでは自動コミット機能があり、insertUser()の処理が全て終了した時点で勝手にコミットしてくれるため本来は記載不要です。

例として今回はINSERTのコードを記載しましたが、UPDATEしたい場合はnewの箇所をfind()によるエンティティ取得に置き換えるだけで実現できます。

JPAでは上記のように様々なメソッドを使用しエンティティのライフサイクル管理を行いDBを操作します。しかし、単純な参照や更新ならともかく複雑なDB操作を行いたい場合にややこしくなります。そのためJPAをそのまま使用するのではなく、簡易的に使用することができる Spring Data JPA の使用を検討する方が多いと思います。(検討する方が多いのはあくまでJPAを使う前提がある人の話です。そもそもJPAの使用自体避けられている印象、、、)

Spring Data JPA

JPAはあくまで仕様であり、実装はJPAプロパイダが行っています。JPAプロパイダにも様々な種類がありますが、Spring Data JPAのデフォルト実装は Hibernate なります。そのため本記事ではHibernateを使用していることを前提に説明を進めていきます。

Spring Data JPAはJPAを簡易的に使用できるようにしてくれると説明しましたが、実際にはどういうことかというとSpring Data JPAはJPAの処理をラップし抽象化してくれます。JPAの説明でメソッドの表があったと思いますが、あそこで記載されている処理を更に1つ1つのメソッドにまとめることでコードの記述量を少なくしてくれます。以下に対応表を用意しました。

Spring Data JPAJPA処理内容
savepersist
merge
PersistenceContextに対象エンティティが存在すれば更新(UPDATE)、存在しなければ新たに管理状態として永続化(INSERT)する。
deletemerge
remove
contains
引数で指定したエンティティがPersistenceContext内に存在するか確認し削除する。
saveAndFlushpersist
merge
flush
save実行後flushを実行。DBにエンティティの情報をすぐ同期したい場合に使用する。

ここではあくまで使用頻度が高いメソッドの対応を示しましたが、他にも Spring Data JPA は JPA のメソッドを統合し、ボイラープレートコードの削減や可読性の向上をしてくれます。

ここまででDB操作の流れは掴めたと思うので、次は筆者が実装していたときにハマった箇所について解説していきます。

実装する上で勘違いしていた点

DBのUPDATE時に楽観ロックエラーが発生した場合は例外をスロー、更新が成功した場合は更に後続の処理行うといった実装を進めていました。イメージとしては以下のようなコードになります。

しかし、このコードでは楽観ロックエラーが発生したとしてもExceptionをcatchしてスローしてくれるのは、処理4がすべて完了し updateUser メソッドが終了する直前になります。つまり更新に失敗しているにも関わらず、更新成功のログ出力がされてしまい後続の処理までも実行されてしまいます。

これを解決するにはJPAのメソッド一覧に記載した flush メソッドをsaveメソッドの後に実行する必要があります。そうすることでsaveメソッドでUserEntityに行った変更をDBに反映することができます。Spring Data JPAでこれを行うには saveAndFlush メソッドがあります。そのため処理2の save メソッドを使用していた箇所を saveAndFlush に修正するだけで、Exceptionを 処理3 実行前にcatchすることができるようになります。

最後に

今回はJPAの仕様について触れていきました。SQLが自動で生成されてくれるのはありがたいですが、その分裏の動きが読めないのは怖いところですね。実際にどのようなSQLがどのタイミングで実行されているかという意識が常に必要なことやDBにデータを反映するまでの流れを事前に把握していないといけないことから学習コストは高いと思います。使用を検討する場合は事前学習がそれなりに必要なことを留意しておきたいです。

書いた人はこんな人

かじー(エンジニア)
かじー(エンジニア)
フルスタックエンジニアとして活躍することを目指す新米エンジニア。
現在はバックエンドを担当しており、日々奮闘中。
プログラムを組んで課題を解決することが好きなため
休日も開発に勤しむことも、、、

関連記事

採用情報

\ あの有名サービスに参画!? /

バックエンドエンジニア

\ クリエイティブの最前線 /

フロントエンドエンジニア

\ 世界を変える…! /

Androidエンジニア

\ みんなが使うアプリを創る /

iOSエンジニア