使用Spring Boot和MyBatis创建一个Hello World的应用程序
怎么了? le?)
我们来尝试在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);
}
本次,我们将使用注解而不是XML文件来创建Mapper接口。
对于像BlogMapper这样的映射器类,还有一种更巧妙的方法。它们的映射语句根本不需要通过XML进行映射,而是可以使用Java注解。
如果给予@Mapper注解,MyBatis Spring Boot Starter会自动检测。
@Mapper
public interface PersonMapper {
MyBatis-Spring-Boot-Starter默认会搜索用@Mapper注解标记的映射器。
插入文本。
@Insert("insert into person(first_name, last_name, age) values(#{firstName}, #{lastName}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Person person);
要将参数嵌入SQL中,可以使用#{变量名}来表示。
另外,由於這次使用了PostgreSQL的序列類型(serial),因此是自動編號。
要使用此选项,需要将useGeneratedKeys设置为true,然后使用keyProperty属性将编号结果映射到模型中。
映射器的 XML 文件/插入、更新和删除
在这个情况下,我们将使用@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(默认值)。
从3.4.3版本开始,通过指定每个参数的名称,可以以任意顺序编写arg元素。要通过参数名称引用构造函数参数,您可以给它们添加@Param注解,也可以使用’-parameters’编译器选项编译项目并启用useActualParamName(默认情况下已启用此选项)。
映射器XML文件/结果映射
此次项目是使用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()));
}
}
插入数据后,我分别执行了准备好的SELECT语句。
最后是主要的类,即 main class。
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,获取查询结果时不会设置带有下划线的列的值。
映射器XML文件/自动映射
除此之外,还可以设置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
选择文也可以的嘛。
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
这次应该就是这样了吧。