JPAの仕様を洗っていく
IT技術
前提
話がややこしくなるので事前に説明しておくと、Javaを介したDB操作をする上でJPA とSpring 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は実行されていました。(詳細については後述)
1try {
2 userRepository.save(userEntity);
3} catch (ObjectOptimisticLockingFailureException e) {
4 // 例外をスロー
5}
そのため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の状態が同期される。 |
detach | PersistenceContextで管理しているエンティティを削除し管理対象から外します。removeと似ていますがライフサイクル的には削除状態にはならず "分離状態" として扱われます。※分離状態のエンティティに対する変更はDBに反映されません(管理対象外のため) |
contain | 引数で指定したエンティティがPersistenceContext内に存在するかチェックする。 |
refresh | 管理状態のエンティティにDBの情報を反映させる。 ※管理状態でないエンティティを指定した場合例外が発生します。 |
clear | PersistenceContextをクリアし、全ての管理状態のエンティティを分離状態にする。 |
例としてUserテーブルにレコードをINSERTする場合のコードを用意しました。
1// PersistenceContextとEntityManagerを紐づけ
2@PersistenceContext(unitName="ExampleUnit")
3private EntityManager em;
4
5public void insertUser() {
6 // UserEntity取得
7 var userEntity = new UserEntity();
8
9 // 名前を変更
10 userEntity.setName("佐藤");
11
12 // PersistenceContextでの管理対象に指定
13 this.em.persist(userEntity);
14
15 // 変更内容をコミット ※JPAはコミット時にEntityへの変更をDBに反映する
16 this.em.getTransaction().commit();
17}
※今回あえて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 JPA | JPA | 処理内容 |
save | persist merge | PersistenceContextに対象エンティティが存在すれば更新(UPDATE)、存在しなければ新たに管理状態として永続化(INSERT)する。 |
delete | merge remove contains | 引数で指定したエンティティがPersistenceContext内に存在するか確認し削除する。 |
saveAndFlush | persist merge flush | save実行後flushを実行。DBにエンティティの情報をすぐ同期したい場合に使用する。 |
ここではあくまで使用頻度が高いメソッドの対応を示しましたが、他にもSpring Data JPA はJPA のメソッドを統合し、ボイラープレートコードの削減や可読性の向上をしてくれます。
ここまででDB操作の流れは掴めたと思うので、次は筆者が実装していたときにハマった箇所について解説していきます。
実装する上で勘違いしていた点
DBのUPDATE時に楽観ロックエラーが発生した場合は例外をスロー、更新が成功した場合は更に後続の処理行うといった実装を進めていました。イメージとしては以下のようなコードになります。
1public void updateUser() {
2
3 // 処理1. 更新対象のレコードが存在するか確認
4 var userEntity = userRepository.findById(1).orElseThrow(// 存在しない場合例外をスロー);
5 // 処理2. 更新
6 try {
7 userRepository.save(userEntity);
8 } catch (ObjectOptimisticLockingFailureException e) {
9 // 楽観ロック発生時に例外をスロー
10 }
11 // 処理3. 更新が成功した旨を出力
12 log.info("更新成功");
13
14 // 処理4. 後続の処理が続く
15}
しかし、このコードでは楽観ロックエラーが発生したとしてもExceptionをcatchしてスローしてくれるのは、処理4がすべて完了しupdateUser メソッドが終了する直前になります。つまり更新に失敗しているにも関わらず、更新成功のログ出力がされてしまい後続の処理までも実行されてしまいます。
これを解決するにはJPAのメソッド一覧に記載したflush メソッドをsaveメソッドの後に実行する必要があります。そうすることでsaveメソッドでUserEntityに行った変更をDBに反映することができます。Spring Data JPAでこれを行うにはsaveAndFlush メソッドがあります。そのため処理2のsave メソッドを使用していた箇所をsaveAndFlush に修正するだけで、Exceptionを処理3 実行前にcatchすることができるようになります。
最後に
今回はJPAの仕様について触れていきました。SQLが自動で生成されてくれるのはありがたいですが、その分裏の動きが読めないのは怖いところですね。実際にどのようなSQLがどのタイミングで実行されているかという意識が常に必要なことやDBにデータを反映するまでの流れを事前に把握していないといけないことから学習コストは高いと思います。使用を検討する場合は事前学習がそれなりに必要なことを留意しておきたいです。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
2022年6月入社。雑食系エンジニア。
おすすめ記事
immichを知ってほしい
2024.10.31