SpringBoot＋MyBatisでCRUDを書いてみた

# はじめに
SpringBootでDB操作を行うRepository層の実装をまとめていきたいと思います。
DBとのインタフェースに使うO/Rマッパーはいろいろありますが、この記事では`MyBatis`を使います。

`MyBatis`はJavaとSQLのマッピングがシンプルでわかりやすく動的SQLの記述も可能です。
MyBatisGeneratorを使うとDBのテーブル情報から、JavaのModelクラスやCRUD用メソッドが実装されたMapperクラスをごそっと作れるのがとても便利です。（別の記事で紹介予定）

今回はModelとMapperの対応を理解するため手動で作成します。

# 開発環境
OS : macOS Catalina
IDE : IntelliJ Ultimate
Java : 11
Gradle : 6.6.1
SpringBoot : 2.3.4
MySQL : 8.0.17


# 1. データベースの準備
まずはアプリケーションで操作するDBの準備を行います。
ここでは`MySQL`のローカルの環境を整えます。

## 1-1. MySQLの準備
DockerでMySQLを使うのがお手軽で簡単です。（丸投げ）

- [dockerでmysqlを使う](https://qiita.com/astrsk_hori/items/e3d6c237d68be1a6f548) 

上記でMySQLに`root`ユーザで接続後、本アプリ用の`database`と`user`を作っておきます。

```sql
-- データベースdemo_dbの作成
CREATE DATABASE demo_db;
-- ユーザ名：demo、パスワード:demo、権限:demo_dbに対する全ての権限
CREATE USER 'demo'@'%' IDENTIFIED BY 'demo';
GRANT ALL PRIVILEGES ON demo_db.* TO 'demo'@'%';
FLUSH PRIVILEGES;
```
これでMySQLの準備は完了です。


## 1-2. SpringBootプロジェクトの作成
SpringBootプロジェクトは[前回のをベース](https://qiita.com/kilvis/items/d75461b3596bfb0f6759#1-%E3%83%86%E3%82%B9%E3%83%88%E5%AF%BE%E8%B1%A1%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E6%BA%96%E5%82%99)にMyBatis、JDBC（MySQL）のライブラリを追加します。

```build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3' // 追加
    compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.22' // 追加
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}
```

## 1-3. アプリ用のデータソースの設定
アプリのローカル環境用に`MySQL`への接続設定を`main/resources/application.yml`に記述します。
先ほど作った `user：demo`、`Database:demo_db` の接続設定は以下の通りです。

```yml:src/main/resources/application.yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo_db
    username: demo
    password: demo
mybatis:
  configuration:
    map-underscore-to-camel-case: true
```
`map-underscore-to-camel-case: true`はDBオブジェクトとJavaオブジェクトをマッピングする際にスネークケースの名前をキャメルケースにマッピングしてくれます。

以上で`MySQL`の接続設定は完了です。


## 1-4. テーブルの作成
簡単なサンプルのテーブルを作成します。

##### customerテーブル仕様
|列名|型|制約など|
|---|---|---|
|id|int|自動採番のプライマリキー|
|name|varchar(100)|not null制約|
|age|int|not null制約|
|address|varchar(200)||

##### DDL
```sql:schema.sql
create table customer
(
    id int auto_increment,
    name varchar(100) not null,
    age int not null,
    address varchar(200) null,
    constraint customer_pk primary key (id)
);
```

##### DML
ついでに初期投入データも作っておきます。

```sql:data.sql
insert into
    customer (name, age, address)
VALUES
    ('ルーク・スカイウォーカー', 19, 'タトゥイーン'),
    ('レイア・オーガナ', 19, 'オルデラン'),
    ('ハン・ソロ', 32, 'コレリア'),
    ('ダース・ベイダー', 41, 'タトゥイーン');
```
上記のDDL、DMLを`MySQL`で実行＆commitしてテーブルと初期データをセットアップします。

また、作ったDDLは`schema.sql`、DMLは`data.sql`として`test/resources`に保存しておきます。
これらのファイルは次回のDBUnitテストで使います。

```
├── build.gradle
└── src
    ├── main
    └── test
        ├── java
        └── resources
            ├── application.yml
            ├── schema.sql   // DDL
            └── data.sql     // DML
```

# 2. MyBatisを使ったRepositoryの作成
DB操作を行うためのRepositoryクラスを作成します。
今回、O/RマッパーにMyBatisを使うため、お作法に則り

- Select句の結果にマッピングするModel(Entity)クラスの作成
- ModelとCRUDのSQLをマッピングするMapperの作成
- Mapperを利用するRepositoryクラスの作成

の順に作っていきます。

## 2-1. Model(Entity)クラスの作成
先ほど作った`customer`テーブルの１行分のデータを保持するModelクラスを作成します。

```java:Customer.java
package com.example.dbunitdemo.domain.model;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class Customer {
    private Long id;
    private String name;
    private Integer age;
    private String address;
}
```

`@lombok.Data`のおかげでアクセサとtoString()が自動で作られるのでシンプルです。

## 2-2. Mapperの作成
先ほど作った`Customer`クラスをCRUDのSQLと結びつけるMapperを作成します。
Mapperは`Javaのインタフェース`と`SQLを記述したXMLファイル`の２つで１組みとなるよう作成します。

##### CustomerMapper.java

```java:com.example.dbunitdemo.domain.mapper.CustomerMapper.java
package com.example.dbunitdemo.domain.mapper;

import com.example.dbunitdemo.domain.model.Customer;
import org.apache.ibatis.annotations.*;
import java.util.List;

@Mapper
public interface CustomerMapper {
    List<Customer> findAll();
    Customer get(@Param("id") Long id);
    int insert(@Param("customer") Customer customer);
    int update(@Param("customer") Customer customer);
    int delete(@Param("id") Long id);
}
```
`@Param`は引数を後述のXMLのSQLステートメントで参照するための名前を指定します。

##### CustomerMappler.xml

```xml:main/resources/com/example/dbunitdemo/domain/mapper/CustomerMapper.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.dbunitdemo.domain.mapper.CustomerMapper">
    <select id="findAll" resultType="com.example.dbunitdemo.domain.model.Customer">
        SELECT id, name, age, address FROM customer
    </select>
    <select id="get" resultType="com.example.dbunitdemo.domain.model.Customer">
        SELECT id, name, age, address FROM customer WHERE id = #{id}
    </select>
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO customer (name, age, address) VALUES (#{customer.name}, #{customer.age}, #{customer.address})
    </insert>
    <update id="update">
        UPDATE customer SET name = #{customer.name}, age = #{customer.age}, address = #{customer.address} WHERE id = #{customer.id}
    </update>
    <delete id="delete">
        DELETE FROM customer WHERE id = #{id}
    </delete>
</mapper>
```
##### 補足：MapperのXML作成の注意点

- `main/resources`配下にJavaのパッケージ階層と同じディレクトリ階層を作成して`(Mapper名).xml`ファイルを作成する。
- `<mapper namespace="...">`にはMapperインタフェースのFQCNを指定する。
- `<insert>`、`<select>`、`<update>`、`<delete>`の`id`属性にMapperインタフェースのメソッド名を指定し、コンテンツ部分にSQLを記述して**MapperのメソッドとSQLを紐付ける。**
- `<select resultType="...">`には検索結果をマッピングするModelクラスのFQCNを指定する。
- `<insert>`で登録時に、自動採番された値を元のModelのプロパティにセットしたい場合`useGeneratedKeys="true"`と`keyProperty="id"`を組み合わせて指定する。


##### AppConfig.java

最後にMapperインタフェースの属するパッケージをコンフィグクラスのアノテーションで指定します。

```java:AppConfig.java
package com.example.dbunitdemo.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.example.dbunitdemo.domain.mapper") // Mapperの属するパッケージを指定
public class AppConfig {
   // ... 省略
}
```
以上で`CustomerMapper`の作成は完了です。

Mapperの作成後のディレクトリ構成は以下のようになっています。

```
├── main
│   ├── java
│   │   └── com
│   │       └── example
│   │           └── dbunitdemo
│   │               ├── DbunitDemoApplication.java
│   │               ├── config
│   │               │   └── AppConfig.java
│   │               └── domain
│   │                   ├── mapper
│   │                   │   └── CustomerMapper.java
│   │                   └── model
│   │                       └── Customer.java
│   └── resources
│       ├── application.yml
│       ├── com
│       │   └── example
│       │       └── dbunitdemo
│       │           └── domain
│       │               └── mapper
│       │                   └── CustomerMapper.xml
```

## 2-3. Mapperを利用するRepositoryクラスの作成
シンプルにDIした`Mapper`を`Repository`から利用します。

```java:CustomerRepository.java
package com.example.dbunitdemo.domain.repository;

import com.example.dbunitdemo.domain.mapper.CustomerMapper;
import com.example.dbunitdemo.domain.model.Customer;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class CustomerRepository {

  private final CustomerMapper customerMapper;

  public List<Customer> findAll() {
    return customerMapper.findAll();
  }

  public Customer get(Long id) {
    return customerMapper.get(id);
  }

  public int create(Customer customer) {
    return customerMapper.insert(customer);
  }

  public int update(Customer customer) {
    return customerMapper.update(customer);
  }

  public int delete(Long id) {
    return customerMapper.delete(id);
  }
}
```
`Mapper`をラップしてるだけで意味のないクラスに思えるかもしれませんが、
`Service`が**インフラやミドルウェアに依存しないようにする**ために必要なことです。

例えば、`Repository`を介さずに直接`Service`から`Mapper`を利用した場合、`Service`のロジック内にMyBatis特有の処理（例えばExampleを使ったクエリ構築）や、下手したらDBがMySQLであることを意識した処理なんかが実装されてしまう可能性があります。

`Service`は`Repository`のトランザクション管理に注力すべきで、インフラやミドルウェアに依存した処理は実装すべきではありません。
そのような部分を隠蔽するために`Repository`を作成します。


# 3. 動作確認する
ここまで作ったら動作確認したいので、サクッと`Repository -> Service -> Controller`まで繋げてみます。
ひとまず、DBの初期データの表示を確認するために `findAll` メソッドにだけ実装してみます。

```java:CustomerService.java
@Service
@RequiredArgsConstructor
public class CustomerService {
  private final CustomerRepository customerRepository;

  @Transactional(readOnly = true)
  public List<Customer> findAll() {
    return customerRepository.findAll();
  }
}
```

```java:CustomerController.java

@RequiredArgsConstructor
@RestController
public class CustomerController {
    private final CustomerService customerService;

    @GetMapping("customers")
    public List<Customer> findAll() {
        return customerService.findAll();
    }
}
```

ここまで作成したら`bootRun`でアプリケーションを起動して、ブラウザから `http://localhost:8080/customers` にアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430093/49a3e0b0-1c05-3dc4-c480-c936c9cdd6b2.png)

無事に投入していたデータが表示されました！

# 次回はDBUnitでテストしてみる
今回は`MySQL`の設定、MyBatisの`Mapper`とそれを利用する`Repository`の実装をまとめました。
次回はこれらをDBUnitを使ったテストケースの実装を紹介していきます。