MyBatisお試し #2 データ取得

### Prev

https://qiita.com/yamashun38/items/37a088595f0d13ae937d

## 概要
- 業務でMyBatisを使用することがあったので理解度向上のために自宅でも色々触ってみる
- 下記のような操作が可能なアプリを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757184/d25c23c7-7d5d-0f52-ecf4-e9dcde43b561.png)

## ソースコード

https://github.com/yamashun38/MyBatis

## ユーザ一覧取得
- [localhost:8080/allUsers](localhost:8080/allUsers)へアクセスするとDBに登録しているデータを全件表示できるよう実装する
- 処理の流れは下記のイメージ
    - ユーザが[localhost:8080/allUsers](localhost:8080/allUsers)へアクセス
    - コントローラ → モデル → マッパー → XMLに記載したSQL実行 → 取得したデータをエンティティへ格納 → エンティティからDTOへ変換 → コントローラ → ビューに取得したデータを表示

### データ取得用のSQL用意
- DBからデータを取得する際はSQLをアノテーションに記載する方法とXMLに記載する方法の2種類がある
- 今回はXMLにSQLを記載する方法を選択

```xml:UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mybatis.mapper.UserMapper">

    <!-- 全ユーザを取得 -->
    <select id="findAll" resultType="com.example.mybatis.entity.UserEntity">
        SELECT
            *
        FROM
            users
    </select>
</mapper>
```

- `UserMapper.xml`のselectタグの値と`UserMapper.java`のメソッド名を一致させることで、メソッドが呼ばれるとSQLを実行することが可能になる
- resultType属性の値にはSQLを実行して取得したデータを格納するクラス(`UserEntity.java`)を指定
    - データ格納用のクラスにはDBのカラムに対応するフィールドを用意する
    - 今回はUSERSテーブルで定義している下記の4つのカラムに対応するフィールドを用意
        - id
        - username
        - age
        - address

### マッパー(DAO)インタフェースの実装
- 上記のXML内のSQLを呼び出すために`UserMapper`インタフェースを用意する

```java:UserMapper.java
package com.example.mybatis.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.example.mybatis.dto.UserDto;

@Mapper
public interface UserMapper {

    /**
     * 全データを取得
     */
    List<UserEntity> findAll();
}
```

- `@Mapper`でマッパーということを明示
- `findAll`メソッドによって`UserMapper.xml`のselect文を呼び出す

### モデル
- データベースとのやり取りを実施する`UserLogic`クラスを作成

```java:UserLogic.java
package com.example.mybatis.model;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.example.mybatis.dto.UserDto;
import com.example.mybatis.mapper.UserMapper;

@Component
public class UserLogic {

    @Autowired
    private UserMapper userMapper;

    /**
     * findAllメソッドを呼び出し
     * USERSテーブルの全てのデータを取得
     */
    public List<UserDto> findAll() {
    
        List<UserEntity> userEntities = userMapper.findAll();
        return toDto(userEntities);
    }

    /**
     * UserEntityからUserDtoに変換
     */
    public List<UserDto> toDto(List<UserEntity> userEntities) {

        List<UserDto> allUsers = new ArrayList<>();
        for (UserEntity entity: userEntities) {
            UserDto userDto = new UserDto();
            userDto.setId(entity.getId());
            userDto.setUsername(entity.getUsername());
            userDto.setAge(entity.getAge());
            userDto.setAddress(entity.getAddress());
            allUsers.add(userDto);
        }
        return allUsers;
    }
}
```

- `@Autowired`によってuserMapperに対してDIを実施
- `userMapper`インタフェースのfindAllメソッドを呼び出し
- returnする際はListを`UserEntity`型から`UserDto`型に変換する必要があるため、`toDto`メソッドを呼び出して変換を実施

### コントローラ
- モデル層からSQL実行結果を格納したデータを受け取りビューへ渡す処理を記述

```java:MybatisController
@Controller
public class MybatisController {

    @Autowired
    private UserLogic userLogic;

    // 中略
    
    @GetMapping("/users")
    public String allUsers(Model model) {
        List<UserDto> allUserList = userLogic.findAll();
        model.addAttribute("users", allUserList);
        return "users";
    }
```

- [localhost:8080/allUsers](localhost:8080/allUsers)へアクセスした際にallUsersメソッドを実行
- モデル(`UserLoic.java`)のfindAllメソッドを呼び出し、DBに登録されている全データをList型の`allUserList`に格納
- 受け取った全データのリストをaddAttributeメソッドにより`users`という名前でビューに渡す
- usersをreturnすることで`user.html`を表示

### エンティティの作成
- SQLを実行し取得したデータを格納するためのエンティティクラスを作成

```java:UserEntity.java
package com.example.mybatis.entity;

import lombok.Data;

@Data
public class UserEntity {
    
    private Integer id;
    private String username;
    private Integer age;
    private String address;
}
```

- USERSテーブルのカラムに対応するフィールドを宣言
- `@Data`によってgetterとsetterを自動生成

### DTO作成
- コントローラとモデル間のデータのやり取りはこのDTOクラスを介して行うことで効率的なデータ受け渡しを実現

```java:UserDto.java
package com.example.mybatis.dto;

import lombok.Data;

@Data
public class UserDto {

    private Integer id;
    private String username;
    private Integer age;
    private String address;
}
```

- 基本的にエンティティと同様にDBのカラムに対応するフィールドとgetter、setterを持つ

### application.ymlの修正
- `application.yml`にもMyBatis関連の設定を追記

```yml:application.yml
mybatis:
  mapper-locations: classpath:mapper/*.xml
```

- `mapper-location`でマッパーのXMLを配置しているパスを指定
    - 今回は`src/main/resources/mapper`に配置

### ビューの作成
- 取得したデータを一覧で表示するビューを作成する

```html:users.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>ユーザ一覧</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body>
    <div class="container mt-3">
        <h1>ユーザ一覧</h1>
        <table class="table table-striped table-bordered">
            <thead class="table-dark">
                <tr>
                    <th>ID</th>
                    <th>ユーザ名</th>
                    <th>年齢</th>
                    <th>住所</th>
                </tr>
            </thead>
            <tbody>
                <!-- users からデータを繰り返し表示 -->
                <tr th:each="user : ${users}">
                    <td th:text="${user.id}">ID</td>
                    <td th:text="${user.username}">ユーザ名</td>
                    <td th:text="${user.age}">年齢</td>
                    <td th:text="${user.address}">住所</td>
                </tr>
            </tbody>
        </table>

        <a th:href="@{/main}" class="btn btn-secondary mt-3">メイン画面へ戻る</a>
    </div>
</body>

</html>
```

- `th:each`の部分によってコントローラから受け取った全ユーザのリスト`users`を`user`として扱い、テーブルとして表示
- 表の見た目はBootstrapにより整えているが本記事の内容とはあまり関係ないため割愛
- メイン画面へ戻るためのボタンも用意

### ユーザ一覧取得
- [localhost:8080/allUsers](localhost:8080/allUsers)へアクセス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757184/7cdaeb28-0905-9140-3e57-4e8b0053cadb.png)

## ユーザ検索
- [localhost:8080/userSearch](localhost:8080/userSearch)へアクセスしIDを入力すると、該当するIDのユーザ情報を表示できるようにする
- 上記で作成したソースに対してそれぞれ下記の通り追記する

### UserMapper.xm

```xml:UseMapper.xml
    <select id="findUser" resultType="com.example.mybatis.entity.UserEntity">
        SELECT
            *
        FROM
            users
        WHERE
            id = #{id}
    </select>
```

- 入力フォームから渡されたIDを受け取りSELECT文によりユーザ情報を抽出

### UserMapper.java

```java:UserMapper.java
    /**
     * 入力フォームで指定したIDのデータを取得
     */
    UserEntity findUser(int id) throws UserNotFoundExeption;
```

- `findUser`メソッドの引数としてIDを渡す
- 指定したIDに対応するユーザが存在しない場合は`UserNotExeption`を投げる
※後ほど作成

### UserLogic.java

```java:UserLogic.java
    /**
     * findUserメソッドを呼び出し
     * URLで指定したIDのデータを取得
     */
    public UserDto findUser(UserDto userDto) throws UserNotFoundExeption {

        UserEntity userEntity = userMapper.findUser(userDto.getId());

        if (userEntity == null) {
            throw new UserNotFoundExeption("指定したユーザは存在しません。");
        }
        return toDto(userDto, userEntity);
    }

    /**
     * UserEntityからUserDtoに変換
     */
    public UserDto toDto(UserDto userDto, UserEntity userEntity) {

        userDto.setUsername(userEntity.getUsername());
        userDto.setAge(userEntity.getAge());
        userDto.setAddress(userEntity.getAddress());
        return userDto;
    }
```

- `findUser`メソッドでは入力フォームで入力されたIDを受け取るために、引数として`userDto`を指定
    - `userDto.getId()`でURLに入力したIDを取得可能
- 全件取得の際と同様に`UserEntity`型から`UserDto`型へ変換しreturnする
- 指定したIDに対応するユーザが存在しない場合は、エラーメッセージと共に`UserNotExeption`をコントローラへ投げる

### MybatisController.java

```java:MybatisController.java
    UserDto userDto = new UserDto();

   /**
     * ユーザ検索画面表示
     */
    @GetMapping("/userSearch")
    public String userSearch() {

        return "search/userSearch";
    }

    /**
     * 検索結果表示
     */
    @GetMapping("/userInfo")
    public String userInfo(@RequestParam("id") int id, Model model) {
        
        try {
            userDto.setId(id);
            userDto = userLogic.findUser(userDto);
            model.addAttribute("user", userDto);
        } catch (UserNotFoundException e) {
            model.addAttribute("errorMessage", e.getMessage());
            return "search/userInfo";
        }
        return "search/userInfo";
    }
```

- [localhost:8080/userSearch](localhost:8080/userSearch)にアクセスするとユーザ検索画面を表示
- 検索ボタンを押下すると[localhost:8080/userInfo](localhost:8080/userInfo)へ遷移
    - `@RequestParam("id")`により入力されたIDを引数として受け取る
- DTOの`id`に入力されたIDをセットしモデル層へ渡す
- モデルから受け取ったSELECT文の実行結果(`userDto`)を`user`という名前でビューへ渡す
- モデルから`UserNotFoundExeption`が返ってきている場合はエラーメッセージをビューへ渡す

### userSearch.html
- ユーザ検索画面

```html:user.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>ユーザ検索</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body>
    <div class="container mt-3">
        <h1>ユーザ検索</h1>
        <form th:action="@{/userInfo}" method="get">
            <div class="mb-3">
                <label for="userId" class="form-label">ユーザID</label>
                <input type="number" class="form-control" id="userId" name="id" required>
            </div>
            <button type="submit" class="btn btn-primary">検索</button>
        </form>

        <a th:href="@{/main}" class="btn btn-secondary mt-3">メイン画面へ戻る</a>
    </div>
</body>

</html>
```

- th:actionではボタンを押した際の遷移先を指定
- 今回は[localhost:8080/userInfo](localhost:8080/userInfo)に遷移するため`userInfo`を指定

### userInfo.html
- 検索結果表示画面

```html:userInfo.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>ユーザ検索</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body>
    <div class="container mt-3">
        <h1>検索結果</h1>
        <!-- 指定したIDのユーザが存在しない場合 -->
        <div th:if="${errorMessage}">
            <p th:text="${errorMessage}">指定したユーザは存在しません。</p>
        </div>
        <!-- 指定したIDのユーザが存在する場合 -->
        <div th:if="${user}">
            <table class="table table-striped table-bordered">
                <thead class="table-dark">
                    <tr>
                        <th>ID</th>
                        <th>ユーザ名</th>
                        <th>年齢</th>
                        <th>住所</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- user のデータを表示 -->
                    <td th:text="${user.id}">ID</td>
                    <td th:text="${user.username}">ユーザ名</td>
                    <td th:text="${user.age}">年齢</td>
                    <td th:text="${user.address}">住所</td>
                </tbody>
            </table>
        </div>

        <a th:href="@{/main}" class="btn btn-secondary mt-3">メイン画面へ戻る</a>
    </div>
</body>

</html>
```

- `th:if`により指定したIDに対応するユーザが存在する場合と存在しない場合の条件分岐を実施
- 存在する場合はユーザ情報を、存在しない場合はエラーメッセージを表示

### UserNotFoundExeption.java
- exeptionパッケージを新たに作成し`UserNotFoundExeption`クラスを作成

```java:UserNotFoundExeption.java
package com.example.mybatis.exception;

/**
 * ユーザが見つからない場合の例外
 */
public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(String message) {
        super(message);
    }
}
```

- `ObjectNotFoundExeption`を使用しても良かったが、エラーメッセージに余計な文章がついてしまうため独自クラスとして実装
- `UserLogic`クラスで指定したメッセージをエラーメッセージとして設定

### ユーザ検索
#### ユーザが存在する場合
- ID：1

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757184/da07865d-6b2a-4bff-bc7c-2e3409dd4d7c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757184/8d5ac25d-0eff-46a0-24e9-f553ed2fe2b7.png)

#### ユーザが存在しない場合
- ID：7

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757184/942c0773-80e6-0063-278b-7cc369f09910.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757184/db26466a-4a7c-7c5d-cd87-29fc67b7452c.png)

## Next

https://qiita.com/yamashun38/items/0d8991711435d15c423b