Spring BootでDAOからデータベースにアクセスしてみた-その1（DAOとアノテーション）

# 背景・目的

過去に、Spring Bootについて下記の記事にまとめました。今回は、EntityManagerによるデータベースアクセスを試します。
- [Spring Bootでバリデーションを試してみた](https://qiita.com/zumax/items/f09bacf18f3bb96d1331)
- [Spring Bootでデータを保存する-その2](https://qiita.com/zumax/items/06d0465218478840a949)
- [Spring Bootでデータを保存する-その1](https://qiita.com/zumax/items/2e387254c736659bf20c)
- [Spring Bootでデータベースへのアクセスを試してみた](https://qiita.com/zumax/items/7487b86f56ea12c31d44)
- [Sprint Bootの環境を構築したときのメモ](https://qiita.com/zumax/items/8effa97f338dd0224b22)


# まとめ
下記に特徴をまとめます

|特徴|説明|
|:--|:--|
|DAO|Database Access Objectの略<BR><BR>永続化のメカニズムを提供する抽象化インターフェイス<BR><BR>この分離により、単一責任の原則をサポートする|
|JPQL|Jakarta Persistence Query Languageの略<BR><BR>リレーショナルデータベースに格納されたエンティティに対してクエリを実行するために使用される|
|クエリアノテーション|メンテナンスの手間が軽減|
|NamedQueryアノテーション|クエリアノテーションの一種<BR><BR>エンティティで実装|
|Queryアノテーション|クエリアノテーションの一種<BR><BR>DAOやリポジトリで実装|


# 概要
Spring Bootでは、JPAの一部である、リポジトリをつかって簡単なデータアクセスができます。
今回は、リポジトリ以外のJPAの機能について、整理します。

## DAO
[Data access object](https://en.wikipedia.org/wiki/Data_access_object)を基に整理します。

- Database Access Object（以降、DAOという）は、データベースや、その他の永続化のメカニズムを提供する抽象化インフェイス
- アプリケーションの呼び出しを永続化レイヤーとのマッピングにより、詳細なデータ操作を隠蔽する
- この分離により、単一責任の原則をサポートする
    - これにより、ドメイン固有のオブジェクトとデータ型 (DAO のパブリック インターフェイス) の観点からアプリケーションが必要とするデータ アクセスが、特定のDBMSでこれらのニーズを満たす方法(DAO の実装) から分離される

## JPQL
[Jakarta Persistence Query Language](https://en.wikipedia.org/wiki/Jakarta_Persistence_Query_Language)を基に整理します。

- Jakarta Persistence Query Languageの略
    - 以前は、Java Persistence Query Languageのようです
- リレーショナルデータベースに格納されたエンティティに対してクエリを実行するために使用される
- そのクエリは構文がSQLクエリに似ている
- データベーステーブルを直接操作するのではなく、JPAエンティティオブジェクトに対して操作する
- JPQLをJPAが受け付けて、SQLに変換し、データベースにクエリを実行する

## クエリアノテーション
[Annotation-based Configuration](https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query-methods.named-queries.annotation-based-configuration)を基に整理します。

- クエリをリテラルとしてDAOクラス内に文字列リテラルとして埋め込まれているのは、メンテナンスの観点から好ましくない
- アノテーションベースの構成には、別の構成ファイルを編集する必要がないため、メンテナンスの手間が軽減されるという利点がある

### NamedQueryアノテーション
- クエリアノテーションには、クエリに名前をつけて利用できる「名前付きクエリ」を作成できる

### Queryアノテーション
- NamedQueryでは、エンティティクラスにあらかじめ定義しておくが、エンティティに寄せると、メンテナンス性が悪い

# 実践
## 前提
[Spring Bootでバリデーションを試してみた](https://qiita.com/zumax/items/f09bacf18f3bb96d1331)で使用していた環境を再利用します。

## DAOを用意する
### インタフェイスの実装

1. コントローラーと同一パッケージにPersonDAOを用意します
1. 下記のコードを追加します
    ```java
    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の実装
1. DAOインタフェイスと同一パッケージにPersonDAOPersonImplを用意します
1. 下記のコードを追記します
    ```java
    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<Person>で得られているかチェックをおこなうか
        - ビルド時に警告を出すかをコントロールする

### コントローラー
1. [Spring Bootでバリデーションを試してみた](https://qiita.com/zumax/items/f09bacf18f3bb96d1331)で使用していたWebControlloerに下記のコードを追加します
    ```Java
      @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にアクセスをまとめておくことで、コントローラーでは呼び出すだけで済む

### ビューテンプレート
1. templateにfind.htmlを追加します
1. 下記のコードを追記します
    ```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>
    ```
### 動作確認
1. ビルドしSpring Bootを再実行します
    ```bash
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080/findにアクセスします。見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f8514903-21da-22ad-ece2-e1ef369f771c.png)

現時点では、Click後のアクションを定義していないので、ここまでとします。

## ID検索できるようにする
### DAOインタフェイス
1. PersonDAOに、下記のメソッドを追加します
    - findById
        - ID番号を引数に指定してエンティティを検索し返す
    - findByName
        - 名前から検索する
    ```java
    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);
    }
    ```

### PersonDAOPersonImplクラス
1. PersonDAOPersonImplに、下記のメソッドを実装します
    ```java
        @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を返す
### コントローラー
1. コントローラーに下記のメソッドを追加します
    ```java
      @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で利用していた


### 動作確認
1. ビルドしSpring Bootを再実行します
    ```bash
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080/findにアクセスします。見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/fb449b85-1864-6fd6-c28b-5dcb9e0b0e45.png)

1. ID「1」を入力し、Clickします。絞り込みされました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/4ac779dc-8204-c9e0-8505-d1027d1509d4.png)

## 名前で検索できるようにする
### コントローラー
1. コントローラーのメソッドを修正します
    ```java
      @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
      }
    ```
### 動作確認
1. ビルドしSpring Bootを再実行します
    ```bash
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080/findにアクセスします。見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6939492a-6893-6bba-fc2a-37fe73d27756.png)

1. firstName「Hanako」を入力し、Clickします。想定通り絞り込めました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/78157287-f235-c053-20bb-161d3918c2b0.png)

1. ID「1」を入力し、Clickします。想定通りヒットしませんでした
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/41f7197d-fa8e-3575-c847-c35207ff5bba.png)

## 複数のパラメータを追加
firstName、LastName、Phone、IDで検索できるように、いずれかの条件にヒットしたら返されるようにします。

### DAOインタフェイス
1. 下記のメソッドを追加します
    ```java
    public List<T> find(String fstr);
    ```

### DAOクラス
1. 下記のメソッドを追加します
    ```java
        @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;
        }
    
    ```
### コントローラーの修正
1. 上記のDAOインタフェイスを利用するように修正します
    ```java
      @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
      }
    ```
### 動作確認
1. ビルドしSpring Bootを再実行します
    ```bash
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080/findにアクセスします。見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d01331d8-7daa-cb1d-4e21-c5096f585b83.png)

1. IDを想定して、「3」を入力し「Click」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/483053d2-7295-e7b3-52b8-567083959b3a.png)

1. firstNameを想定して「Taro」を入力し、「Click」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/78f7444c-3fa9-371a-09e3-dc42bedc9fc2.png)

1. lastNameを想定して「Yamada」を入力し、「Click」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3d15789c-8895-75b4-f80f-18c20526aa48.png)

1. Phoneを想定して「0003」を入力し、「Click」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/96d69e46-9eb5-e64f-d5ce-713541f9c035.png)

## クエリアノテーション
`@NamedQuery`というアノテーションを使えます。

### エンティティの修正
1. Personクラスに、下記を追加します
    - クラス上に`@NamedQuery`アノテーションを追加します
    ```java
    @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 {
    ```
1. 複数を追加する場合には、下記のように変更します
    ```java
    @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を利用します。

1. DAOのfindメソッドを下記のように修正します
    ```java
    @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;
    }
    ```
### 動作確認
1. ビルドしSpring Bootを再実行します
    ```bash
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080/findにアクセスします。見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/90f3eeeb-1db8-9c5e-12ed-0e4acef26eed.png)

1. lastNameを想定して、「Yamada」を入力し、「Click」します。想定通りでした
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/2ab839af-925f-212a-cf5d-bb3ca26b5e5e.png)

## リポジトリやアノテーションを実装する
### リポジトリ
1. PersonRepositoryインターフェイスに新たにfindメソッドを追加します
    ```java
    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);
    }
    ```

### コントローラーの修正
1. WebControllerクラスの、indexメソッドを下記のように修正します
    ```java
      @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;
      }
    ```
### 動作確認
1. ビルドしSpring Bootを再実行します
    ```bash
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080/findにアクセスします。見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/55f2cd74-05e0-1a8b-ea20-0baf23c0969b.png)

## リポジトリやアノテーションを実装する（パラメータ渡し）
### リポジトリ
1. PersonRepositoryにfindByNameメソッドを追加します
    ```java
    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);
    }
    ```

### コントローラー
1. searchメソッドを下記のように修正し、作成した
    ```java
      @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
      }
    ```

### 動作確認
1. ビルドしSpring Bootを再実行します
    ```bash
    $ mvn package
    $ java -jar target/rest-db-0.0.1-SNAPSHOT.jar
    ```
1. localhost:8080/findにアクセスします。見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/7ed56e60-dcdb-51dd-e553-9381798f91d7.png)

1. lastNameを想定して「Yamada」を入力し、「Click」します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d0680a27-3dfe-f708-cf74-dd32f9d266e0.png)

1. firstNameを想定して「Taro」を入力し、「Click」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/1ea3a2c8-9098-af82-7064-991207cf6414.png)


# 考察
今回、DAOを使用してデータベースにアクセスしました。また、アノテーションを使用してあらかじめクエリを定義しアクセスすることでメンテナンス性を向上させるテクニックを試しました。
今後も、他の機能についても継続して、試してみます。

# 参考
https://en.wikipedia.org/wiki/Data_access_object

https://en.wikipedia.org/wiki/Jakarta_Persistence_Query_Language

https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query-methods.named-queries.annotation-based-configuration

[Spring Boot 3 プログラミング入門](https://www.amazon.co.jp/Spring-Boot-3-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%85%A5%E9%96%80-%E6%8E%8C%E7%94%B0%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798069167)