背景・目的
過去に、Spring Bootについて下記の記事にまとめました。今回は、EntityManagerによるデータベースアクセスを試します。
- Spring Bootでバリデーションを試してみた
- Spring Bootでデータを保存する-その2
- Spring Bootでデータを保存する-その1
- Spring Bootでデータベースへのアクセスを試してみた
- Sprint Bootの環境を構築したときのメモ
まとめ
下記に特徴をまとめます
特徴 | 説明 |
---|---|
DAO | Database Access Objectの略 永続化のメカニズムを提供する抽象化インターフェイス この分離により、単一責任の原則をサポートする |
JPQL | Jakarta Persistence Query Languageの略 リレーショナルデータベースに格納されたエンティティに対してクエリを実行するために使用される |
クエリアノテーション | メンテナンスの手間が軽減 |
NamedQueryアノテーション | クエリアノテーションの一種 エンティティで実装 |
Queryアノテーション | クエリアノテーションの一種 DAOやリポジトリで実装 |
概要
Spring Bootでは、JPAの一部である、リポジトリをつかって簡単なデータアクセスができます。
今回は、リポジトリ以外のJPAの機能について、整理します。
DAO
Data access objectを基に整理します。
- Database Access Object(以降、DAOという)は、データベースや、その他の永続化のメカニズムを提供する抽象化インフェイス
- アプリケーションの呼び出しを永続化レイヤーとのマッピングにより、詳細なデータ操作を隠蔽する
- この分離により、単一責任の原則をサポートする
- これにより、ドメイン固有のオブジェクトとデータ型 (DAO のパブリック インターフェイス) の観点からアプリケーションが必要とするデータ アクセスが、特定のDBMSでこれらのニーズを満たす方法(DAO の実装) から分離される
JPQL
Jakarta Persistence Query Languageを基に整理します。
- Jakarta Persistence Query Languageの略
- 以前は、Java Persistence Query Languageのようです
- リレーショナルデータベースに格納されたエンティティに対してクエリを実行するために使用される
- そのクエリは構文がSQLクエリに似ている
- データベーステーブルを直接操作するのではなく、JPAエンティティオブジェクトに対して操作する
- JPQLをJPAが受け付けて、SQLに変換し、データベースにクエリを実行する
クエリアノテーション
Annotation-based Configurationを基に整理します。
- クエリをリテラルとしてDAOクラス内に文字列リテラルとして埋め込まれているのは、メンテナンスの観点から好ましくない
- アノテーションベースの構成には、別の構成ファイルを編集する必要がないため、メンテナンスの手間が軽減されるという利点がある
NamedQueryアノテーション
- クエリアノテーションには、クエリに名前をつけて利用できる「名前付きクエリ」を作成できる
Queryアノテーション
- NamedQueryでは、エンティティクラスにあらかじめ定義しておくが、エンティティに寄せると、メンテナンス性が悪い
実践
前提
Spring Bootでバリデーションを試してみたで使用していた環境を再利用します。
DAOを用意する
インタフェイスの実装
- コントローラーと同一パッケージにPersonDAOを用意します
- 下記のコードを追加します
package com.example.data.access; import java.io.Serializable; // Add this import statement import java.util.List; public interface PersonDAO<T> extends Serializable { public List<T> getAll(); }
DAOの実装
- DAOインタフェイスと同一パッケージにPersonDAOPersonImplを用意します
- 下記のコードを追記します
package com.example.data.access; import java.util.List; // Import the correct List class import org.springframework.stereotype.Repository; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.Query; // Import the Query class @Repository public class PersonDAOPersonImpl implements PersonDAO<Person> { private static final long serialVersionUID = 1L; @PersistenceContext private EntityManager entityManager; public PersonDAOPersonImpl() { super(); } @Override public List<Person> getAll() { Query query = entityManager.createQuery("FROM Person p"); @SuppressWarnings("unchecked") List<Person> list = query.getResultList(); // Remove the unnecessary cast entityManager.close(); return list; } }
- EntityManagerクラス
- エンティティを操作するための機能を有する
- エンティティを扱う場合は、用意する
-
@PersistenceContext
- EntityManagerにつけられたアノテーション
- EntityManagerのBeanを取得し、フィールドに設定するためにある
- EntityManagerは、Spring Bootアプリケーションを起動時に自動手でBeanとしてインスタンスが登録されている。これを
@PersisutentContext
により、フィールドに割り当てる - このアノテーションを利用するのが、Spring Data JPAの基本
- createQueryメソッド
- Queryオブジェクトは、SQLでデータを問い合わせるためのクエリに相当する
- JPAでは、JPQLと呼ばれるクエリがある。SQLに似た形の問い合わせ言語
- createQueryにより、JPQLによるクエリを指定して呼び出している
- getResultListメソッド
- ListインスタンスでJPQLの結果が返される
- from Personとしているので、ListにはPersonインスタンスがまとめられている
-
@SuppressWarnings
アノテーション- uncheckedを引数に渡すことで、Listで得られているかチェックをおこなうか
- ビルド時に警告を出すかをコントロールする
- EntityManagerクラス
コントローラー
-
Spring Bootでバリデーションを試してみたで使用していたWebControlloerに下記のコードを追加します
@Autowired PersonDAOPersonImpl dao; @RequestMapping(value = "/find", method=RequestMethod.GET) public ModelAndView index(ModelAndView mav) { mav.setViewName("find"); mav.addObject("title","Find Page"); mav.addObject("message","Please enter a name."); List<Person> list = dao.getAll(); mav.addObject("people", list); return mav; }
- DAOのAutowired
- Autowiredは、アプリケーションが用意したインスタンスを自動的に割り当てるアノテーション
- findメソッドの処理
- PersonDAOPersonImplからgetAllでListを取得している
- DAOにアクセスをまとめておくことで、コントローラーでは呼び出すだけで済む
- DAOのAutowired
ビューテンプレート
- templateにfind.htmlを追加します
- 下記のコードを追記します
<!DOCTYPE html> <html> <head> <title>top page</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"> </head> <body class="container"> <h1 class="display-4 mb-4" th:text="${title}"></h1> <p th:text="${message}"></p> <form method="post" action="/find"> <div class="input-group"> <input type="text" class="form-control me-1" name="find_str" th:value="${value}" /> <span class="input-group-btn"> <input type="submit" class="btn btn-primary px-4" value="Click" /> </span> </div> </form> <table class="table"> <thread> <tr> <th>firstName</th> <th>lastName</th> <th>Phone</th> </tr> </thread> <tbody> <tr th:each="person : ${people}"> <td th:text="${person.firstName}"></td> <td th:text="${person.lastName}"></td> <td th:text="${person.phone}"></td> </tr> </tbody> </table> </body> </html>
動作確認
- ビルドしSpring Bootを再実行します
$ mvn package $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
- localhost:8080/findにアクセスします。見えました
現時点では、Click後のアクションを定義していないので、ここまでとします。
ID検索できるようにする
DAOインタフェイス
- PersonDAOに、下記のメソッドを追加します
- findById
- ID番号を引数に指定してエンティティを検索し返す
- findByName
- 名前から検索する
package com.example.data.rest_db; import java.io.Serializable; // Add this import statement import java.util.List; public interface PersonDAO<T> extends Serializable { public List<T> getAll(); public T findById(int id); public List<T> findByFirstName(String name); }
- findById
PersonDAOPersonImplクラス
- PersonDAOPersonImplに、下記のメソッドを実装します
@Override public Person findById(long id) { return (Person)entityManager.createQuery("FROM Person p where p.id = :id") .setParameter("id", id) .getSingleResult(); } @SuppressWarnings("unchecked") @Override public List<Person> findByFirstName(String name) { return (List<Person>)entityManager.createQuery("FROM Person p where p.firstName = :name") .setParameter("name", name) .getResultList(); }
- findById
- JPQLのクエリ
- getSingleResultは、Queryから得られるエンティティを1つだけ取り出す
- findByName
- getResultListは、Listを返す
- findById
コントローラー
- コントローラーに下記のメソッドを追加します
@RequestMapping(value = "/find", method=RequestMethod.POST) public ModelAndView search(HttpServletRequest request, ModelAndView mav){ mav.setViewName("find"); String param = request.getParameter("find_str"); if (param == ""){ mav = new ModelAndView("redirect:/find"); } else { mav.addObject("title","Find result"); mav.addObject("message","Result for " + param); mav.addObject("value", param); Person person = dao.findById(Long.parseLong(param)); Person[] list = new Person[] {person}; mav.addObject("people", list); } return mav; // Add this return statement }
- HttpServletRequest
- JSPやServlertで利用されていたもの
- doGet/doPostで利用していた
- HttpServletRequest
動作確認
-
ビルドしSpring Bootを再実行します
$ mvn package $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
名前で検索できるようにする
コントローラー
- コントローラーのメソッドを修正します
@RequestMapping(value = "/find", method=RequestMethod.POST) public ModelAndView search(HttpServletRequest request, ModelAndView mav){ mav.setViewName("find"); String param = request.getParameter("find_str"); if (param == ""){ mav = new ModelAndView("redirect:/find"); } else { mav.addObject("title","Find result"); mav.addObject("message","Result for " + param); mav.addObject("value", param); // Person person = dao.findById(Long.parseLong(param)); // Person[] list = new Person[] {person}; List<Person> list = dao.findByFirstName(param); mav.addObject("people", list); } return mav; // Add this return statement }
動作確認
-
ビルドしSpring Bootを再実行します
$ mvn package $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
複数のパラメータを追加
firstName、LastName、Phone、IDで検索できるように、いずれかの条件にヒットしたら返されるようにします。
DAOインタフェイス
- 下記のメソッドを追加します
public List<T> find(String fstr);
DAOクラス
- 下記のメソッドを追加します
@SuppressWarnings("unchecked") @Override public List<Person> find(String fstr){ List<Person> list = null; String qstr = "from Person where id =:fid OR firstName like :fname OR lastName like :lname OR phone like :ph"; Long fid = 0L; try { fid = Long.parseLong(fstr); } catch (NumberFormatException e) { e.printStackTrace(); } Query query = entityManager.createQuery(qstr) .setParameter("fid", fid) .setParameter("fname", "%"+fstr+"%") .setParameter("lname", "%"+fstr+"%") .setParameter("ph", "%"+fstr+"%"); list = query.getResultList(); return list; }
コントローラーの修正
- 上記のDAOインタフェイスを利用するように修正します
@RequestMapping(value = "/find", method=RequestMethod.POST) public ModelAndView search(HttpServletRequest request, ModelAndView mav){ mav.setViewName("find"); String param = request.getParameter("find_str"); if (param == ""){ mav = new ModelAndView("redirect:/find"); } else { mav.addObject("title","Find result"); mav.addObject("message","Result for " + param); mav.addObject("value", param); // Person person = dao.findById(Long.parseLong(param)); // Person[] list = new Person[] {person}; // List<Person> list = dao.findByFirstName(param); List<Person> list = dao.find(param); mav.addObject("people", list); } return mav; // Add this return statement }
動作確認
-
ビルドしSpring Bootを再実行します
$ mvn package $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
クエリアノテーション
@NamedQuery
というアノテーションを使えます。
エンティティの修正
- Personクラスに、下記を追加します
- クラス上に
@NamedQuery
アノテーションを追加します
@Entity @Table(name = "people") @NamedQuery( name = "find", query = "from Person where id =:fid OR firstName like :fname OR lastName like :lname OR phone like :ph" ) public class Person {
- クラス上に
- 複数を追加する場合には、下記のように変更します
@Entity @Table(name = "people") @NamedQueries( @NamedQuery( name = "find", query = "from Person where id =:fid OR firstName like :fname OR lastName like :lname OR phone like :ph" ) ) public class Person {
DAOの修正
上記のエンティティに用意した、NamedQueryを利用します。
- DAOのfindメソッドを下記のように修正します
@SuppressWarnings("unchecked") @Override public List<Person> find(String fstr){ List<Person> list = null; Long fid = 0L; try { fid = Long.parseLong(fstr); } catch (NumberFormatException e) { e.printStackTrace(); } Query query = entityManager .createNamedQuery("find") .setParameter("fid", fid) .setParameter("fname", "%"+fstr+"%") .setParameter("lname", "%"+fstr+"%") .setParameter("ph", "%"+fstr+"%"); list = query.getResultList(); return list; }
動作確認
-
ビルドしSpring Bootを再実行します
$ mvn package $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
リポジトリやアノテーションを実装する
リポジトリ
- PersonRepositoryインターフェイスに新たにfindメソッドを追加します
package com.example.data.rest_db.repositories; import com.example.data.rest_db.Person; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; // Add this import statement import java.util.Optional; // Add this import statement @Repository public interface PersonRepository extends JpaRepository<Person, Long> { @Query("SELECT p FROM Person p ORDER BY p.id") List<Person> find(); public Optional<Person> findById (Long id); }
コントローラーの修正
- WebControllerクラスの、indexメソッドを下記のように修正します
@RequestMapping("/") public ModelAndView index( @ModelAttribute("formModel") Person person, ModelAndView mav) { mav.setViewName("index"); mav.addObject("title","Test page"); mav.addObject("message","This is JPA sample content."); // List<Person> list = repository.findAll(); List<Person> list = repository.findAllOrderByName(); mav.addObject("people", list); return mav; }
動作確認
- ビルドしSpring Bootを再実行します
$ mvn package $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
- localhost:8080/findにアクセスします。見えました
リポジトリやアノテーションを実装する(パラメータ渡し)
リポジトリ
- PersonRepositoryにfindByNameメソッドを追加します
package com.example.data.rest_db.repositories; import com.example.data.rest_db.Person; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import org.springframework.data.repository.query.Param; // Add this import statement import java.util.List; // Add this import statement import java.util.Optional; // Add this import statement @Repository public interface PersonRepository extends JpaRepository<Person, Long> { @Query("SELECT p FROM Person p ORDER BY p.id") List<Person> findAllOrderByName(); @Query("FROM Person WHERE firstName LIKE :sname OR lastName LIKE :sname ") public List<Person> findByName(@Param("sname") String sname); public Optional<Person> findById (Long id); }
コントローラー
- searchメソッドを下記のように修正し、作成した
@RequestMapping(value = "/find", method=RequestMethod.POST) public ModelAndView search(HttpServletRequest request, ModelAndView mav){ mav.setViewName("find"); String param = request.getParameter("find_str"); if (param == ""){ mav = new ModelAndView("redirect:/find"); } else { mav.addObject("title","Find result"); mav.addObject("message","Result for " + param); mav.addObject("value", param); // Person person = dao.findById(Long.parseLong(param)); // Person[] list = new Person[] {person}; // List<Person> list = dao.findByFirstName(param); // List<Person> list = dao.find(param); List<Person> list = repository.findByName(param); mav.addObject("people", list); } return mav; // Add this return statement }
動作確認
-
ビルドしSpring Bootを再実行します
$ mvn package $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
考察
今回、DAOを使用してデータベースにアクセスしました。また、アノテーションを使用してあらかじめクエリを定義しアクセスすることでメンテナンス性を向上させるテクニックを試しました。
今後も、他の機能についても継続して、試してみます。
参考