SpringBoot教程(十一) | SpringBoot集成Mybatis

2022-04-08 17:05:11 浏览数 (1)

上一篇文章我们介绍了SpringBoot集成JdbcTemplate.简单体验了一下JdbcTemplate框架的用法,今天的内容比较重要,我们来介绍一下SpringBoot集成Mybatis的步骤。

1、 Mybatis 介绍

MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAO)。

MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手工设置参数以及抽取结果集。MyBatis 使用简单的 XML 或注解来配置和映射基本体,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

Mybatis特点:

1、Mybatis实现了接口绑定,使用更加方便。

2、对象关系映射的改进,效率更高

3、MyBatis采用功能强大的基于OGNL的表达式来消除其他元素。

优点:

1、简单易学

mybatis本身就很小且简单。没有任何第三方依赖,最简单安装只要两个jar文件 配置几个sql映射文件易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。

2、灵活

mybatis不会对应用程序或者数据库的现有设计强加任何影响。 sql写在xml里,便于统一管理和优化。通过sql基本上可以实现我们不使用数据访问框架可以实现的所有功能,或许更多。

3、解除sql与程序代码的耦合

通过提供DAL层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。

4、提供映射标签,支持对象与数据库的orm字段关系映射

5、提供对象关系映射标签,支持对象关系组建维护

6、提供xml标签,支持编写动态sql。

缺点:

1、编写SQL语句时工作量很大,尤其是字段多、关联表多时,更是如此。

2、SQL语句依赖于数据库,导致数据库移植性差,不能更换数据库。

3、框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。

4、二级缓存机制不佳

上面的内容都是从网上拷贝的,因为我想大多数的人应该都用过mybatis,并且我们本文的侧重点主要是SpringBoot的集成方式,而不是从头介绍Mybatis,如果大家对Mybatis的使用不太了解,建议先去学习一下Mybatis的用法。

2、集成步骤

接下来我们开始进行集成。为了方便操作,由于我们上次刚刚集成过JdbcTemplate, 代码中共存多个DAO层框架可能会有问题,我们在原有项目基础上拉取一个新的分支来进行开发,分支名就叫 feature/mybaits。完成的代码都会托管到gitCode上,大家可在文末获取地址。

2.1 引入依赖

首先我们先引入Mybatis所需依赖,mybatis本身已经提供了用于适配springBoot的Starter, 同时我们还需要引入 mysql-connector. 在pom.xml中添加:

代码语言:javascript复制
<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
    <scope>provided</scope>
</dependency>

<!-- mybatis-spring-boot-starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.1</version>
</dependency>

<!-- MySQL连接 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

2.2 配置数据库连接

在spring的配置文件中,配置我们需要访问的数据库的连接信息,这个配置和前面jdbcTemplate的配置一样

代码语言:javascript复制
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_learning
    username: root
    password: root

2.3 开发Mapper

在JdbcTemplate中我们访问数据库的这一层使用dao表示的。但是在mybatis中,我们一般都把这一层称之为mapper, 并且一般类名也用这个结尾,其实代表的都是一个意思,就是使用习惯的问题。在Mybatis中的Mapper也是分为接口和实现,比较特殊的是mapper的实现一般使用xml文件的形式来体现。我们的sql也都是写在xml文件中。

我们在项目中创建一个mapper的文件夹,用来存放所有的Mapper接口。我们在里边创建UserMapper用来处理user表的增删改查操作。

代码语言:javascript复制
@Mapper
public interface UserMapper {
    /**
     * 删除操作
     * @param id
     * @return
     */
    int deleteByPrimaryKey(Integer id);

    /**
     * 插入操作
     * @param record
     * @return
     */
    int insert(User record);

    /**
     * 插如操作
     * @param record
     * @return
     */
    int insertSelective(User record);

    /**
     * 根据id查询操作
     * @param id
     * @return
     */
    User selectByPrimaryKey(Integer id);

    /**
     * 更新操作
     * @param record
     * @return
     */
    int updateByPrimaryKeySelective(User record);

    /**
     * 更新操作
     * @param record
     * @return
     */
    int updateByPrimaryKey(User record);
}

然后在resources 资源目录下(application.yml同级目录下)创建一个mapper文件夹,用于存放xml格式的mapper实现。在里面写一个UserMapper.xml

代码语言:javascript复制
<?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">
<mapper namespace="com.lsqingfeng.springboot.mapper.UserMapper">

    <resultMap id="BaseResultMap" type="com.lsqingfeng.springboot.entity.User">
        <id column="id" jdbcType="INTEGER" property="id" />
        <result column="name" jdbcType="VARCHAR" property="name" />
        <result column="age" jdbcType="INTEGER" property="age" />
        <result column="address" jdbcType="VARCHAR" property="address" />
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
    </resultMap>


    <!--sql语句片段,将公共部分抽出-->
    <sql id="Base_Column_List">
        id, name, age,address,create_time, update_time
    </sql>

    <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from t_user
        where id = #{id,jdbcType=INTEGER}
    </select>

    <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
        delete from t_user
        where id = #{id,jdbcType=INTEGER}
    </delete>

    <insert id="insert" parameterType="com.lsqingfeng.springboot.entity.User">
        insert into t_user ( name, age,address,create_time ,update_time
        )
        values (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER},#{address},#{createTime,jdbcType=TIMESTAMP},#{updateTime,jdbcType=TIMESTAMP}
               )
    </insert>


    <!--动态sql-->
    <insert id="insertSelective" parameterType="com.lsqingfeng.springboot.entity.User">
        insert into t_user
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="id != null">
                id,
            </if>
            <if test="name != null">
                name,
            </if>
            <if test="age != null">
                age,
            </if>
            <if test="address != null">
                address,
            </if>
            <if test="createTime != null">
                create_time,
            </if>
            <if test="updateTime != null">
                update_time,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="id != null">
                #{id,jdbcType=INTEGER},
            </if>
            <if test="name != null">
                #{name,jdbcType=VARCHAR},
            </if>
            <if test="age != null">
                #{age,jdbcType=INTEGER},
            </if>
            <if test="address != null">
                #{address},
            </if>
            <if test="updateTime != null">
                #{createTime,jdbcType=TIMESTAMP},
            </if>
            <if test="updateTime != null">
                #{updateTime,jdbcType=TIMESTAMP},
            </if>
        </trim>
    </insert>


    <update id="updateByPrimaryKeySelective" parameterType="com.lsqingfeng.springboot.entity.User">
        update t_user
        <set>
            <if test="name != null">
                name = #{name,jdbcType=VARCHAR},
            </if>
            <if test="age != null">
                age = #{age,jdbcType=INTEGER},
            </if>
            <if test="address != null">
                address = #{address},
            </if>
            <if test="createTime != null">
                create_time = #{createTime,jdbcType=TIMESTAMP},
            </if>
            <if test="age != null">
                update_time = #{updateTime,jdbcType=TIMESTAMP}
            </if>
        </set>
        where id = #{id,jdbcType=INTEGER}
    </update>

    <update id="updateByPrimaryKey" parameterType="com.lsqingfeng.springboot.entity.User">
        update t_user
        set name = #{name,jdbcType=VARCHAR},
            age = #{age,jdbcType=INTEGER},
            address = #{address,jdbcType=VARCHAR},
            create_time = #{createTime,jdbcType=TIMESTAMP},
            update_time = #{updateTime,jdbcType=TIMESTAMP}
        where id = #{id,jdbcType=INTEGER}
    </update>
</mapper>

然后在Service层中引用mapper.

2.4 配置Mapper的路径

我们现在是把xml类型的文件都放在了resources下的mapper文件夹中了。但是这个路径对与我们的项目中的mybatis来说他是不知道的,所以我们需要告诉它这个路径的位置。怎么告诉呢,就是在application.yml中配置一下。

代码语言:javascript复制
mybatis:
  mapper-locations: classpath:mapper/*.xml

mapper的位置位于 classpath下的mapper文件夹中的所有xml结尾的文件。

这里还有一个问题也提下。就是mapper的接口的位置我们并没有配置,他是怎么知道的呢,其实是我们在每个Mapper的接口上都加上了@Mapper的注解,所以他可以自动扫描到。如果要加这个注解,那么所有的Mapper接口上就都需要加,这其实是比较麻烦的。怎么办呢,我们可以都不加这个注解,然后在SpringBoot的启动类上,加上一个MapperScan注解,将mapper的包路径配置进去,就都不用加了。

我们去掉@Mapper注解,也不加任何配置会报错:

我们在主类上添加注解:

代码语言:javascript复制
@SpringBootApplication
@MapperScan("com.lsqingfeng.springboot.mapper")
public class SpringBootLearningApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootLearningApplication.class, args);
    }
}

再次启动:

成功了。

3、 接口测试

组装好项目后,我们来调用一下接口。继续使用之前的jdbc里写好的Controller进行测试。

调用成功后,观察数据库。

李四这条使我们刚刚插入进去的,只不过两个时间是空值,因为我们并没有给时间赋值。

再看看查询接口。

4、mybatis自动填充时间

上面的案例中,由于我们没有对时间进行设置,导致创建时间和修改时间都是空的。其实Mybatis中为我们提供了拦截器的机制,相当于可以对每次执行的sql进行拦截,这样我们就可以对对操作进行判断,如果是插入操作,就直接设置创建时间和修改时间为当前时间。如果是更新操作则设置更新时间为当前时间。

拦截器代码如下。

代码语言:javascript复制
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Date;

/**
 * Mybatis拦截器,用能与设置创建时间和更新时间
 */
@Component
@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }) })
public class CreateTimeSetInterceptor implements Interceptor {

  private static final String CREATE_TIME_SETTER = "setCreateTime";
  private static final String UPDATE_TIME_SETTER = "setUpdateTime";

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
    Object parameter = invocation.getArgs()[1];

    if (ms.getSqlCommandType() == SqlCommandType.INSERT) {
      setTimeIfNeccesary(parameter, CREATE_TIME_SETTER);
      setTimeIfNeccesary(parameter, UPDATE_TIME_SETTER);
    } else if (ms.getSqlCommandType() == SqlCommandType.UPDATE) {
      setTimeIfNeccesary(parameter, UPDATE_TIME_SETTER);
    }
    return invocation.getMethod().invoke(invocation.getTarget(), invocation.getArgs());
  }

  private void setTimeIfNeccesary(Object param, String methodName) {
    Class<?> cls = param.getClass();

    if (cls == ParamMap.class) {
      @SuppressWarnings("unchecked")
      ParamMap<Object> map = (ParamMap<Object>) param;
      map.entrySet().forEach(entry -> {
        if (!entry.getKey().equals("et")) {
          setIfSetterExist(entry.getValue(), methodName);
        }
      });
    } else {
      setIfSetterExist(param, methodName);
    }
  }

  private void setIfSetterExist(Object param, String methodName) {
    Class<?> cls = param.getClass();
    try {
      Method m = null;
      try {
        m = cls.getDeclaredMethod(methodName, new Class[] { Date.class });
        if (m != null) {
          m.setAccessible(true);
          m.invoke(param, new Date());
        }
      } catch (NoSuchMethodException e1) {
        m = cls.getDeclaredMethod(methodName, new Class[] { Timestamp.class });
        if (m != null) {
          m.setAccessible(true);
          m.invoke(param, new Timestamp(System.currentTimeMillis()));
        }
      }
    } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
        | InvocationTargetException e) {
      e.printStackTrace();
    }
  }

}

接下来我们再来执行一个插入操作》

再次观察数据库:

发现时间已经有值了。

5、 mybatis-generator 插件

我们上面通过User表创建了UserMapper接口和UserMapper的xml实现。其实这类文件中的内容是比较有规律的,所以如果每次没有都去自己写,是比较耗费时间的,所有mybatis为我们提供了mybatis-generator插件,通过这个插件,我们可以设置对应的数据库连接和表名,然后插件就会帮我们自动生成对应的实体,mapper接口和Mapper实现,里面的方法只有常用的增删改查操作。能够大大较少我们操作的工作量。 这个步骤,我们一般称之为 mybatis 逆向生成。 逆向生成的方法比较多,有兴趣的自己去找找吧,这里不想展开了,因为现在随着mybatisPlus的普及,使用mybatis-plus-generator的更多一些。

好了关于SpringBoot集成Mybatis我们就介绍这么多。欢迎大家一起交流,有问题随时留言。

另: 配套项目代码已托管中gitCode: gitcode.net/lsqingfeng/…

0 人点赞