Spring Boot - 整合 MyBatis

前言

当前基本上对数据库的操作,都会使用 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 的执行机制大致如下:

  1. 首先它会自动检测存在的DataSource
  2. 然后将DataSource传入给SqlSessonFactoryBean,创建并注入一个SqlSessionFactory实例
  3. 然后从SqlSessionFactory中创建并注册一个SqlSessionTemplate
  4. 最后会自动扫描项目中定义的Mappers,将它们关联到SqlSessionTemplate,同时会将这些Mappers注入到 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 对该表进行增、删、改、查操作,具体步骤如下:

  1. 首先导入相关依赖,具体内容参考上文

  2. 在配置文件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

  3. 创建表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 +
                    '}';
        }
    

    其中,GenderEnum类型,源码如下:

    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手动设置了序列化字段名称,一律采用小写进行表示。

  4. 创建类型转换器,用于处理字段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设置为全局默认转换器即可,具体配置请参考附录:配置全局默认转换器

  5. 创建数据表操作接口:

    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);
        }
    
    }
    
  6. 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

  1. 首先创建一个服务接口IUserService,定义数据库业务操作行为:

    public interface IUserService {
        void addUser(User user);
    
        void deleteUser(long id);
    
        void updateUser(User user);
    
        List<User> getAllUser();
    }
    
  2. 创建一个业务服务接口实现类,并注入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();
        }
    }
    
  3. 创建 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 配置。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容