Spring系列-实战篇(4)-你有多了解MyBatis

1. 概念

在我上大学的时候,最流行的JavaEE框架是 SSH (Struts+Spring+Hibernate),现在同学们应该都在学 SSM(Spring+SpringMVC+MyBatis)了。从历史演变来看,Spring是越来越强大,而MyBatis则是顶替了Hibernate的地位。今天的“主角”就是MyBatis。

1.1. ORM的历史演变

我们先聊一聊ORM(Object Relational Mapping),翻译为“对象关系映射”,就是通过实例对象的语法,完成关系型数据库的操作的技术。ORM用于实现面向对象编程语言里不同类型系统的数据之间的转换,其实是创建了一个可在编程语言里使用的"虚拟对象数据库"。

ORM 把数据库映射成对象:

  • 数据库的表(table) --> 类(class)
  • 记录(record,行数据)--> 对象(object)
  • 字段(field)--> 对象的属性(attribute)

基于传统ORM框架的产品有很多,其中就有耳熟能详的Hibernate。ORM通过配置文件,使数据库表和JavaBean类对应起来,提供简便的操作方法,增、删、改、查记录,不再拼写字符串生成sql,编程效率大大提高,同时减少程序出错机率,增强数据库的移植性,方便测试。

但是有些时候我还是喜欢原生的JDBC,因为在某些特殊的应用场景中,对于sql的应用复杂性比较高,或者需要对sql的性能进行优化,这些ORM框架就显得很笨重。Hibernate这类“全自动化”框架,对数据库结构封装的较为完整,这种一站式的解决方案未必适用于所有的业务场景。

幸运的是,不只我一个人有这种感受,很久之前大家开始关注一个叫 iBATIS 的开源项目,它相对传统ORM框架而言更加的灵活,被定义为“半自动化”的ORM框架。2010年,谷歌接管了iBATIS,MyBatis就随之诞生了。虽然2010年我都还没上大学,但很可惜,MyBatis在国内的大火的比较晚,我在校园期间都没有接触过。

1.2. 开启MyBatis

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

MyBatis为半自动化,需要自己书写sql语句,需要自己定义映射。增加了程序员的一些操作,但是带来了设计上的灵活。并且也是支持Hibernate的一些特性,如延迟加载,缓存和映射等,而且随之SSM架构的成熟,MyBatis肯定会被授予有越来越多新的特性。那么接下来就开始 MyBatis 的实战演练吧!

2. MyBatis 基本使用

下面讲解在SpringBoot 中,使用MyBatis的基本操作。

2.1. 基础配置

在SpringBoot中集成 MyBatis 的方式很简单,只需要引用 MyBatis的starter包即可,不过针对不同的数据源,需要导入所依赖的驱动jar包(如:mysql(mysql-connector-java-x.jar)/oracle(ojdbcx.jar)/sql server(sqljdbcx.jar)等)

pom.xml(示例)



    org.mybatis.spring.boot
    mybatis-spring-boot-starter
    1.3.2



    com.oracle
    ojdbc6
    6



    com.alibaba
    druid-spring-boot-starter
    1.1.9

对于相关数据源的连接信息,需要在application.properties中配置,同样提供示例

# Oracle数据库的连接信息
spring.datasource.url=jdbc:oracle:thin:@ip:port/instance
spring.datasource.username=username
spring.datasource.password=password
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver

#mybatis 驼峰式命名映射,可将ResultMap返回值通过驼峰式映射给pojo
mybatis.configuration.map-underscore-to-camel-case=true

#mybatis xml文件路径
mybatis.mapper-locations=classpath:mapper/*Mapper.xml

#开启mybatis dao层的日志
logging.level.com.df.stage.tasktimer.mapper=debug

2.2. 使用MyBatis方式一:xml配置

MyBatis3 之前,需要手动获取SqlSession,并通过命名空间来调用MyBatis方法,比较麻烦。而MyBatis3 就开始支持接口的方式来调用方法,这也成为当前即为普遍的用法,本文就以此为例。

通过在Java中写dao层的 Interface 类,然后与之对应写一个 xml 文件,作为 Interface 的实现,如下:

DfTimerTaskMapper.java

@Mapper
public interface DfTimerTaskMapper {
    /**
     * 查询 df_timer_task 表
     * @param searchValue
     * @return
     */
    public List queryTask(@Param("searchValue") String searchValue);
}

DfTimerTaskMapper.xml




    

上一节中我们在application.properties 文件中有配置MyBatis中 xml
配置文件的位置,在SpringBoot 项目启动时则会扫描所有Mapper的xml文件,并通过 mapper的namespace 找到与之对应的dao层 Interface类,将其注册为Spring的Bean,那么就可以通过IOC,随便调用 dao层的方法啦。

可以看到我在示例中用到了 where、if 等标签,正是这些标签使得MyBatis更加具有灵活性。MyBatis的动态sql,避免了很多其他框架拼接 SQL 语句的痛苦。

2.3. 使用MyBatis方式一:注解

人总是趋向于懒惰的,我开始期望于jdbc的一些特性。现在写一个dao层方法,还要在xml中写对应的实现,能不能做到我只写Java就可以了?很幸运,我能想到的MyBatis都做到了。

Java中自定义注解类,就是自定义了想要规范输入的元数据。就像MyBatis 的xml中那些标签一样,同样可以通过在Java接口中添加注解的方式,实现方法的sql。例如:

DfTimerTaskMapper.java

@Mapper
public interface DfTimerTaskMapper {
 /**
     * 查询已存在task_code 的数量
     * @param taskCode
     * @return
     */
    @Select("select count(1) from df_timer_task where task_code=#{taskCode}")
    public int countTask(@Param("taskCode")String taskCode);
}

只需要通过在 Interface 的抽象方法上方,通过注解sql,就能实现dao层的方法,不需要再写 Mapper的xml。

那么在日常开发中,“xml配置”和“注解”这两种方式我们该做何选择呢?我的偏向是简单的sql通过注解方式实现。复杂的sql,例如需要用到动态sql,或者sql语句过长需要排版美化的,都通过xml配置的方式实现。当然,仁者见仁,智者见智。你怎么喜欢就怎么来,MyBatis作为“半自动化”ORM框架,就是让程序员能减少框架的束缚。

3. 分页查询

在为前端报表数据查询写接口的时候,我们经常需要分页返回数据。例如:返回第 1~ 20行,或21~40行数据等。我们不仅需要返回指定行数区间的数据,还需要算出来该查询条件下一共有多少行数据。我写过很多数据库的分页sql:Oracle通过rownum,mysql通过 limit,sql server通过 top,等等。标准不一样,当分页的查询多了,代码写起来很冗余。网上和MyBatis完美结合的分页插件,下面我推荐的是PageHelper。

3.1. PageHelper 分页器

先直接上使用的代码吧,使用PageHelper插件仅需要通过pom.xml添加jar包



    com.github.pagehelper
    pagehelper-spring-boot-starter
    1.2.10

使用PageHelper的方式也很简单,先执行PageHelper.startPage(pageIndex,pageSize,true)方法,传入你定义的页面码pageIndex,和每页的记录数pageSize,然后紧跟着执行你自定义的查询语句。最后根据查询语句返回的对象列表,创建PageInfo的实例,PageInfo对象的属性里面就包含所需的:总记录数、总页数、查询数据列表,等等。

PageHelper.startPage(pageIndex,pageSize,true);
List dfTimerTaskLogVList=  dfTimerTaskLogMapper.queryLog(executeStatus,
       taskCode,methodType,methodName,fromBeginTime,toBeginTime,fromFinishTime,toFinishTime);
PageInfo pageInfo=new PageInfo<>(dfTimerTaskLogVList);
// 分页查询的数据集 List :pageInfo.getList();
//总记录数 long:pageInfo.getTotal();

如果我们打印出dao层的执行sql,会发现虽然我们的的查询语句中并没有实现分页,但是PageHelper已经替我们加上了分页的sql。PageHelper首先将前端传递的参数保存到Page这个对象中,接着将Page的副本存放入ThreadLoacl中,这样可以保证分页的时候,参数互不影响,接着利用了MyBatis提供的拦截器,取得ThreadLocal的值,重新拼装分页SQL,完成分页。

3.2. 数据返回封装

PageHelper针对分页查询返回的数据集提供了封装类PageInfo,但团队开发过程中,PageInfo定义的属性名不一定符合我们的要求,那我们能不能自定义返回的类类型呢?当然可以。上节在分析PageInfo的实现原理时了解到,是通过Page对象存储在ThreadLocal中实现,我们只要获取Page值就行了。下面提供我封装的类

PageQueryResult.java

/**
 * 基于分页的方法改造
 * PageHelper -> PageInfo -> PageSerializable
 * @param 
 */
public class PageQueryResult implements Serializable {
    private static final long serialVersionUID = 1L;
    protected long count;
    protected List result;

    public PageQueryResult() {
    }

    public PageQueryResult(List list) {
        this.result = list;
        if (list instanceof Page) {
            this.count = ((Page) list).getTotal();
        } else {
            this.count = (long) list.size();
        }

    }

    public static  PageQueryResult of(List list) {
        return new PageQueryResult(list);
    }

    public long getCount() {
        return this.count;
    }

    public void setCount(long total) {
        this.count = total;
    }

    public List getResult() {
        return this.result;
    }

    public void setResult(List list) {
        this.result = list;
    }

    public String toString() {
        return "PageQueryResult{count=" + this.count + ", result=" + this.result + '}';
    }
}

调用方式示例:

PageHelper.startPage(pageIndex,pageSize,true);
PageQueryResult pageQueryResult=new PageQueryResult<>(dfTimerTaskLogMapper.queryLog(executeStatus,
                taskCode,methodType,methodName,fromBeginTime,toBeginTime,fromFinishTime,toFinishTime));
return Response.ok().data(pageQueryResult);

4. MyBatis缓存

使用缓存可以使应用更快地获取数据,避免频繁的数据库交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存的作用就越明显。MyBatis 作为持久化框架,提供了非常强大的查询缓存特性,可以非常方便地配置和定制使用。一般提到 MyBatis 缓存的时候,都是指二级缓存。一级缓存(也叫本地缓存)默认会启用,并且不能控制,因此很少会提到。

4.1. 一级缓存

我们先看看SqlSession的定义:在 MyBatis 中,你可以使用 SqlSessionFactory 来创建 SqlSession。一旦你获得一个 session 之后,你可以使用它来执行映射了的语句,提交或回滚连接,最后,当不再需要它的时候,你可以关闭 session。使用 MyBatis-Spring 之后,你不再需要直接使用 SqlSessionFactory 了,因为你的 bean 可以被注入一个线程安全的 SqlSession,它能基于 Spring 的事务配置来自动提交、回滚、关闭 session。我们在使用MyBatis时是可以手动创建和关闭SqlSession,但也可以向本文一样,通过接口的方式调用方法,将SqlSession交给Spring框架来接管。

一级缓存是默认开启的。MyBatis提供了一级缓存的方案来优化在数据库会话间重复查询的问题。实现的方式是每一个SqlSession中都持有了自己的缓存,一种是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个statement有效。

MyBatis通常和Spring进行整合开发。Spring将事务放到Service中管理,对于每一个service中的sqlsession是不同的,这是通过mybatis-spring中的org.mybatis.spring.mapper.MapperScannerConfigurer创建sqlsession自动注入到service中的。 每次查询之后都要进行关闭sqlSession,关闭之后数据被清空。所以spring整合之后,如果没有事务,一级缓存是没有意义的。

4.2. 二级缓存

二级缓存默认关闭,它是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

例如:UserMapper有一个二级缓存区域(按namespace分),其它mapper也有自己的二级缓存区域(按namespace分)。每一个namespace的mapper都有一个二级缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。

默认的二级缓存会有如下效果。

  • 映射语句文件中的所有SELECT语句将会被缓存。
  • 映射语句文件中的所有 INSERT、UPDATE、DELETE 语句会刷新缓存。
  • 缓存会使用Least Recently Used( LRU,最近最少使用 的)算法来收回。
  • 根据时间表( 如 no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新。
  • 缓存会存储集合或对象( 无论查询方法返回什么类型的值)的1024 个 引用。

对于SpringBoot项目,开启二级缓存需要在配置文件中加上@EnableCaching 的注解。而且二级缓存一般配合Redis之类的key-value 数据库来使用,具体的实践,本文将不做详述。

你可能感兴趣的:(mybatis,springboot)