前言
当前基本上对数据库的操作,都会使用 ORM 框架。
在 Java 后端方面,ORM 框架主要有两类:Hibernate 和 MyBatis:
Hibernate:Hibernate 拥有良好的映射机制,开发者基本无需书写 Sql 语句与结果映射,直接调用相应方法即可操作数据库。当前使用最多的就是 Spring Data Jpa 这种开发模式。
Hiberntate 的优点是开发效率高,缺点是对大型项目来说,缺少了一定的灵活性。MyBatis:MyBatis 对数据库进行操作,需要开发者手动书写对应 Sql 语句,以及维护结果映射。初期配置相对麻烦,但是好处是由于 Sql 语句都是自己定制的,因此其可以进行更细致化的 Sql 优化,在数据库复杂操作场景下,其效率与灵活性会更高。
简单来说,Hibernate 是全自动 ORM 框架,而 MyBatis 是一个半自动 ORM 框架。
本篇博文主要介绍下如何在 Spring Boot 中集成 MyBatis。
注:对于 MyBatis 相关内容,可参考文章:MyBatis 简明教程
依赖导入
在 Spring Boot 中使用 MyBatis,使用的起步依赖为:MyBatis-Spring-Boot-Starter,如下在pom.xml
中进行导入:
<dependencies>
<!-- MyBatis 起步依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 数据库连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
执行机制
MyBatis-Spring-Boot-Starter 的执行机制大致如下:
- 首先它会自动检测存在的
DataSource
- 然后将
DataSource
传入给SqlSessonFactoryBean
,创建并注入一个SqlSessionFactory
实例 - 然后从
SqlSessionFactory
中创建并注册一个SqlSessionTemplate
- 最后会自动扫描项目中定义的
Mapper
s,将它们关联到SqlSessionTemplate
,同时会将这些Mapper
s注入到 Spring 容器中,方便后续在其他地方进行使用。
示例
下面通过一个示例,来驱动阐述 Spring Boot 集成 MyBatis 具体操作步骤:
注:MyBatis 同时提供了「注解配置」 和 「XML 配置」,本文主要介绍基于 XML 配置操作方法。
假设现在我们有如下一张用户表:
CREATE TABLE IF NOT EXISTS `tb_user` (
`id` BIGINT AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL UNIQUE COMMENT 'user name',
`gender` ENUM('male','female') DEFAULT 'male',
PRIMARY KEY(`id`)
);
我们希望使用 MyBatis 对该表进行增、删、改、查操作,具体步骤如下:
首先导入相关依赖,具体内容参考上文
-
在配置文件
application.yml
中配置数据库和 MyBatis 相关信息:spring: # 数据相关配置 datasource: url: jdbc:mysql://localhost:3306/db_test?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 # MyBatis 相关配置 mybatis: # MyBatis 全局配置文件 config-location: classpath:mybatis-config.xml # Mapper.xml 配置文件 mapper-locations: classpath:mapper/**/*.xml # 开启 MyBatis 日志 logging: level: # 捕获指定包日志 com: yn: mybatisintegrationdemo: dao: debug
其中:
mapper-locations
:用于配置指定 Sql 映射文件(即 Mapper.xml)存放路径。上面配置将其放置到resource/mapper/
路径下。-
config-location
:用于指定 MyBatis 全局配置文件。上述配置将全局配置文件指定为:resource/mybatis-config.xml
,其内容如下:<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://www.mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!-- 字段属性名下划线自动转驼峰 --> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <typeAliases> <!-- type指定类全限定名,alias指定别名,且别名不区分大小写--> <typeAlias type="com.yn.mybatisintegrationdemo.entity.User" alias="user" /> <!-- 类名即为别名,且不区分大小写--> <package name="com.yn.mybatisintegrationdemo.entity"/> </typeAliases> <typeHandlers> <!-- 配置类型转换器 --> <typeHandler handler="com.yn.mybatisintegrationdemo.handler.GenderTypeHandler" javaType="com.yn.mybatisintegrationdemo.enums.Gender"/> </typeHandlers> </configuration>
注:上述还配置了一个类型转换器
GenderTypeHandler
,主要是因为表tb_user
中有一个Enum
字段gender
,因此这里需要自定义一个转换器,用来解析转换gender
字段,具体内容参见后文。
其他更多 MyBatis 配置选项,请参考:MyBatis-Spring-Boot-Starter - Configuration
-
创建表
tb_user
对应的 POJO 类:public class User implements Serializable { // tb_user.id private Long id; // tb_user.name private String name; // tb_user.gender private Gender gender; // getters // setters @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", gender=" + gender + '}'; }
其中,
Gender
为Enum
类型,源码如下:public enum Gender { @JsonProperty("male") MALE("male"), @JsonProperty("female") FEMALE("female"); private String gender; Gender(String gender) { this.gender = gender; } @Override public String toString() { // 直接把字符串返回 return this.gender; } }
注:默认情况下,Jackson 对
Enum
类型的序列化,其键值采用的是Enum#name()
方法,因此返回的是大写字符串名称,而进行反序列化时,如果我们传递的是小写字母,则反序列化会失败。上述源码采用了@JsonProperty
手动设置了序列化字段名称,一律采用小写进行表示。 -
创建类型转换器,用于处理字段
gender
的 java 类型 和 jdbc 类型转换:public class GenderTypeHandler extends BaseTypeHandler<Gender> { private Gender selectGender(String gender) { return Arrays.stream(Gender.values()) .filter(item -> item.toString().equals(gender)) .findFirst() .orElse(Gender.MALE); } // 把 Java 类型参数(paramter)转换为 jdbc 类型 @Override public void setNonNullParameter(PreparedStatement ps, int i, Gender parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.toString()); } // 通过字段名 columnName 从结果集 rs 中获取到数据,将该数据转换为对应的 Java 类型 @Override public Gender getNullableResult(ResultSet rs, String columnName) throws SQLException { String gender = rs.getString(columnName); return this.selectGender(gender); } // 通过字段索引 columnIndex 从结果集 rs 中获取到数据,将该数据转换为对应的 Java 类型 @Override public Gender getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String gender = rs.getString(columnIndex); return this.selectGender(gender); } // 通过字段索引 columnIndex 从存储过程中获取到数据,将该数据转换为对应的 Java 类型 @Override public Gender getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String gender = cs.getString(columnIndex); return this.selectGender(gender); } }
类型转换器创建完成后,还需注册到配置文件
mybatis-config.xml
中,具体配置详情请参考上文内容。注:通常系统中会存在多处需要类型转换的字段,这些字段的转换方式是一样的,比如上述代码的
GenderTypeHandler
,其功能就是将Gender
类型与其对应的字符串描述进行互转,但是当有很多个字段要进行这样互转时,就需要为每个 Java 类型设置该TypeHandler
,配置重复繁琐,此时可以通过将该TypeHandler
设置为全局默认转换器即可,具体配置请参考附录:配置全局默认转换器 -
创建数据表操作接口:
package com.yn.mybatisintegratioindemo.dao; @Mapper @Repository public interface IUserMapper { // 增:添加用户 void addUser(User user); // 删:删除用户 void deleteUserById(long id); // 改:修改用户信息 void updateUserByName(User user); // 查:查询所有用户信息 List<User> selectAll(); }
注:此处需要在
Mapper
接口类上使用注解@Mapper
,表明该接口是一个Mapper
类。当包中Mapper
类比较多的时候,也可以直接在 Spring Boot 启动类上使用注解@MapperScan
,直接指定要扫描的包,这样就无需为每个Mapper
类注解@Mapper
:@SpringBootApplication // 扫描指定包下的 Mapper 类 @MapperScan(basePackages = {"com.yn.mybatisintegratioindemo.dao"}) public class MybatisIntegratioinDemoApplication { public static void main(String[] args) { SpringApplication.run(MybatisIntegratioinDemoApplication.class, args); } }
-
在
resources/mapper/
目录下,创建表tb_user
对应的 Sql 映射文件:resources/mapper/com/yn/mybatisintegrationdemo/dao/IUserMapper.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"> <!--namesapce:指定对应表操作接口--> <mapper namespace="com.yn.mybatisintegrationdemo.dao.IUserMapper"> <!-- 为 POJO 类指定与表字段的映射关系 --> <resultMap id="userMap" type="com.yn.mybatisintegrationdemo.entity.User"> <id property="id" column="id"/> <!-- POJO 属性与表字段名称相同可省略配置 --> <result property="name" column="name"/> <result property="gender" column="gender" javaType="com.yn.mybatisintegrationdemo.enums.Gender"/> </resultMap> <!-- 增 --> <insert id="addUser" parameterType="com.yn.mybatisintegrationdemo.entity.User"> insert into `tb_user`(`name`,`gender`) values(#{name},#{gender}) </insert> <!-- 删 --> <delete id="deleteUserById" parameterType="_long"> delete from `tb_user` where `id` = #{id} </delete> <!-- 改 --> <!-- User 此处使用的是简写,因为在全局配置文件配置了简写 --> <update id="updateUserByName" parameterType="User"> update `tb_user` set `gender`=#{gender} <if test="id != null"> ,`id` = #{id} </if> where `name` = #{name} </update> <!-- 查 --> <select id="selectAll" resultMap="userMap"> select * from `tb_user` </select> </mapper>
以上,就完成了 MyBatis 的配置与操作过程。现在就可以对数据库进行操作了,下面实现一个 RESTful 风格 API 来操作数据表tb_user
:
-
首先创建一个服务接口
IUserService
,定义数据库业务操作行为:public interface IUserService { void addUser(User user); void deleteUser(long id); void updateUser(User user); List<User> getAllUser(); }
-
创建一个业务服务接口实现类,并注入
IUserMapper
:@Service public class UserServiceImpl implements IUserService { @Autowired private IUserMapper userMapper; @Override public void addUser(User user) { this.userMapper.addUser(user); } @Override public void deleteUser(long id) { this.userMapper.deleteUserById(id); } @Override public void updateUser(User user) { this.userMapper.updateUserByName(user); } @Override public List<User> getAllUser() { return this.userMapper.selectAll(); } }
-
创建 RESTful 风格控制器:
@RestController @RequestMapping("/user") public class UserApi { @Autowired private IUserService userService; @PostMapping public String addUser(@RequestBody User user) { this.userService.addUser(user); return "add user done!"; } @DeleteMapping("/{userId}") public String deleteUser(@PathVariable("userId") long userId) { this.userService.deleteUser(userId); return "delete user done!"; } @PutMapping public String udpateUser(@RequestBody User user) { this.userService.updateUser(user); return "update user done!"; } @GetMapping public List<User> getAllUser() { return this.userService.getAllUser(); } }
注:完整示例代码可查看:MyBatis-Demo
现在,我们就可以访问UserApi
提供的接口,实现对数据表tb_user
的增、删、改、查操作了,测试如下:
# 增:增加一个用户 zhangsan
$ curl -X POST 'localhost:8080/user' --data '{"name":"zhangsan","gender":"male"}' --header 'Content-Type: application/json; charset=utf-8'
add user done!%
# 增:再增加一个用户 wangmeimei
$ curl -X POST 'localhost:8080/user' --data '{"name":"wangmeimei","gender":"female"}' --header 'Content-Type: application/json; charset=utf-8'
add user done!%
# 查:查看用户,可以看到之前的添加都成功了
$ curl -X GET 'localhost:8080/user'
[{"id":1,"name":"zhangsan","gender":"male"},{"id":2,"name":"wangmeimei","gender":"female"}]%
# 删:将 wangmeimei 删除掉
$ curl -X DELETE 'localhost:8080/user/2'
delete user done!%
# 查:删除成功
$ curl -X GET 'localhost:8080/user'
[{"id":1,"name":"zhangsan","gender":"male"}]%
# 改:将 zhansan 改成 female
$ curl -X PUT 'localhost:8080/user' --data '{"name":"zhangsan","gender":"female"}' --header 'Content-Type: application/json; charset=utf-8'
update user done!%
# 查:修改成功
$ curl -X GET 'localhost:8080/user'
[{"id":1,"name":"zhangsan","gender":"female"}]%
附录
-
配置全局默认转换器:当系统存在多个字段需要进行类型转换,且其类型转换机制一致情况下,可以考虑配置一个默认的全局转换器,节省配置。
比如,现在将表
tb_user
修改为如下:CREATE TABLE IF NOT EXISTS `tb_user` ( `id` BIGINT AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL UNIQUE COMMENT 'user name', `gender` ENUM('male','female') DEFAULT 'male' comment '性别', `role` ENUM('admin','normal','guest') DEFAULT 'normal' comment '角色', PRIMARY KEY(`id`) );
也就是现在表中有两个
Enum
类型字段,其对应的 Java 类型如下:public enum Gender { @JsonProperty("male") MALE("male"), @JsonProperty("female") FEMALE("female"); private String gender; // ... } public enum Role { @JsonProperty("admin") admin("admin"), @JsonProperty("normal") normal("normal"), @JsonProperty("guest") guest("guest"); private String role; // ... }
这两种类型的转换规则都是一样的:查询数据表时,将字符串转换为对应的
Enum
类型,写数据表时,将Enum
类型转换为对应的字符串类型。
所以我们可以将这两种类型看做同一种类型,这里使用一个接口进行表示即可,如下所示:public interface IEnum2StringConverter { // 将 Enum 类型转换为 String String stringify(); } public enum Gender implements IEnum2StringConverter { // ... @Override public String stringify() { return this.gender; } } public enum Role implements IEnum2StringConverter { // ... @Override public String stringify() { return this.role; } }
然后创建一个转换器,该转换器可以对
IEnum2StringConverter
类型进行转换:// E 是 Enum类型,也是 IEnum2StringConverter 类型 public class Enum2StringHandler<E extends Enum<E> & IEnum2StringConverter> extends BaseTypeHandler<E> { private Class<E> type; // type:enum 类型 public Enum2StringHandler(Class<E> type) { if (type == null) { throw new IllegalArgumentException("Type argument can not be null!"); } this.type = type; } /** * @param typeStr Enum 的字符串描述 * @return 返回 typeStr 对应的 Enum 类型 */ private E getCorrespondingType(String typeStr) { return Arrays.stream(this.type.getEnumConstants()) .filter(item -> item.stringify().equals(typeStr)) .findFirst() .orElse(null); } @Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.stringify()); } @Override public E getNullableResult(ResultSet rs, String columnName) throws SQLException { return this.getCorrespondingType(rs.getString(columnName)); } @Override public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return this.getCorrespondingType(rs.getString(columnIndex)); } @Override public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return this.getCorrespondingType(cs.getString(columnIndex)); } }
此时我们就可以将
Enum2StringHandler
设置为默认转换器,不过该默认转换器对其他类型无法进行转换,所以此时其实还可以进行一层包装,创建一个自动类型切换转换器:public class AutoSwitchTypeHandler<E extends Enum<E> & IEnum2StringConverter> extends BaseTypeHandler<E> { private BaseTypeHandler<E> handler; // type:Enum 类型 public AutoSwitchTypeHandler(Class<E> type) { if (type == null) { throw new IllegalArgumentException("Type argument can not be null!"); } if (IEnum2StringConverter.class.isAssignableFrom(type)) { // 如果是 IEnum2StringConverter 类型,那么就使用 Enum2StringHandler 转换器 this.handler = new Enum2StringHandler<>(type); } else { // 其他类型降级为使用 EnumTypeHandler this.handler = new EnumTypeHandler<>(type); } } @Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { this.handler.setNonNullParameter(ps, i, parameter, jdbcType); } @Override public E getNullableResult(ResultSet rs, String columnName) throws SQLException { return this.handler.getNullableResult(rs, columnName); } @Override public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return this.handler.getNullableResult(rs, columnIndex); } @Override public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return this.handler.getNullableResult(cs, columnIndex); } }
AutoSwitchTypeHandler
对于IEnum2StringConverter
类型,对采用Enum2StringHandler
进行数据转换,而对于其他类型,则采用默认的EnumTypeHandler
,同时,后续如果有其他类型转换,实现一个自定义TypeHandler
,然后仿造Enum2StringHandler
一样注册到AutoSwitchTypeHandler
即可,让自定义TypeHandler
对新类型进行处理转换,这样,基本上对大多数类型我们就都能进行互转了,此时只需将AutoSwitchTypeHandler
注册为默认转换器即可:<!-- mybatis-config.xml --> <configuration> <settings> <setting name="defaultEnumTypeHandler" value="com.yn.mybatisintegrationdemo.handler.AutoSwitchTypeHandler"/> </settings> </configuration>
以上,便完成了整个配置过程。后续,项目中即使新增了一些
Enum
类型,只要他们也同样是进行对应字符串的互转,简单让他们实现IEnum2StringConverter
接口即可,无需进行额外的 XML 配置。