Spring Boot＋MyBatisでHello World

# What's？

Spring Boot環境下で、MyBatisのHello World的なことをやってみよう、ということで。

# 環境

今回の環境。

```shell
$ java --version
openjdk 11.0.15 2022-04-19
OpenJDK Runtime Environment (build 11.0.15+10-Ubuntu-0ubuntu0.20.04.1)
OpenJDK 64-Bit Server VM (build 11.0.15+10-Ubuntu-0ubuntu0.20.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: /home/charon/.sdkman/candidates/maven/current
Java version: 11.0.15, vendor: Private Build, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-109-generic", arch: "amd64", family: "unix"
```

データベースは、PostgreSQL 14.2を使うことにします。Dockerで用意。

```shell
$ docker container run \
  -it --rm --name postgres \
  -p 5432:5432 \
  -e POSTGRES_DB=example \
  -e POSTGRES_USER=charon \
  -e POSTGRES_PASSWORD=password \
  postgres:14.2-bullseye
```

# Spring Boot＋MyBatisプロジェクトを作成する

Spring Initializrを使って、MyBatisが使えるようにしたSpring Bootプロジェクトを作成します。

```shell
$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.6.7 \
  -d javaVersion=11 \
  -d name=spring-hello-mybatis \
  -d groupId=com.example \
  -d artifactId=hello-mybatis \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=com.example.spring.mybatis \
  -d dependencies=mybatis,postgresql \
  -d baseDir=hello-mybatis | tar zxvf -
```

ディレクトリ内に移動。

```shell
$ cd hello-mybatis
```

自動生成されたソースコードは、今回は使わないので削除。

```shell
$ rm src/main/java/com/example/spring/mybatis/SpringHelloMybatisApplication.java src/test/java/com/example/spring/mybatis/SpringHelloMybatisApplicationTests.java
```

依存関係やプラグインの設定なども見ておきます。

```xml
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.2.2</version>
		</dependency>

		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
```

`mvn dependency:tree`で見ると、`spring-boot-starter`や`spring-boot-starter-jdbc`も含まれているようなので、最低限であれば`mybatis-spring-boot-starter`だけで良さそうですね。

```shell
[INFO] +- org.mybatis.spring.boot:mybatis-spring-boot-starter:jar:2.2.2:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:2.6.7:compile
[INFO] |  |  +- org.springframework.boot:spring-boot:jar:2.6.7:compile
[INFO] |  |  |  \- org.springframework:spring-context:jar:5.3.19:compile
[INFO] |  |  |     +- org.springframework:spring-aop:jar:5.3.19:compile
[INFO] |  |  |     \- org.springframework:spring-expression:jar:5.3.19:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-autoconfigure:jar:2.6.7:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:2.6.7:compile
[INFO] |  |  |  +- ch.qos.logback:logback-classic:jar:1.2.11:compile
[INFO] |  |  |  |  \- ch.qos.logback:logback-core:jar:1.2.11:compile
[INFO] |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
[INFO] |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] |  |  |  \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile
[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] |  |  \- org.yaml:snakeyaml:jar:1.29:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.6.7:compile
[INFO] |  |  +- com.zaxxer:HikariCP:jar:4.0.3:compile
[INFO] |  |  \- org.springframework:spring-jdbc:jar:5.3.19:compile
[INFO] |  |     +- org.springframework:spring-beans:jar:5.3.19:compile
[INFO] |  |     \- org.springframework:spring-tx:jar:5.3.19:compile
[INFO] |  +- org.mybatis.spring.boot:mybatis-spring-boot-autoconfigure:jar:2.2.2:compile
[INFO] |  +- org.mybatis:mybatis:jar:3.5.9:compile
[INFO] |  \- org.mybatis:mybatis-spring:jar:2.0.7:compile
```

# ソースコードを作成する

最初に、テーブルを決めましょう。今回は、こちらをお題にします。

```sql:src/main/resources/schema.sql
drop table if exists person;

create table person (
  id serial,
  first_name varchar(20),
  last_name varchar(20),
  age integer,
  primary key(id)
);
```

次に、Javaのソースコードを書いていきます。

MyBatisに関しては、このあたりを参考にしていきます。

https://mybatis.org/mybatis-3/getting-started.html

https://mybatis.org/mybatis-3/sqlmap-xml.html

https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

テーブルにマッピングするモデル。

```java:src/main/java/com/example/spring/mybatis/Person.java
package com.example.spring.mybatis;

public class Person {
    Long id;
    String firstName;
    String lastName;
    Integer age;

    public static Person create(String firstName, String lastName, Integer age) {
        Person person = new Person();

        person.setFirstName(firstName);
        person.setLastName(lastName);
        person.setAge(age);

        return person;
    }

    // getter、setterは省略
}
```

次に、Mapperインターフェースを作成。

```java:src/main/java/com/example/spring/mybatis/PersonMapper.java
package com.example.spring.mybatis;

import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface PersonMapper {
    @Insert("insert into person(first_name, last_name, age) values(#{firstName}, #{lastName}, #{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Person person);

    @Select("select id, first_name, last_name, age from person order by id")
    List<Person> findAll();

    @Select("select id, first_name, last_name, age from person where last_name = #{lastName}")
    List<Person> findByLastName(String lastName);

    @Select("select id, first_name, last_name, age from person where last_name = #{lastName} and age > #{age}")
    List<Person> findByLastNameAndAge(Person person);

    @Select("select id, first_name, last_name, age from person where last_name = #{lastName} and age > #{age}")
    List<Person> findByLastNameAndAgePrimitive(String lastName, int age);
}
```

今回は、`Mapper`インターフェースはXMLファイルではなくアノテーションを使って作成します。

> There's one more trick to Mapper classes like BlogMapper. Their mapped statements don't need to be mapped with XML at all. Instead they can use Java Annotations.

https://mybatis.org/mybatis-3/getting-started.html

`@Mapper`アノテーションを付与しておけば、MyBatis Spring Boot Starterが自動検出してくれます。

```java
@Mapper
public interface PersonMapper {
```

> The MyBatis-Spring-Boot-Starter will search, by default, for mappers marked with the @Mapper annotation.

https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

`insert`文。

```java
    @Insert("insert into person(first_name, last_name, age) values(#{firstName}, #{lastName}, #{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Person person);
```

SQLにパラメーターを埋め込むには、`#{変数名}`表記を使います。

また、今回`id`はPostgreSQLの`serial`型を使用しているので自動採番です。

こちらを使うには`useGeneratedKeys`を`true`にして、さらにモデルに反映するには`keyProperty`で採番結果を反映するプロパティを指定します。

[Mapper XML Files / insert, update and delete](https://mybatis.org/mybatis-3/sqlmap-xml.html#insert_update_and_delete)

この時、アノテーションとしては`@Options`を使用します。

あとは、いくつかの`select`文のバリエーションです。

```java
    @Select("select id, first_name, last_name, age from person order by id")
    List<Person> findAll();

    @Select("select id, first_name, last_name, age from person where last_name = #{lastName}")
    List<Person> findByLastName(String lastName);

    @Select("select id, first_name, last_name, age from person where last_name = #{lastName} and age > #{age}")
    List<Person> findByLastNameAndAge(Person person);

    @Select("select id, first_name, last_name, age from person where last_name = #{lastName} and age > #{age}")
    List<Person> findByLastNameAndAgePrimitive(String lastName, int age);
```

最後の複数パラメーターの例は確認のために入れているのですが、引数が複数ある場合は`@Param`で明示的に名前を指定するか、コンパイル時に`-parameters`オプションを指定しつつ`useActualParamName`を`true`（デフォルト）とする必要があるようです。

> Since 3.4.3, by specifying the name of each parameter, you can write arg elements in any order. To reference constructor parameters by their names, you can either add @Param annotation to them or compile the project with '-parameters' compiler option and enable useActualParamName (this option is enabled by default).

[Mapper XML Files / Result Maps](https://mybatis.org/mybatis-3/sqlmap-xml.html#Result_Maps)

今回はプロジェクトをSpring Initializrで作成したので`spring-boot-starter-parent`が親`pom`となっており、`-parameters`が指定されている状態になっているので、この前提のまま進みます。

https://github.com/spring-projects/spring-boot/blob/v2.6.7/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle#L78-L84

この`Mapper`インターフェースを使うクラスを作成。

```java:src/main/java/com/example/spring/mybatis/Runner.java
package com.example.spring.mybatis;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class Runner implements ApplicationRunner {
    Logger logger = LoggerFactory.getLogger(Runner.class);
    PersonMapper personMapper;

    public Runner(PersonMapper personMapper) {
        this.personMapper = personMapper;
    }

    @Transactional
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // insert
        Person maruko = Person.create("ももこ", "さくら", 9);
        Person sakiko = Person.create("さきこ", "さくら", 11);
        Person tamae = Person.create("たまえ", "穂波", 9);

        personMapper.insert(maruko);
        personMapper.insert(sakiko);
        personMapper.insert(tamae);

        List
                .of(maruko, sakiko, tamae)
                .forEach(p -> logger.info("inserted: id = {}, first_name = {}, last_name = {}, age = {}", p.getId(), p.getFirstName(), p.getLastName(), p.getAge()));

        // select all
        List<Person> allPeople = personMapper.findAll();
        allPeople.forEach(p -> logger.info("find all: id = {}, first_name = {}, last_name = {}, age = {}", p.getId(), p.getFirstName(), p.getLastName(), p.getAge()));

        // select by last_name
        List<Person> sakuraFamily = personMapper.findByLastName("さくら");
        sakuraFamily.forEach(p -> logger.info("find by last_name: id = {}, first_name = {}, last_name = {}, age = {}", p.getId(), p.getFirstName(), p.getLastName(), p.getAge()));

        // select by object
        List<Person> results = personMapper.findByLastNameAndAge(Person.create(null, "さくら", 10));
        results.forEach(p -> logger.info("find by object: id = {}, first_name = {}, last_name = {}, age = {}", p.getId(), p.getFirstName(), p.getLastName(), p.getAge()));

        // select by primitives
        List<Person> results2 = personMapper.findByLastNameAndAgePrimitive("さくら", 10);
        results2.forEach(p -> logger.info("find by primitives: id = {}, first_name = {}, last_name = {}, age = {}", p.getId(), p.getFirstName(), p.getLastName(), p.getAge()));
    }
}
```

データを`insert`した後に、用意した`select`文をそれぞれ実行しています。

最後に`main`クラス。

```java:src/main/java/com/example/spring/mybatis/App.java
package com.example.spring.mybatis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class);
    }
}
```

# 設定をする

設定もしておきましょう。

`spring.datasource.*`プロパティで、用意したPostgreSQLへの接続情報を設定。

```properties:src/main/resources/application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/example
spring.datasource.username=charon
spring.datasource.password=password

spring.sql.init.mode=always

mybatis.configuration.map-underscore-to-camel-case=true
```

`spring.sql.init.mode`を`always`にしているのは、`schema.sql`を常に実行するためです。

`mybatis.configuration.map-underscore-to-camel-case`を`true`にしているのは、

`Mapper`インターフェースで作成していたクエリーの結果のカラム名を、キャメルケースのJavaのプロパティにマッピングするためのものです。

```java
    @Select("select id, first_name, last_name, age from person order by id")
    List<Person> findAll();
```

こういう書き方の場合、`mybatis.configuration.map-underscore-to-camel-case`を`true`にしておかないとクエリーの結果を取得する際に、このようなアンダースコア入りの列の値が設定されません。

[Mapper XML Files / Auto-mapping](https://mybatis.org/mybatis-3/sqlmap-xml.html#Auto-mapping)

https://mybatis.org/mybatis-3/configuration.html

他にも`resultMap`を設定するといった方法もありますが、今回はこちらの方針でいきます。

# 実行する

では、実行してみます。

```shell
$ mvn spring-boot:run
```

結果のログ。

```shell
2022-04-30 21:14:34.597  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : inserted: id = 1, first_name = ももこ, last_name = さくら, age = 9
2022-04-30 21:14:34.598  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : inserted: id = 2, first_name = さきこ, last_name = さくら, age = 11
2022-04-30 21:14:34.598  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : inserted: id = 3, first_name = たまえ, last_name = 穂波, age = 9
2022-04-30 21:14:34.605  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : find all: id = 1, first_name = ももこ, last_name = さくら, age = 9
2022-04-30 21:14:34.606  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : find all: id = 2, first_name = さきこ, last_name = さくら, age = 11
2022-04-30 21:14:34.606  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : find all: id = 3, first_name = たまえ, last_name = 穂波, age = 9
2022-04-30 21:14:34.608  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : find by last_name: id = 1, first_name = ももこ, last_name = さくら, age = 9
2022-04-30 21:14:34.608  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : find by last_name: id = 2, first_name = さきこ, last_name = さくら, age = 11
2022-04-30 21:14:34.610  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : find by object: id = 2, first_name = さきこ, last_name = さくら, age = 11
2022-04-30 21:14:34.612  INFO 30274 --- [           main] com.example.spring.mybatis.Runner        : find by primitives: id = 2, first_name = さきこ, last_name = さくら, age = 11
```

`insert`文の実行後に、`serial`で採番された`id`がモデルに反映されています。

```shell
inserted: id = 1, first_name = ももこ, last_name = さくら, age = 9
inserted: id = 2, first_name = さきこ, last_name = さくら, age = 11
inserted: id = 3, first_name = たまえ, last_name = 穂波, age = 9
```

`select`文もOKそうですね。

```shell
find all: id = 1, first_name = ももこ, last_name = さくら, age = 9
find all: id = 2, first_name = さきこ, last_name = さくら, age = 11
find all: id = 3, first_name = たまえ, last_name = 穂波, age = 9


find by last_name: id = 1, first_name = ももこ, last_name = さくら, age = 9
find by last_name: id = 2, first_name = さきこ, last_name = さくら, age = 11


find by object: id = 2, first_name = さきこ, last_name = さくら, age = 11

find by primitives: id = 2, first_name = さきこ, last_name = さくら, age = 11
```

今回はこんなところでしょうか。