What's?
Spring Boot環境下で、MyBatisのHello World的なことをやってみよう、ということで。
環境
今回の環境。
$ 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で用意。
$ 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プロジェクトを作成します。
$ 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 -
ディレクトリ内に移動。
$ cd hello-mybatis
自動生成されたソースコードは、今回は使わないので削除。
$ rm src/main/java/com/example/spring/mybatis/SpringHelloMybatisApplication.java src/test/java/com/example/spring/mybatis/SpringHelloMybatisApplicationTests.java
依存関係やプラグインの設定なども見ておきます。
<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
だけで良さそうですね。
[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
ソースコードを作成する
最初に、テーブルを決めましょう。今回は、こちらをお題にします。
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に関しては、このあたりを参考にしていきます。
テーブルにマッピングするモデル。
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インターフェースを作成。
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.
@Mapper
アノテーションを付与しておけば、MyBatis Spring Boot Starterが自動検出してくれます。
@Mapper
public interface PersonMapper {
The MyBatis-Spring-Boot-Starter will search, by default, for mappers marked with the @Mapper annotation.
insert
文。
@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
この時、アノテーションとしては@Options
を使用します。
あとは、いくつかのselect
文のバリエーションです。
@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
今回はプロジェクトをSpring Initializrで作成したのでspring-boot-starter-parent
が親pom
となっており、-parameters
が指定されている状態になっているので、この前提のまま進みます。
このMapper
インターフェースを使うクラスを作成。
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
クラス。
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への接続情報を設定。
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のプロパティにマッピングするためのものです。
@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
他にもresultMap
を設定するといった方法もありますが、今回はこちらの方針でいきます。
実行する
では、実行してみます。
$ mvn spring-boot:run
結果のログ。
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
がモデルに反映されています。
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そうですね。
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
今回はこんなところでしょうか。