簡易書籍検索アプリケーションの説明(EntityManager版)

8.3 簡易書籍検索アプリケーションの説明(EntityManager版)

8.2で作成したアプリケーションをもとに、EntityManagerを利用したより高度なデータベースアクセスが実際どのような流れで動作しているのかを説明していきます。

8.3.1 BookDaoクラス(DAOクラス)

まずは今回初めて登場したDAOクラスですが、EntityManagerを利用した一連の流れを確認していきましょう。

	package jp.co.f1.spring.bms.dao;

	import org.springframework.stereotype.Repository;
	
	import jakarta.persistence.EntityManager;
	import jakarta.persistence.criteria.CriteriaBuilder;
	import jakarta.persistence.criteria.CriteriaQuery;
	import jakarta.persistence.criteria.Root;
	import java.util.ArrayList;
	
	import jp.co.f1.spring.bms.entity.Book;
	
	@Repository
	public class BookDao {
	
	    // エンティティマネージャー
	    private EntityManager entityManager;
	
	    // クエリ生成用インスタンス
	    private CriteriaBuilder builder;
	
	    // クエリ実行用インスタンス
	    private CriteriaQuery<Book> query;
	
	    // 検索されるエンティティのルート
	    private Root<Book> root;
	
	    /**
	     * コンストラクタ(DB接続準備)
	     */
	    public BookDao(EntityManager entityManager) {
	        // EntityManager取得
	        this.entityManager = entityManager;
	        // クエリ生成用インスタンス
	        builder = entityManager.getCriteriaBuilder();
	        // クエリ実行用インスタンス
	        query = builder.createQuery(Book.class);
	        // 検索されるエンティティのルート
	        root = query.from(Book.class);
	    }
	
	    /**
	     * 書籍情報検索
	     * @param String isbn
	     * @param String title
	     * @param String price
	     * @return ArrayList<Book> book_list
	     */
	    public ArrayList<Book> find(String isbn, String title, String price) {
	        // SELECT句設定
	        query.select(root);
	
	        // WHERE句設定
	        query.where(
	            builder.like(root.get("isbn"), "%" + isbn + "%"),
	            builder.like(root.get("title"), "%" + title + "%"),
	            builder.like(root.get("price"), "%" + price + "%")
	        );
	
	        // クエリ実行
	        return (ArrayList<Book>)entityManager.createQuery(query).getResultList();
	    }
	
	}
	

■@Repositoryアノテーション
6章で作成したリポジトリインターフェース同様、データベースにアクセスするためのクラスにも、この@Repositoryアノテーションを付けておきます。    13: @Repository
   14: public class BookDao {

■EntityManager
ここでは「EntityManager」というクラスを補完するためのフィールドを用意しています。このEntityManagerというクラスは、エンティティを利用する為に必要な機能を提供します。Spring Data JPAでデータベースアクセスを行うには、このEntityManagerクラスの使い方さえ覚えておけば、たいていのことは実装できるようになる、といってよいでしょう。
なお、ここではインスタンスを作成する処理は用意されていませんが、これはこの後解説するコントローラー側からBookDAOインスタンスを作成する際に設定します。    16: // エンティティマネージャー
   17: private EntityManager entityManager;
   18:
   19: …途中、省略…
   20:
   28: /**
   29: * コンストラクタ(DB接続準備)
   30: */
   31: public BookDao(EntityManager entityManager) {
   32: // EntityManager取得
   33: this.entityManager = entityManager;

EntityManagerは、エンティティを操作するための機能を一通り持っています。エンティティを扱う場合は、どんな操作であれ、まずはEntityManagerを用意することから始める、と覚えておきましょう。


■Criteria APIの基本3クラス
JPAには「Criteria API」という機能があり、これを利用することで、Javaらしいデータベースアクセスが行えるようになります。Criteria APIでは、3つのクラスを組み合わせて利用します。

この3つのクラスを使いこなすことで、必要なエンティティを検索したりすることができるようになります。

① CriteriaBuilderの取得
   CriteriaBuilder builder = entityManager.getCriteriaBuilder();

最初に行うのは、CriteriaBuilderインスタンスの用意です。これはEntityManagerのgetCriteriaBuilderを呼び出すだけです。ここでは下記のように行なっています。
   19: // クエリ生成用インスタンス
   20: private CriteriaBuilder builder;

   34: // クエリ生成用インスタンス
   35: builder = entityManager.getCriteriaBuilder();

② CriteriaQueryの作成
   CriteriaQuery<エンティティ名> query = builder.createQuery(エンティティ名.class);
CriteriaQueryは、Criteria API専用のクエリ実行用クラスと考えるとよいでしょう。CriteriaQueryは、クエリ文を使いません。特定のエンティティにアクセスするには、そのエンティティのclassプロパティを引数に指定します。ここでは下記のように行なっています。
   22: // クエリ実行用インスタンス
   23: private CriteriaQuery<Book> query;

   36: // クエリ実行用インスタンス
   37: query = builder.createQuery(Book.class);

③ Rootの取得
   Root<エンティティ名> root = query.from(エンティティ名.class);
RootをCriteriaQueryのfromメソッドで取得します。引数には、検索するエンティティのclassプロパティを指定します。「from」メソッドを呼び出すことで、Bookから取得される、全Bookを情報として保持したRootインスタンスが得られます。これで検索の準備が整いました。
   25: // 検索されるエンティティのルート
   26: private Root<Book> root;

   38: // 検索されるエンティティのルート
   39: root = query.from(Book.class);

④ CriteriaQueryのメソッドを実行
CriteriaQueryでエンティティを絞り込むためのメソッドを呼び出します。その他のメソッドについては 8.4 でご紹介しますが、これにはいくつか用意されており連続して呼び出していきます。ここでは下記のように行なっています。
   42: /**
   43: * 書籍情報検索
   44: * @param String isbn
   45: * @param String title
   46: * @param String price
   47: * @return ArrayList<Book> book_list
   48: */
   49: public ArrayList<Book> find(String isbn, String title, String price) {
   50: // SELECT句設定
   51: query.select(root);
   52:
   53: // WHERE句設定
   54: query.where(
   55: builder.like(root.get("isbn"), "%" + isbn + "%"),
   56: builder.like(root.get("title"), "%" + title + "%"),
   57: builder.like(root.get("price"), "%" + price + "%")
   58: );
   59:
   60: // クエリ実行
   61: return (ArrayList<Book>)entityManager.createQuery(query).getResultList();
   62: }

■selectメソッド
Root取得後、Bookの情報を取得するのに、CriteriaQueryの「select」を呼び出しています。引数にBook.classを指定することで、Rootに保持されている全Bookを取得するようにCriteriaQueryが設定されます。
   50: // SELECT句設定
   51: query.select(root);

■whereメソッド
ここでは、フォームから送信された値(isbn、title、price)を含むエンティティだけを検索するようにしています。selectメソッドを呼び出した後、取り出すエンティティを絞り込むための処理として、以下のようにメソッドを呼び出しています。
Root取得後、Bookの情報を取得するのに、CriteriaQueryの「select」を呼び出しています。引数にBook.classを指定することで、Rootに保持されている全Bookを取得するようにCriteriaQueryが設定されます。
   53: // WHERE句設定
   54: query.where(
   55: builder.like(root.get("isbn"), "%" + isbn + "%"),
   56: builder.like(root.get("title"), "%" + title + "%"),
   57: builder.like(root.get("price"), "%" + price + "%")
   58: );

これは単純なようですが、いくつかのメソッドが組み合わせられていることがわかると思います。簡単に整理しておきましょう。

   《CriteriaQuery》.where(《Expression<boolean>》)

引数に指定するExpressionによって、エンティティを絞り込む処理を行ないます。Expressionというのは後述しますが、様々な式の評価を扱います。

   《CriteriaBuilder》.like(《Path》,《String》)

第1引数に指定した要素の値が、第2引数の文字列を含んでいるかどうかをチェックします。SQLのLIKE演算子と同じく、値の前後に「%」を付けると、あいまい検索として文字列を比較できます。


引数に指定した値により、両者が条件に一致するかどうかを確認し、結果をPredicateというクラスのインスタンスとして返します。これはExpressionのサブクラスです。これはメソッドによって指定される条件や式などの記述をオブジェクトとして表す役割を果たします。
Predicateは、多数のエンティティがあるところに、絞り込む条件を付加する働きをします。つまり、equalならば、引数に指定したものが等しいという条件を示すPredicateが用意されることになります。これを元にして、その条件に合致するエンティティを絞り込むことができるわけです。

   《Root》.get(《String》)

Rootにあるメソッドで、エンティティから指定のプロパティの値に関するPathインスタンス(これもExpressionのサブクラスです)を返します。

■Expressionについて
Criteria APIが非常に複雑に思えるのは、ここで登場する「Expression」がうまくイメージできない、という理由が大きいでしょう。
これは文字通り、「評価」を扱うオブジェクトです。whereならば、一度SQLの考え方に戻って、「WHERE句はどういう働きをするものか」を考えるとイメージしやすいでしょう。WHERE句では、その後に記述された式を評価し、その結果がtrueとなるレコードだけを絞り込んで取得する働きをします。

このwhereメソッドも、行なっていることはそれと同じです。ただ、クエリ文のテキストではなく、オブジェクトとして引数を指定する点が異なっているだけです。ということは、真偽値で評価する式に相当するものがオブジェクトとして渡されるはずだ、ということが想像つくのではないでしょうか。それが、引数のbuilder.likeの戻り値であるPredicateだったのです。

Predicateは、多数のエンティティからさまざまな条件によってデータを絞り込むのに重要な役割を果たします。Criteria APIには、さまざまな条件を示すためのPredicateを返すメソッドが用意されており、これらを使って得られたPredicateを組み合わせて、複雑な絞り込みが行なえるようになるのです。

⑤ createQueryして結果を取得
最後に、createQueryでQueryを生成し、getResultListで結果のListを取得してメソッドの呼び出し元に返します。
   60: // クエリ実行
   61: return (ArrayList<Book>)entityManager.createQuery(query).getResultList();

8.3.2 BookControllerクラス(コントローラークラス)

最後に、ここまでのデータアクセス処理を呼び出す側を確認します。

■@PersistenceContextアノテーション
ここでは、EntityManagerのフィールドを用意していますが、そこに見たことのないアノテーションが付けられています。
   28: // EntityManager自動インスタンス化
   29: @PersistenceContext
   30: private EntityManager entityManager;

この「@PersistenceContext」というアノテーションは、EntityManagerのBeanを取得してフィールドに設定します。EntityManagerは、Spring Bootの場合、起動時に自動的にBeanとしてインスタンスが登録されています。これを@PersistenceContextにより、このフィールドに割り当てているのです。
EntityManagerの取得には、このアノテーションを利用するのがSpring Bootの基本と考えておきましょう。

■@Autowiredアノテーション
ここでは BookDao インスタンスをフィールドに関連付けています。
   32: // DAO自動インスタンス化
   33: @Autowired
   34: private BookDao bookDao;

■@PostConstructアノテーション
ここでは、initメソッドに「@PostConstruct」というアノテーションが付けられています。これはコンストラクタによりインスタンスが生成された後に呼び出されるメソッドであることを示すものです。このアノテーションが付けられていれば、メソッド名はなんでも構いません。
コントローラーは、最初に一度だけインスタンスが作成され、以後はそのインスタンスが保持されます。そのため、ここでDAOのインスタンスを作成しておけば、アプリケーション実行時に必ず一度だけ実行され、コントローラー内で共有することができます。
   36: @PostConstruct
   37: public void init() {
   38: bookDao = new BookDao(entityManager);
   39: }

■フォームから送信された値を受け取って、DAOのfindメソッドを呼び出す
searchメソッドでは、フォームから送信された値を受け取って処理を行うことになります。これまで、こうした際には@RequestParamアノテーションを使ってきましたが、ここでは「HttpServletRequest」を引数に用意しています。
59: /**
60: * 「/search」へアクセスがあった場合
61: */
62: @GetMapping("/search")
63: public ModelAndView search(HttpServletRequest request, ModelAndView mav) {
64: // bookinfoテーブルから検索
65: Iterable<Book> book_list = bookDao.find(
66: request.getParameter("isbn"),
67: request.getParameter("title"),
68: request.getParameter("price")
69: );

HttpServletRequestというのは、JSP/サーブレットでお馴染みの、あのHttpServletRequestです。サーブレットで doGet/doPost する際に皆さんが使ってきましたね。
これまでフォームから値を受け取るメソッドでは@RequestParamを利用してきましたが、HttpServletRequestが使えないわけではありません。要するに@RequestParamのパラメータというのは、HttpServletRequestのgetParameterを呼び出してパラメーターを受け取る操作を自動的に行い、その結果を引数に設定するものだった、というわけです。

HttpServletRequestと同様に、HttpServletResponseも引数に指定することができます。サーブレットでお馴染みのオブジェクトも、Spring Bootでは使えるのです。

8.3.3 @PersistenceContextは複数回割り当てられない

基本的なEntityManagerとDAOの使い方はこれでわかったかと思います。が、おそらく皆さんの中には、EntityManagerの配置の仕方に疑問を感じた人もいるかも知れません。
ここでは、EntityManagerをBookDao内で利用しています。「だったら、BookDaoにあるEntityManagerフィールドに、@PersistenceContextを付けて自動的に割り当てるようにすればいいじゃないか」と思ったことでしょう。確かにその通りで、下記のように修正することできちんと動きます。

  ① DAOクラスに、@PersistenceContextでEntityManagerを用意する。
  ② コントローラーには、@AutowiredでDAOを用意する。

ただ、ここで注意したいのは、「DAOは必ず@Autowiredで割り当てる」という点です。@Autowiredで自動的にバインドされる際に、DAO内の@PersistenceContextも自動的に割り当てられます。したがって、DAOを@Autowiredしないと、DAO内の@PersistenceContextは機能せず、nullとなってしまうので注意してください。
このやり方ならば、いちいちコントローラーでDAOのインスタンスを作成し、EntityManagerを割り当てる、なんて作業をする必要もありません。すべてBeanが自動的に割り当てられ、インスタンス作成のコードなど書くことなく使えるようになります。

では、なぜここではこんな面倒なことを行なったのか。それは、「@PersistenceContextを使ったBeanのバインドは、複数回設定できない」からです。

■Beanをバインドできるのは1つだけ
@PersistenceContextは、アプリケーションによってあらかじめ用意されているBeanを割り当てます。このBeanは、「1クラスにつき1インスタンス」しか用意されません。
つまり、色々なDAOクラスでEntityManagerを利用しようとした場合、別々に@PersistenceContextを利用してEntityManagerを割り当てようとしても、エラーとなってしまうのです。割り当ては1箇所にまとめておく必要があります。


NEXT>> 8.4 値を比較するCriteriaBuilderのメソッド