#author("2021-04-28T02:40:18+00:00","","") #author("2021-06-24T04:34:04+00:00","","") [[SpringBoot]] * Spring Data JPA を使ったデータ永続化 [#i96daab5] 2021-04-05 中田 JPAの説明とJpaRepository等の説明を加筆 2020-05-19 中村 ** Spring Data JPAとは [#t4205079] Spring Bootにおいて,データの永続化は@Repositoryが担当する. これをJDBC(Java Database Connectivity)を使ってゴリゴリ自前で 実装してもよいが,多くの場合 [[Boilerplate Code:https://en.wikipedia.org/wiki/Boilerplate_code]]が出来上がるだけである. [[JPA (Java Persistence API):https://ja.wikipedia.org/wiki/Java_Persistence_API]]は, Javaでデータ永続化を効率的に行うAPIをまとめた規格であり,これをSpring の@Repositoryの実装に使えるようにしたものが [[Spring Data JPA:https://spring.io/projects/spring-data-jpa]] である. Spring Data JPA を使えば,DBに対する''基本的なCRUD操作が全自動で実装'' されるので,@Repositoryの実装が爆速(多くの場合コードレス) になる.また,DBのテーブルをEntity Beansに合わせて自動生成 する機能や,値の検査(バリデーション)を行う機能もあるため, DB操作に付随する手間も大きく削減できる. これを使わない手はない! *** JDBC、ORM、JPAとはそもそも何か [#gb137a6c] - JDBC(Java Database Connectiveity) -- 様々なデータベースへのアクセスを抽象化することで、異なるデータベースに同じ手段でアクセスできるようにするODBC(Open Databace Connectivety)のJava版 -- 各データベースのベンダがJDBCのドライバを提供しているため、JDBCで様々なデータベースが利用できる - ORM(Object-relational mapping) -- RDBのレコードとOOPにおけるオブジェクトを対応付ける手法 -- つまり、DB上の要素をJavaクラスに紐づけて使えるようにする(逆も然り) -- JavaではHibernateがデファクトスタンダード - JPA(Java Persistence API) -- 各種ORMをJavaでまとめて扱うためのAPI -- データ読み書きのためにDAO(Data Access Object)を毎回書く手間が地獄だった --- そこでSpring Data JPAの出番! ** 例題 [#x5ab4ba8] 健康診断システム([[ソフトウェア工学第4回課題:http://www27.cs.kobe-u.ac.jp/~masa-n/lecture/newse/04/enshuu04.html]])における受診者Personを考える. - newse / newse+- @Data public class Person { private Long number; // 受診番号(ID) private String name; // 受診者名前 private Date birthday; // 誕生日 private double height; // 身長 cm private double weight; // 体重 kg } これをEntityとしてSpring Data JPAで永続化することを考える.DBはMySQLを使う. ** 準備 [#d125e8f7] + ''build.gradle'' を編集する.dependenciesの中に以下の依存関係を追加する: //Spring JPA implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //MySQL runtimeOnly 'mysql:mysql-connector-java' //バリデーション用 compile 'org.springframework.boot:spring-boot-starter-validation' -- Ctrl+Shift+P -> Java:Update project configurationで依存関係をupdateすることを忘れない + MySQLに新規DB(例:medicalcheck)を作成する. -- やり方は[[データベース/MySQL]]あたりを参照のこと.テーブルは作成しなくてよい -- ユーザ名/パスワードはそのDB用のものを作る.例:newse/newse -- シェルでやる方法 $ mysql -u root -p パスワード入力 mysql> CREATE DATABASE medicalcheck; mysql> CREATE USER newse IDENTIFIED BY 'newse'; mysql> GRANT ALL ON medicalcheck.* TO newse; + ''application.properties''を編集.DBへの接続情報とJPAのDDL設定を追記. # MySQLデータベース接続設定 spring.datasource.url=jdbc:mysql://localhost:3306/medicalcheck?serverTimezone=JST spring.datasource.username=newse spring.datasource.password=newse # Spring-JPA: DBのテーブルを自動作成してくれる機能 # create: 新規作成, update: なければ新規作成, create-drop: 新規作成し終了時に削除 spring.jpa.hibernate.ddl-auto=update ** PersonをJPAのEntityとして設定する [#t2cbddae] Personクラスを下記のようにアノテーションする. @Data @Entity public class Person { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long number; // 受診番号(ID) private String name; // 受診者名前 @Temporal(TemporalType.DATE) private Date birthday; // 誕生日 private double height; // 身長 cm private double weight; // 体重 kg } - 適宜 import javax.persistence.クラス名; を追記すること (エディタのQuick Fixで可能) - @Entity はこのクラスをEntity Beanにするという宣言 - @Idはこのメンバを主キーにするという意味 -- 主キーを整数型にするときは,intよりLongを使う慣例がある - @GeneratedValueはこのメンバの値を陽に指定しない場合は自動生成するという意味 -- 生成の戦略をStrategyに指定.IDENTITYは「識別子として重複しないように生成せよ」という意味 -- MySQLの場合は自動増分採番(Auto Increment, AI)として設定される - @Temporalは日時型のオブジェクトの型を指定. -- DATEだと日付の部分のみ.他にもTIME, TIMESTAMPなどを指定可能. [[参考:https://www.codeflow.site/ja/article/spring-data-jpa-query-by-date]] ** PersonRepository の実装 [#s09c5603] JPAが用意している [[''CrudRepository'':https://docs.spring.io/spring-data/commons/docs/2.4.5/api/org/springframework/data/repository/CrudRepository.html]]を継承する形でPersonRepositoryを作成する. [[公式Doc(Spring Data 2.4.5):https://docs.spring.io/spring-data/commons/docs/2.4.5/api/org/springframework/data/repository/CrudRepository.html]] ''PersonRepository.java'' @Repository public interface PersonRepository extends CrudRepository<Person, Long> { } - クラスではなく''インタフェース(interface)''であることに注意. - JPAのCrudRepositoryを継承する.その際ジェネリクス<T,ID>を指定する必要がある. -- Tは管理対象のEntityクラス.ここではPerson -- IDはEntityクラスの主キーの型.ここではLong - 基本的なCRUD操作はこれで実装完了! 実際のクラスは自動生成される. ** PersonRepositry (CrudRepository) の基本メソッド [#db6421b2] デフォルトで以下のクラスが自動生成される. &attachref(class.png); 自動生成されるメソッドの説明は以下の通り. *** 書き込み系(CREATE, UPDATE) [#g2860fc7] - save()はpersonオブジェクトを引数にとり,それをDBに永続化する. -- CREATE: 引数のpersonオブジェクトで「numberをセットせず」に呼び出すと新規作成される --- 裏ではMySQLのINSERT文が実行されPersonテーブルに1行挿入される --- numberは自動採番される -- UPDATE: 引数のpersonオブジェクトで「numberをセットして」呼び出すと更新 --- 裏ではMySQLのUPDATE文が実行されPersonテーブルの当該行が上書きされる -- テーブル自動作成: Personテーブルが存在しない場合には,自動的に作成される --- spring...ddl-autoの設定による -- 返り値: DBに実際に挿入されたオブジェクトのリファレンスが返る --- 引数に渡したオブジェクトとは''異なる''ことに注意!! これは結構盲点. - saveAll()は複数のPersonオブジェクトをバルクインサートする *** 読み出し系(READ) [#c53c6a24] - findById()は主キーであるnumberを引数として,1つのPersonオブジェクトを取得する -- 返り値はOptional<Person>型となり,当該オブジェクトが[[存在しない場合にも備えられるようになっている:https://engineer-club.jp/java-optional]]. -- 呼び出し側(@Service)では以下のように使えばよい Person p = persons.findById(number).orElseThrow( ()-> new NoSuchPersonException(number + ": No such person exists Person ") ); --- 存在すればpに代入され,存在しない場合は例外が投げられる - findAll()は全件取得.返り値はIterable<Person>なので,拡張for文などで数え上げ可能 *** 削除系 (DELETE) [#j1781944] - delete() 1件削除 - deleteById() 主キーを指定して1件削除 - deleteAll() バルクで一括削除 *** その他 [#j83e431b] - count() 件数カウント - existsById() 主キーで存在検査 *** 注意点 [#i6160e52] - 上記のメソッドはPersonのDBへの基本的なCRUD操作を規定しているに過ぎない - 「新規作成時にIDが重複してたらどうする」とか,「読み出し時にIDがちゃんと指定されているか」 「更新時のデータ値がおかしかったらどうしよう」などの心配は''呼び出し側で考える'' - これらがいわゆる「ビジネスルール」であり,@Serviceクラスで処理する事柄なのである. ** カスタムクエリの作成 [#h256c345] *** カスタムクエリの例 [#f01d2288] PersonRepositoryで「もう少し凝った」データの問い合わせ(クエリ)を行いたいという要求を考えよう. 例えば以下のような感じ. + 名前で受診者を検索したい + 受診者のうち身長が160cm~170cmの人だけを取り出したい + 受診者のうち誕生日が1月の人だけを取り出したい + 受診者のうちBMIが25未満の人の体重を知りたい + 受診者のうち肥満度2の人を見つけたい これらの要求に対し,とりあえずfindAll()でPersonを全件取り出して,サービス層でデータ分析処理を 行うというやり方も可能だが,めんどくさいしテストも大変である. せっかくDBを使っているのだから,こうした操作はDB側でSQLでやってもらいたいわけである. このような場合,PersonRepositoryに''カスタム・メソッド''を追加することで解決できる. ここで紹介するやりかたは以下の2種類. *** 命名規則に従ったメソッド名による自動実装 [#ybc5bdf7] 上記のPersonRepositoryインタフェースに メソッド・シグニチャ(関数の宣言部)を追加するだけで, JPAが勝手に中身を実装してくれるというもの. 例えばこんな感じ. ''PersonRepository.java'' @Repository public interface PersonRepository extends CrudRepository<Person, Long> { /** 名前が LIKE name な受診者を返す */ public Iterable<Person> findPersonByNameLike(String name); /** 身長がmin~maxの間にある受診者を返す */ public Iterable<Person> findPersonByHeightBetween(double min, double max); } ヤヴァイぐらいシンプルである.どういうカラクリなのか? 実は [[ある命名規則:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation]] に従ってメソッド・シグニチャをつければ,Spring Data JPAが自動実装してくれるのである. 大まかにいうと, - find...By(カラム名)(検索方法) のような感じである. 大体のカスタムクエリはこれで用が足りてしまう. より具体的な活用は,[[この記事:https://qiita.com/shindo_ryo/items/af7d12be264c2cc4b252]]などを参照すること. *** @QueryにSQLを書いて実装 [#xf89c914] 命名規則のやり方で出来ないような少し複雑なクエリは,@Queryアノテーションをつけて直接SQLを書くやり方がある. PersonRepositoryに以下を追加してみる. /** 生まれ月で検索 */ @Query(value = "SELECT * FROM person p where MONTH(p.birthday) = ?1", nativeQuery = true) public Iterable<Person> findPersonByBirthMonth(int month); - @Queryは中にクエリ言語を直接書くという意味. - デフォルトでは[[JPQL:https://www.ne.jp/asahi/hishidama/home/tech/java/spring/boot/jpa/jpql.html]]という SQLに似ているが異なる言語を受け付ける - SQLで書きたい場合には,nativeQuery = trueをつける -- &color("red"){テーブル名(ここではperson)は, 環境によって大文字・小文字を区別する}; ことがある -- DBに作られたテーブル名を確認して,大文字・小文字を合わせたほうが無難(PersonにしてLinuxのMariaDBでハマった) --- o.h.engine.jdbc.spi.SqlExceptionHelper : Table 'medicalcheck.Person' doesn't exist --- まさにこれ→ http://osprey-jp.hatenablog.com/entry/2017/06/02/092419 - ?1 はプレースホルダ.メソッドのパラメータの1番目の値がここに代入されるという意味. 同様に ?2 ?3 ... とすれば引数を2個,3個と取るメソッドを作ることができる - pはPersonテーブルのエイリアスとしてはたらく ** Spring Data JPAのリポジトリインタフェースの違いについて [#jb04ad7e] - CrudRepository -- CRUD関数を提供する - PagingAndSortingRepository -- ページ付け、ソートのメソッドを提供する - JpaRepository -- 各種JPAメソッドを提供 JpaRepository -> PagingAndSortingRepository -> CrudRepository -> Repository の順で継承しているため、利用したい最小機能のRepositoryインタフェースを利用する。 ** すべてのソースコード [#o7bc60ef] - GitLabにあげておいたので参考にしてください. -- http://teamserv.cs.kobe-u.ac.jp:443/masa-n/medicalcheckapplication * 関連を持つエンティティの永続化 [#ed728f9c] - 複数のテーブルが関連を持つ場合など,より複雑なEntityの永続化については,また今度違うページに書きたい - 興味のある人は,例えば下記を参照されたい -- https://qiita.com/rubytomato@github/items/6781e91bcf7ab8a68c40 * 参考文献 [#o435a70c] - Spring Data JPA Tutorial: Adding Custom Methods to a Single Repository -- https://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-part-eight-adding-functionality-to-a-repository/ - Spring Data JPA Tutorial Part Three: Custom Queries with Query Methods -- https://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-three-custom-queries-with-query-methods/ - Spring BootとJPAでREST APIを実装する(アプリケーション層編) -- https://qiita.com/YutaKase6/items/487edfa9f6e39ac2f939 - Spring Boot + MySQLでシンプルなWeb REST APIサーバを実装する -- https://qiita.com/YutaKase6/items/007f6b7d12d342c1330c - SpringBoot + Spring JPAでデータベースに接続する -- https://qiita.com/t-iguchi/items/685c0a1bb9b0e8ec68d2 - Java Persistence API 2.1のおさらいメモ -- https://qiita.com/rubytomato@github/items/6781e91bcf7ab8a68c40 - JpaRepository と CrudRepository の違いって何? https://qiita.com/nokonoko/items/0c3d4eaf19fe79fe99bb -- - What is the difference between CrudRepository and JpaRepository interfaces in Spring Data JPA? -- https://stackoverflow.com/questions/14014086/what-is-difference-between-crudrepository-and-jparepository-interfaces-in-spring -- &ref(https://i.stack.imgur.com/SBx4b.png,50%); - [Java][Spring Boot][JPA] データベースを使う - NetBeansで始めるSpring Boot (6) -- https://qiita.com/sengoku/items/2424c59f3b4ac94a1cb0 - 【Spring Data JPA】自動実装されるメソッドの命名ルール -- https://qiita.com/shindo_ryo/items/af7d12be264c2cc4b252 - Spring Data JPA入門(JPA、Hibernate、JDBCあたりがまとめられている) -- https://dawaan.com/spring-data-jpa-basics/ - JDBC Tutorial -- https://www.javatpoint.com/java-jdbc - ORMとは(Qiita) -- https://qiita.com/yk-nakamura/items/acd071f16cda844579b9 - JPA Tutorial -- https://www.javatpoint.com/jpa-tutorial - CrudRepositoryとJpaRepositoryの違い -- https://qastack.jp/programming/14014086/what-is-difference-between-crudrepository-and-jparepository-interfaces-in-spring - 公式 -- Spring Data --- https://spring.io/projects/spring-data -- Accessing data with JPA --- https://spring.io/guides/gs/accessing-data-jpa/ -- Accessing JPA Data with REST --- https://spring.io/guides/gs/accessing-data-rest/ * トラブルシューティング [#v8eb3020] - アノテーションやクラスがみつからない -- 原因: プロジェクトの依存関係がうまく解決されていない -- 対策: VSCodeにて依存関係が解決されない場合には,.vscode/settings.jsonの中身が以下のようになっているか確認. なっていなければそのようにする. { "java.configuration.updateBuildConfiguration": "automatic" }