在我上大学的时候,最流行的JavaEE框架是 SSH (Struts+Spring+Hibernate),现在同学们应该都在学 SSM(Spring+SpringMVC+MyBatis)了。从历史演变来看,Spring是越来越强大,而MyBatis则是顶替了Hibernate的地位。今天的“主角”就是MyBatis。
我们先聊一聊ORM(Object Relational Mapping),翻译为“对象关系映射”,就是通过实例对象的语法,完成关系型数据库的操作的技术。ORM用于实现面向对象编程语言里不同类型系统的数据之间的转换,其实是创建了一个可在编程语言里使用的"虚拟对象数据库"。
ORM 把数据库映射成对象:
基于传统ORM框架的产品有很多,其中就有耳熟能详的Hibernate。ORM通过配置文件,使数据库表和JavaBean类对应起来,提供简便的操作方法,增、删、改、查记录,不再拼写字符串生成sql,编程效率大大提高,同时减少程序出错机率,增强数据库的移植性,方便测试。
但是有些时候我还是喜欢原生的JDBC,因为在某些特殊的应用场景中,对于sql的应用复杂性比较高,或者需要对sql的性能进行优化,这些ORM框架就显得很笨重。Hibernate这类“全自动化”框架,对数据库结构封装的较为完整,这种一站式的解决方案未必适用于所有的业务场景。
幸运的是,不只我一个人有这种感受,很久之前大家开始关注一个叫 iBATIS 的开源项目,它相对传统ORM框架而言更加的灵活,被定义为“半自动化”的ORM框架。2010年,谷歌接管了iBATIS,MyBatis就随之诞生了。虽然2010年我都还没上大学,但很可惜,MyBatis在国内的大火的比较晚,我在校园期间都没有接触过。
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
MyBatis为半自动化,需要自己书写sql语句,需要自己定义映射。增加了程序员的一些操作,但是带来了设计上的灵活。并且也是支持Hibernate的一些特性,如延迟加载,缓存和映射等,而且随之SSM架构的成熟,MyBatis肯定会被授予有越来越多新的特性。那么接下来就开始 MyBatis 的实战演练吧!
下面讲解在SpringBoot 中,使用MyBatis的基本操作。
在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
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 语句的痛苦。
人总是趋向于懒惰的,我开始期望于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框架,就是让程序员能减少框架的束缚。
在为前端报表数据查询写接口的时候,我们经常需要分页返回数据。例如:返回第 1~ 20行,或21~40行数据等。我们不仅需要返回指定行数区间的数据,还需要算出来该查询条件下一共有多少行数据。我写过很多数据库的分页sql:Oracle通过rownum,mysql通过 limit,sql server通过 top,等等。标准不一样,当分页的查询多了,代码写起来很冗余。网上和MyBatis完美结合的分页插件,下面我推荐的是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,完成分页。
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);
使用缓存可以使应用更快地获取数据,避免频繁的数据库交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存的作用就越明显。MyBatis 作为持久化框架,提供了非常强大的查询缓存特性,可以非常方便地配置和定制使用。一般提到 MyBatis 缓存的时候,都是指二级缓存。一级缓存(也叫本地缓存)默认会启用,并且不能控制,因此很少会提到。
我们先看看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整合之后,如果没有事务,一级缓存是没有意义的。
二级缓存默认关闭,它是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
例如:UserMapper有一个二级缓存区域(按namespace分),其它mapper也有自己的二级缓存区域(按namespace分)。每一个namespace的mapper都有一个二级缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。
默认的二级缓存会有如下效果。
对于SpringBoot项目,开启二级缓存需要在配置文件中加上@EnableCaching 的注解。而且二级缓存一般配合Redis之类的key-value 数据库来使用,具体的实践,本文将不做详述。