共计 7963 个字符,预计需要花费 20 分钟才能阅读完成。
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(示例)
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--oracle jdbc-->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>6</version>
</dependency>
<!--druid 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
对于相关数据源的连接信息,需要在 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<DfTimerTask> queryTask(@Param("searchValue") String searchValue);
}
DfTimerTaskMapper.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" >
<mapper namespace="com.df.stage.tasktimer.mapper.DfTimerTaskMapper">
<select id="queryTask" resultType="com.df.stage.tasktimer.pojo.DfTimerTask" parameterType="String">
select * from df_timer_task
<where>
<if test="searchValue!=null">
task_code like '%'||#{searchValue}||'%'
or method_type =#{searchValue}
or method_name like '%'||#{searchValue}||'%'
or status =#{searchValue}
</if>
</where>
</select>
</mapper>
上一节中我们在 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 包
<!-- 分页器 pagehelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
使用 PageHelper 的方式也很简单,先执行 PageHelper.startPage(pageIndex,pageSize,true) 方法,传入你定义的页面码 pageIndex,和每页的记录数 pageSize,然后紧跟着执行你自定义的查询语句。最后根据查询语句返回的对象列表,创建 PageInfo 的实例,PageInfo 对象的属性里面就包含所需的:总记录数、总页数、查询数据列表,等等。
PageHelper.startPage(pageIndex,pageSize,true);
List<DfTimerTaskLogV> dfTimerTaskLogVList= dfTimerTaskLogMapper.queryLog(executeStatus,
taskCode,methodType,methodName,fromBeginTime,toBeginTime,fromFinishTime,toFinishTime);
PageInfo<DfTimerTaskLogV> pageInfo=new PageInfo<>(dfTimerTaskLogVList);
// 分页查询的数据集 List<DfTimerTaskLogV>: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 <T>
*/
public class PageQueryResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
protected long count;
protected List<T> result;
public PageQueryResult() {}
public PageQueryResult(List<T> list) {
this.result = list;
if (list instanceof Page) {this.count = ((Page) list).getTotal();} else {this.count = (long) list.size();}
}
public static <T> PageQueryResult<T> of(List<T> list) {return new PageQueryResult(list);
}
public long getCount() {return this.count;}
public void setCount(long total) {this.count = total;}
public List<T> getResult() {return this.result;}
public void setResult(List<T> list) {this.result = list;}
public String toString() {return "PageQueryResult{count=" + this.count + ", result=" + this.result + '}';
}
}
调用方式示例:
PageHelper.startPage(pageIndex,pageSize,true);
PageQueryResult<DfTimerTaskLogV> 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 数据库来使用,具体的实践,本文将不做详述。