MyBatis 笔记

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

插曲

学习 MyBatis 之前先来个小插曲,即如何在 JDBC 中获取 MySQL 自增主键的值(插入一条新纪录的时候),有两种方法,一种是使用原生的 SQL 语句,即 select last_insert_id(),另一种方法则是使用 JDBC 3.0 提供的 getGeneratedKeys() API,推荐使用后者,少了一次 SQL 查询,效率更高。

我们先来看看 select last_insert_id() 方式,在使用它之前,我们先说一下它的几个特点:

  • insert 和 select 语句所使用的 Connection 必须是同一个,否则返回值是不可预料的。
  • LAST_INSERT_ID() 与表无关,如果向表 A 插入数据后再向表 B 插入数据,LAST_INSERT_ID 返回表 B 的 id。
  • 假如你使用一条 INSERT 语句插入多个行,LAST_INSERT_ID() 只返回插入第一行数据时产生的 id 值(需要特别注意)。
  • 假如你使用 INSERT IGNORE,则 AUTO_INCREMENT 计数器不会增量,而 LAST_INSERT_ID() 返回 0,表示没有插入新纪录。

select last_insert_id() 的原理大概是这样的,mysql 会在每个 connection 中将上一次自增出来的 id 值保存到一个 connection 变量中,而 last_insert_id() 函数其实就是读取的这个变量而已,因为是保存在每个 connection 中,所以不同 connection(mybatis 中就是不同的 sqlSession)中保存的 last_insert_id 是不一样的,互不影响。所以只要每个线程拥有的 sqlSession/connection 不同,select last_insert_id() 就是安全的,只是需要注意一下,如果一次性插入多行,如 insert into employee(name, email) values(A, B), (C, D), (E, F),那么实际上 last_insert_id() 返回的只是 (A, B) 这条记录的自增 ID 值!这时候就需要在程序中处理一下,加上个 2 才是 (E, F) 记录的自增 ID 值。

我们来在 mysql 中测试一下,看看是否符合我们上面的定义:
MySQL Last Insert Id
MySQL Last Insert Id Result 1
MySQL Last Insert Id Result 2

OK,接下来我们来看下 JDBC 3.0 提供的 getGeneratedKeys() 方法如何使用,看看是否有插入多条记录的问题:

执行结果:

没有问题,JDBC 提供的 getGeneratedKeys() API 可以返回多个自增生成的 ID,只要使用 while 去遍历结果集就行。

入门

使用 mybatis 很简单,只需要一个 mybatis-x.x.x.jar 依赖包,当然为了连接数据库,我们还需要一个 mysql-connector-java-x.x.x.jar 依赖包,使用 maven 构建项目的话,只需添加以下 dependency 到 pom.xml:

mybatis 的四大核心概念
SqlSessionFactoryBuilderSqlSessionFactorySqlSessionMapper,只要了解了这几大概念,就可知 MyBatis 八九。

SqlSessionFactoryBuilder
从命名上可以看出,这是一个 Builder 模式的,用于创建 SqlSessionFactory 的类。SqlSessionFactoryBuilder 根据配置来构造 SqlSessionFactory。配置方式有两种,一种是常用的 XML 文件方式,另一种则是 Java Config 方式,如下:

1、XML 配置

mybatis-config.xml 就是 MyBatis 的主配置文件:

2、Java Config

Java Config 相比较 XML 文件的方式而言,会有一些限制。比如修改了配置文件需要重新编译,注解方式没有 XML 配置项多等。所以,业界大多数情况下是选择 XML 文件的方式。但到底选择哪种方式,这个要取决与自己团队的需要。比如,项目的 SQL 语句不复杂,也不需要一些高级的 SQL 特性,那么 Java Config 则会更加简洁一点;反之,则可以选择 XML 文件的方式。

SqlSessionFactory
SqlSessionFactory 顾名思义,就是用于生产 SqlSession 的工厂。通过 SqlSessionFactory 的 openSession() 方法来获取 sqlSession 实例:

SqlSession
SqlSession 包含了执行 SQL 的所有方法,基本上你可以将其看作为 JDBC 中的 Connection,例子:

当然上面这种是 mybatis 旧版本中使用的方式,不能做到类型安全,因为全是字符串,新版可以这样做:

Mapper
Mapper 顾名思义,是用做 Java 与 SQL 之间的映射的。包括了 Java 映射为 SQL 语句,以及 SQL 返回结果映射为 Java。比如这是一个常见的 Mapper 接口映射文件,注意 namespace 不要乱取,它是这个 mapper.xml 对应的 Mapper 接口的全限定类名,而里面的 select、insert、update、delete 语句的 id 就是这个 Mapper 接口里面的方法名,mybatis 会自动将这个 Mapper 接口和 Mapper.xml 映射文件对应起来,然后我们就能直接像上面那样,直接调用 Mapper 接口中的方法来执行底层的数据库操作了,比如 session.selectOne(id, arg),做到类型安全。

当然即使存在一对 Mapper 接口和 Mapper 映射文件,我们也还可以使用旧版本中的非类型安全方式,但更建议使用类型安全方式:

当然,mybatis 3 也支持注解形式的 mapper.xml 配置,和 spring 有点像,即我们可以直接在 BlogMapper 接口中写上我们的 sql 语句:

这种情况下,就不需要对应的 mapper.xml 映射文件了,但是由于 Java 注解的局限性,很多映射选项我们不能用在注解形式的配置上,所以用的最多的还是映射文件形式,这个和 Spring 基本是相反的,因为 Spring 的话,貌似全部使用 Java Annotation 形式会更简洁(但我自己不太喜欢全部用注解)。

官方文档中关于这四大概念的解释
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的(静态单例模式)。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。

从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。但是也可以使用任意的输入流(InputStream)实例,包括相对于 classpath 的路径(resource)或者相对于文件系统的绝对路径(file://,url)来配置。MyBatis 包含一个名叫 Resources 的实用工具类,它包含一些实用方法,能够让我们更加容易的从 classpath 或文件系统中加载资源文件。

XML 配置文件(configuration XML)中包含了对 MyBatis 的核心设置,包含获取数据库连接实例的数据源(DataSource)和决定事务作用域和控制方式的事务管理器(TransactionManager)。XML 配置文件的详细内容后面再探讨,上面也给出了对应的配置文件例子。当然,还有很多配置项可以在 XML 文件中进行配置,上面的示例指出的则是最关键的部分。要注意 XML 头部的声明,用来验证 XML 文档正确性。environment 元素中包含了 事务管理数据库连接池 的配置。mappers 元素则是包含一组 mapper 映射器(这些 mapper 的 XML 文件包含了 SQL 代码和映射定义信息)。

当然也可以不使用任何 XML 配置文件(包括配置文件和映射文件),而是全部使用 Java 代码 + Java 注解的形式来进行 MyBatis 系统的配置。注意到我们上面的 Java Config 代码中,添加了一个 BlogMapper 映射接口,这样做的目的是为了完全脱离 Mapper.xml 映射文件。不过,由于 Java 注解的一些限制加之某些 MyBatis 映射的复杂性,XML 映射对于大多数高级映射(比如:嵌套 Join 映射)来说仍然是必须的。有鉴于此,如果存在一个对等的 XML 配置文件的话,MyBatis 会自动查找并加载它(这种情况下,BlogMapper.xml 将会基于类路径和 BlogMapper.class 的类名被加载进来,举个例子就是如果类路径的同一个目录中,存在 BlogMapper.class 和 BlogMapper.xml 两个文件,那么即使是纯 Java 形式,MyBatis 也会加载这个 XML 文件)。

四个核心对象的作用域与生命周期
1、SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但是最好还是不要让其一直存在以保证所有的 XML 解析资源开放给更重要的事情。

2、SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏味道(bad smell)”。因此 SqlSessionFactory 的最佳作用域是应用作用域。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

3、SqlSession
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。也绝不能将 SqlSession 实例的引用放在任何类型的管理作用域中,比如 Servlet 架构中的 HttpSession。如果你现在正在使用一种 Web 框架,要考虑 SqlSession 放在一个和 HTTP 请求对象相似的作用域中。换句话说,每次收到的 HTTP 请求,就可以打开一个 SqlSession,返回一个响应,就关闭它。这个关闭操作是很重要的,你应该把这个关闭操作放到 finally 块中以确保每次都能执行关闭。下面的示例就是一个确保 SqlSession 关闭的标准模式:

4、映射器实例(Mapper Instances)
映射器是一个你创建来绑定你映射的语句的接口。映射器接口的实例是从 SqlSession 中获得的。因此从技术层面讲,任何映射器实例的最大作用域是和请求它们的 SqlSession 相同的。尽管如此,映射器实例的最佳作用域是方法作用域。也就是说,映射器实例应该在调用它们的方法中被请求,用过之后即可废弃。并不需要显式地关闭映射器实例,尽管在整个请求作用域(request scope)保持映射器实例也不会有什么问题,但是很快你会发现,像 SqlSession 一样,在这个作用域上管理太多的资源的话会难于控制。所以要保持简单,最好把映射器放在方法作用域(method scope)内。如:

小结
SqlSessionFactoryBuilder 是用来根据 XML 文件或 Configuration 对象构建出一个 SqlSessionFactory 工厂对象的,构建完之后这个 Builder 对象就不需要用了,然后这个 SqlSessionFactory 工厂对象通常在一个应用程序中,都只有一个,即单例模式,是线程安全的,通过 SqlSessionFactory 的 openSession() 方法可以获取一个非线程安全的 SqlSession 实例,注意是非线程安全的哦,然后我们又可以通过这个 SqlSession 对象获取一个 Mapper 接口实例,SqlSession 对象和 Mapper 接口对象应该看作一对,作用域和生命周期也应该相同,不能共享给其他线程,所以最好就放在方法内部。其实到后面你会知道,这个 Mapper 接口的对象其实是 MyBatis 通过 JDK 动态代理来生成的,你自己打印一下它的全限定类名就能看得出,最后就是,sqlSession 对象一定要记得关闭,最好的方法就是在 finally 块中写上 sqlSession.close() 调用,最后还有一点就是默认的 openSession() 方法获取的 session 实例是不会自动提交的,所以要记得在 insert、update、delete 之后调用 session.commit() 方法来提交到数据库中。

然后就是 mybatis 的配置文件,为了不混淆,在后面我将使用 配置文件(主配置文件)、映射文件 来分别表示 mybatis-config.xml 和 mybatis-mapper.xml。config.xml 中就是 mybatis 的一些环境设置,如数据源、事务管理方式、别名设置、映射文件设置等,而映射文件则建议与对应的 Mapper 接口一一对应,且最好与 Mapper 接口放在一起,当然为了好看,我们可以直接在 resources 目录下创建一个 package 层次结构,然后将 mapper.xml 放到这里面去,什么意思呢?即假设我现在有一个 com.zfl9.mapper.EmployeeMapper 映射接口(放在 src/main/java 目录下),则我们将其对应的映射文件放到 src/main/resources/com/zfl9/mapper/EmployeeMapper.xml 位置,因为最终运行时,它们都会合并到一个 classpath 路径中,所以就是同一个路径下了,这样做的目的是更加简洁,不用将 java 文件和 xml 文件混在一起,不便于管理和维护。

其实本质上,你可以将 mapper 文件看作是 mapper 接口的实现类,只不过这个实现类是使用 xml 形式来定义的,mybatis 运行时会根据这个 mapper 文件动态生成一个 mapper 实例。这也是为什么我们喜欢将它们放在同一个类路径下的原因,而且我们通常也会给它们起一样的名字,即 EmployeeMapper.class、EmployeeMapper.xml。当然前面的官方文档中也说了,我们可以直接在 mapper 接口中直接实现它自己,也就是用对应的 @Select@Insert@Update@Delete 注解来标注对应的接口方法,在上面写我们的 sql 语句。但是官方文档还说了,即使是这种方式,也会查找对应目录下的 mapper 文件,我们来测试一下:

具体代码就不贴出来了,先说下结论吧,官方文档说的没错,因为普通情况下,我们需要一个 mybatis-config.xml 文件,不过现在我们使用 Java Config 的形式来配置它而已(Configuration 对象),然后我们在类路径下放了 EmployeeMapper.class 和 EmployeeMapper.xml,测试结果如下,首先我没有在 EmployeeMapper 接口中定义注解,全部都是使用对应的 xml 形式实现,运行没问题,然后我对其中一个方法使用注解标注,然后运行报错,因为 mybatis 会找到两个实现版本,一个是注解实现,一个是 xml 实现,所以就报错了,然后我把同名的那个 xml 定义给他删了,运行起来就没问题,运行其它没用注解替换的方法也是正常的,所以结论是,mybatis 会结合 EmployeeMapper.class 中的注解和 EmployeeMapper.xml 文件来共同实现 EmployeeMapper 接口。

CRUD

pom.xml

Employee.java

mybatis-config.xml

EmployeeMapper.java

EmployeeMapper.xml

EmployeeMapperTest.java

MySQL - employee 表
employee 表

关联查询

上一节演示了基本的 CRUD 数据库操作,所谓 CRUD 就是增删改查的缩写,增删改操作都比较简单,没什么可讲的,关键是查,很多 SQL 优化都是指的查询优化。因为 SQL 查询语句往往比增删改语句复杂的多,比如常见的多表查询,需要考虑到查询性能,如何优化查询语句以提高应用的响应速度等。

在这里我们就来讲解一下常见的进阶查询操作,即:一对一查询一对多查询,也称为关联查询,本质上一对一查询和一对多查询都是差不多的。假设数据库中存在两张表,teacher、student,表示老师和学生,一个老师教多个学生(一对多),而一个学生只被一个老师教(一对一),表结构如下:

通过 student 表的 teacher_id 字段可以找到对应的 teacher 记录。

一对一查询,查询 student 表,并获取对应的 teacher 记录

我们来看看如何使用 MyBatis 来实现这种一对一的查询操作,因为关联查询是比较复杂的一种查询,所以我们不能直接在 select 标签中使用 resultType 属性,而应该改用可以定制化的 resultMap 属性,这个属性指定的是一个 resultMap 标签的 id,而这个 resultMap 标签的 type 属性则是对应的 java bean 对象,其实我们之前直接使用的 resultType 属性就是 resultMap 的一种语法糖,在内部它依旧是一个 resultMap 标签。

Student 实体类:

Teacher 实体类:

mybatis-config.xml 配置文件:

为了避免在 mappers 标签中编写很多重复的 mapper 映射(通常 mapper 接口和 mapper 文件都是放到同一个类路径/package下),所以直接使用 <package name="com.zfl9.mapper"/> 来告诉 mybatis 扫描指定包下的所有 mapper 接口和对应的 mapper 文件,这样只要一行配置就行了。

StudentMapper.java 映射接口:

StudentMapper.xml 映射文件:

resultMap 标签的 type 属性就是对应的实体类的全限定类名,即 Student,里面的 id 标签用来填写主键的 column,而 result 标签则用来填写普通的 column,id 和 result 标签都可以有多个,因为同一张表中主键虽然只能有一个,但是一个主键可以有多个 column。它们的 column 属性就是数据表中的列名,而 property 则是实体类中的属性名。然后我们需要关注的重点就是 association 标签,association 的中文意思就是关联,在 mybatis 中用来表示一对一关系,因为一个学生只于一个老师相关联,property 属性是实体类中的属性名(teacher),而 javaType 属性则是关联的实体类的全类名,也就是 Teacher,association 标签内部的 id 和 result 标签的意思与上面的 id 和 result 标签的意思是一样的,column 是当前结果表中的字段名,property 则是 Teacher 实体类中的属性名。

StudentMapperTest 测试类:

执行结果如下:

现在我们来看一下如何实现 一对多查询,比如查询 teacher 表的一条记录,因为一个老师对应多个学生,刚好符合我们的要求,原生 SQL:

TeacherMapper.java 映射接口:

TeacherMapper.xml 映射文件:

其实就是把 association 改为了 collection 标签,然后就是 javaType 属性改为了 ofType 属性(集合元素的类型),其它的没什么变化。

TeacherMapperTest 测试类:

执行结果如下:

一对一查询的另一种方式(分步查询)

前面我们介绍的一对一查询和一对多查询都是只有一次 SQL 查询操作,而这里的则是两次 SQL 查询操作,通常情况下,不建议使用这种方式,因为多了一次 SQL 查询,当然针对这种分步查询的做法也有优化手段,那就是启用 mybatis 的延迟加载功能,当我们不需要用到 Student 实体类中的 teacher 属性时,这个 getTeacherById 语句就不会被执行,可以缓解数据库的压力,但是如果条件允许,还是使用 join 查询比较好。

当然也并不是说 join 查询就一定比分步查询好,比如这么一种场景,有一张用户表,还有一张聊天记录表。一个用户可能关联了 1000 条聊天记录。如果是关联查询,那么就一次性就把这 1000 条数据查出来了,但是可能程序仅仅想查询用户的一些信息,不需要用到聊天记录数据,那么就纯属浪费了;而分步查询 + 懒加载的目的就是在用户真正需要使用数据的时候才去查询数据库,而不是第一次就一下子全部给查询出来,所以它们都有各自适用的场合。

一对多查询的另一种方式(分步查询)

稍微解释一下,无论是 association 还是 collection,分步查询的 resultMap 都是差不多的:

property 是实体类的属性名,column 则是传递给 select 语句的查询参数,select 就是实际执行查询操作的语句 id。

而传统的 join 关联查询的 resultMap 则是这样的,注意多了一个 javaType、ofType,用来告诉 mybatis 对应的实体类类型,那为什么使用分步查询就不用指定这个类型呢?因为 select 语句上本身就有 resultType、resultMap 来指定对应的实体类类型,所以不需要重复指定它:

N + 1 问题
所谓 N + 1 问题(我感觉叫做 1 + N 问题会比较合适)就是指这么一种情况:还是以上面的 student 表和 teacher 表为例,我现在想查询出所有 teacher 记录,并且查询出 teacher 记录相关联的 student 记录,并且使用“分步查询”,则可以简单表示为:

假设 teacher 表有 N 条记录,那么 ORM 框架(N + 1 问题是 ORM 框架中的问题)实际上就会执行 N 次语句 2,加上查询了一次语句 1,就是 N + 1 次查询,导致程序的效率非常低。当然要避免这个问题有两种常见的解决方法(在 mybatis 中),第一种方式就是使用 join 多表查询,这个前面已经演示过了;第二种方式就是使用 mybatis 的延迟加载技术,所谓延迟加载就是首次查询时不会先去查询语句 2,只有等我们的应用实际用到了 teacher 对象的 student 成员时才会去执行语句 2 去查询数据库,为了对应用程序透明化,mybatis 使用了动态代理技术(当然不是 jdk 动态代理,因为我们的实体类通常都没有去实现任何 java 接口,而 jdk 动态代理要求委托类至少实现一个接口,所以使用的是 cglib/javassit,v3.3 版本之前默认使用 cglib,v3.3 版本之后,含 v3.3 版本,默认使用的是 javassit),现在我们就来学习一下 mybatis 的延迟加载如何使用。

主要涉及到的是这三个 mybatis-config.xml 里面的 setting:

  • lazyLoadingEnabled:延迟查询的默认开关,默认为 false,注意是默认开关,association/collection 中的 fetchType 属性的优先级更高。
  • aggressiveLazyLoading:激进模式的延迟加载,3.4.1 之前默认为 true,3.4.1 之后默认为 false。所谓激进模式是指当我们第一次访问代理对象(注意是代理对象)的任何方法时就会去查询数据库,把里面的延迟加载对象的数据都加载出来,而当我们设置为 false 时,表示只有我们第一次访问代理对象内部的延迟对象的时候才会去数据库中加载对应的对象数据,后者应该才是我们想要的值,所以请尽量设置为 false。
  • lazyLoadTriggerMethods:关闭激进模式的延迟加载对象后(即 aggressiveLazyLoading 为 false),但又希望在调用代理对象的某些方法之前就把所有的延迟加载对象都从数据库加载出来,怎么办呢?这个时候我们就可以通过 lazyLoadTriggerMethods 参数来指定这些代理对象的方法的名称。默认是 equals、hashCode、clone 和 toString,当我们访问代理对象的这些方法时通常都需要将内部的所有数据成员给加载出来,所以基本上我们也不需要动它,默认值很合理。

即使 lazyLoadingEnabled 属性为 false,也可以通过 association/collection 元素的 fetchType 属性来指定是否进行延迟加载,该属性有两个取值:eager(立即加载)、lazy(延迟加载)。很多时候我们并不希望给全部关联查询都启用延迟加载,毕竟不是都需要,所以单独设置可能会好一些。

首先配置 mybatis-config.xml 里面的 settings 元素,添加 lazy loading 相关的配置项:

修改我们的测试方法,打印一下实体类是不是代理对象:

运行结果:

discriminator 鉴别器
discriminator 既不是一对一查询也不是一对多查询,它类似 Java 中的 switch 语句,根据一个 column 值来确定使用哪个 resultMap。例子:

它的具体含义是这样的,mybatis 从 select 语句的执行结果中,获取一条记录,然后取出 vehicle_type 这个字段,将它的值看作一个 int 类型,然后与里面的 1、2、3、4 进行比对,如果匹配成功,则使用对应的 resultMap 来封装该条记录为一个 pojo,如果一个都没有匹配上,则使用 discriminator 标签外部的 resultMap 来封装该条记录为一个 pojo(即 vehicleResult 这个 resultMap)。discriminator 里面的 4 个 type 都是 Vehicle 的子类,discriminator 的作用大多数也是这种,根据某个列的值来动态的选择实例化哪个 pojo(存在继承关系的 pojo)。

注意,默认情况下,discriminator 是具有“排他性”的,即如果当前记录的 vehicle_type 为 1,那么就使用 case 为 1 的那个 resultMap 来封装记录为 pojo,而它外部的 vin、year、make、model、color 都不会被封装上(除非你在 carResult 里面封装这些属性),只有当 vehicle_type 的值没有匹配到任何一个 case 时才会使用外部的 resultMap;很多时候这并不是我们想要的,我们想要的是让 discriminator 内部的 resultMap 继承外部的 resultMap,而不是排他性的。那这种情况该怎么办呢?也简单,在 carResult 这个 resultMap 标签上添加一个属性就可以继承 vehicleResult 标签了,这个属性就是 extends="vehicleResult",即从上面这个变为下面这个:

当然我们也可以直接将这个外部 resultMap 内联进来,直接放到 case 标签内部,反正它也是继承的,基本无法重用,还不如放到一起,增加可读性。

这种写法和上面的分开写且加了 extends 属性的写法是一样的,推荐使用这种写法,可读性强。case 标签内部可以有 resultMap 标签中的任意元素。

下面我们来通过一个简单的例子来验证一下 discriminator 标签的用法和作用是不是和我们上面描述的一致,顺便巩固一下 discriminator 标签的理解。

People 实体类(我创建的是抽象类,实际上 mybatis 没有这个要求,我这样做只是为了便于测试):

MalePeople 实体类:

FemalePeople 实体类:

PeopleMapper.java 映射接口:

PeopleMapper.xml 映射文件:

简单解释一下:getMaleFeijiById 和 getFemaleZiweiById 的 resultType 不知道你注意到没?没错,我们的 select 语句可以返回任意 java 类型,不只是 pojo 哦,比如我们上面就是返回的 String 类型。然后就是 resultMap 这个标签,首先我们给它指定的 type 是 People 基类,然后里面的 id、result 就是 People 基类里面定义的属性,然后我们根据 sex 这个数据表字段来进行 switch 选择,如果是 M 那就实例化 MalePeople 实体类(resultType 指定的),如果是 F 那就实例化 FemalePeople 实体类(resultType 指定的),因为这两个具体类里面的 feiji、ziwei 字段不在当前 people 表中,而是在各自的 male_people、female_people 表中,所以我们使用一个 association 标签告诉 mybatis,这个 feiji、ziwei 属性需要分步查询,即调用 getMaleFeijiById、getFemaleZiweiById 标签,并传递 column 属性指定的参数(即 id)给它们查询,最后封装为对应的 MalePeople、FemalePeople 实体类对象,返回给我们;注意,mybatis 查到记录之后首先是去执行 discriminator 鉴别器标签,找到匹配的 javaType,然后实例化这个 javaType,最后才会使用当前 resultMap 及其父 resultMap 中的 id、result、association、collection 等标签来将数据记录封装为一个 pojo 给我们,整个过程是没有实例化 People 基类的,这也是我为什么将其设为抽象类的原因,就是为了验证 mybatis 没有实例化 People 类。

PeopleMapperTest 测试类:

运行结果如下:

执行器类型

mybatis 提供三种 Executor(执行器)类型,分别是(sqlSessionFactory.openSession() 方法的参数):

  • ExecutorType.SIMPLE:默认值,不会做任何特殊的事情,每个语句都会创建一个 PreparedStatement。
  • ExecutorType.REUSE:这种类型将重复使用 PreparedStatements。
  • ExecutorType.BATCH:通常用于数据的批量更新和插入。

SIMPLE 执行器可以很轻松的返回每次 insert 操作的自增 id 值,只需要简单的使用 getGeneratedKeys 和 keyProperty 属性就行。而 BATCH 执行器因为是专门用于批量更新的(这里指的更新就是增删改),所以无法优雅的返回 insert 后的自增 id 值,只能返回最后一次 insert 操作的自增 id 值,并且无法返回成功更新的记录数目。

批量插入数据有两种实现方式,一种是使用 SIMPLE 执行器,用单条 sql 语句来插入所有数据,即 insert into employee(name, email) values(...),(...),(...) 方式,当然这种方式无法使用 mybatis 的 getGeneratedKeys 和 keyProperty 功能。另一种则是使用 BATCH 执行器,因为每个 sqlSession 都有自己相关联的 Executor,并且在获取之后就不能再更改了,所以通常我们需要临时获取一个 BATCH 类型的 session,然后进行批量插入,代码如下(网上找的,能看就行):

除了调用 session.commit() 会执行批量更新之外,也可以调用 session 的 List<BatchResult> flushStatements() 方法来执行批量更新(推荐)。

#{}${} 的区别

阅读过 mybatis 的官方文档就可以知道,mybatis 的 select、insert、update、delete 语句的类型(statementType)都是 PREPARED(预编译类型,对应 jdbc 中的 PreparedStatement),statementType 有三个取值:STATEMENTPREPAREDCALLABLE,这会让 MyBatis 分别使用 StatementPreparedStatementCallableStatement,因为默认情况下是预编译语句类型,所以我们可以使用 #{} 占位符,这个占位符其实就是 PreparedStatement 中的 ? 占位符。

在学习 jdbc 的时候我们没有详细了解过 PreparedStatement 语句类型,现在我要告诉大家的是,Statement 不能防止 SQL 注入,而 PreparedStatement 能够防止 SQL 注入(Statement 是 PreparedStatement 的父接口,而 PreparedStatement 又是 CallableStatement 的父接口),那么什么是 SQL 注入呢?假设存在这样一条 SQL 语句,select * from user where username = ? and password = ?,作用很简单,就是查找与 username 和 password 相匹配的 user 记录(比如用户登录),正常情况下是这样的(暂不考虑 PreparedStatement 的防注入功能,只是简单的字符串替换),用户传入的 username 为 'Otokaze',password 为 '123456',那么实际发送给 mysql 的语句就是 select * from user where username = 'Otokaze' and password = '123456',没什么问题,正常找出这条记录;那么如果用户不怀好意,故意将 password 改为 '123456' or 1 = 1,那么实际执行的 sql 就是 select * from user where username = 'Otokaze' and password = '123456' or 1 = 1,因为 and 的优先级比 or 的优先级高,所以这条判断语句可以看作为 (username = 'Otokaze' and password = '123456') or (1 = 1),显然,1 = 1 是永远会成立的,这样的话,即使用户不知道用户名和密码,就可以得到 user 表中的所有记录!这就是所谓的”SQL 注入攻击”。

那么 PreparedStatement 是如何防止这种注入攻击的呢?还是假设 sql 语句为 select * from user where username = ? and password = ?,当我们通过 PreparedStatement 的 setString() 方法传入 Otokaze123456 时,PreparedStatement 会将 ? 作为一个字符串来处理,即实际的语句为 select * from user where username = 'Otokaze' and password = '123456',这时候即使你传入 1 = 1 这样的语句,也不会其作用,因为此时它只是字符串的一部分,即 select * from user where username = 'Otokaze' and password = '123456 or 1 = 1',如果 password 或 username 中存在单引号,那么 PreparedStatement 会将它进行转义,即 \',这样就能防止绝大多数 SQL 注入攻击了。

OK,回到 mybatis 的 #{}${},前面说了,默认情况下,mybatis 的语句类型就是 PreparedStatement,而 #{} 其实就是 PreparedStatement 里面的 ? 占位符,因为 PreparedStatement 会将其作为一个字符串处理,所以可以防止 SQL 注入攻击;那么 ${} 又是什么东西呢?也简单,${} 作用和 #{} 是相似的,只不过 ${} 会在生成 PreparedStatement 之前被进行变量替换(类似 shell 中的变量替换),即在生成预编译语句之前,这个 ${} 占位符就被对应的变量值给替换了(注意不会当作一个字符串看待),所以不能防止 SQL 注入攻击,但是有些时候我们又不得不使用 ${},比如我们想动态传递一个 table 名给 sql 语句进行拼装,因为 table 名不能使用字符串表示(单双引号都不行,而数值、字符串、时间日期这些基本上都可以使用字符串表示),即 select * from ${table_name} 这样,传入的 table_name = user 才会生效,如果使用 select * from #{table_name} 就会变为 select * from 'user' 了,不是合法的 sql 语句,然后就会报错了。

总结:能用 #{} 的地方就使用 #{},一是为了安全(防止 sql 注入),二是为了性能(重用预编译语句),如果某些时候必须使用 ${}(比如动态表名),尽量在应用程序中过滤一些不安全的字符,防止 sql 注入。

mapper 参数处理

mybatis 的结构其实比较简单,总的来说就是,一个配置文件,即 mybatis-config.xml,主要配置的东西是:事务管理器类型、JDBC DataSource、Mapper 映射相关的配置,然后就是我们的 mapper 接口和 mapper 文件,mapper 文件其实就是 mapper 接口的一种实现,我们的 sql 语句就是写在 mapper 文件中,而我们的方法接口(返回值、方法名、形参列表等)这些都是在 mapper 接口中进行配置的(就是一个普通的接口方法声明),现在我们就来讨论一下形参列表的一些细节问题。

  • 单参数(没用 @Param):分为这么几种情况(单个参数不会被封装为 map,但仍然需要 key 来访问,语法为 #{key},下同):
    • 标量类型:数值、数值包装类、字符串等,访问的 key 是没有要求的,随便一个 key 就能访问,通常我们会使用参数名称作为 key 来访问。
    • 数组类型:这里说的数组就是 Java 的内置数组,即 built-in 数组,key 为 array,使用 array[index] 来获取指定元素。
    • 列表类型:列表类型就是 java.util.List 类型,key 为 list,使用 list[index] 来访问指定索引位置的元素。
    • 集合类型:java.util.Collection 的 key 为 collection,也可使用其子类特定的 key,比如 list
    • Set 类型:MyBatis 并没有给 Set 提供专有的参数名,如 set,必须通过 collection 来访问。
    • Map 类型:这个就简单了,访问里面的 key 直接使用 #{key} 来访问就行了,很简单。
    • Pojo类型:也简单,和访问 map 一样,key 就是 getter 方法的属性名称。
  • 单参数(用了 @Param):可以将其看作为只有一个参数的“多参数”类型,访问形参的方式为 #{annotationValue}#{param1} 来访问。
  • 多参数:会将参数列表封装为一个 map,这个 map 是形参列表的 map,所以我们需要先通过一个 key 来获取对应的形参,然后再使用上述方法来访问形参里面的数据,而默认情况下,如果没有使用 @Param 注解标注形参,那么访问这些形参的 key 就是 0...index(只能访问标量类型的形参)、param1...paramN,如果形参使用了 @Param 注解,也可以使用注解指定的 value 值作为 key 来访问对应的形参。举个例子,第 1 个形参声明方式为 @Param("user") User user,则可以通过 #{param1.username} 或者 #{user.username} 来访问 user.getUsername() 的值;又比如第 3 个形参为 @Param("list") List<String> names,可以通过 #{param3[0]} 或者 #{list[0]} 来访问这个 list 的第零个元素。

mapper 返回值相关

上一节说到,mapper 方法有三个重要的元素:返回值、方法名、形参列表。方法名比较简单,就是 select/insert/update/delete 标签的 id,而形参列表的一些处理细节在上一节中也详细介绍了,那么现在只剩下返回值没介绍了。

在前面的 CRUD 例子中,EmployeeMapper 接口是这样声明的:

我们知道,mybatis 中分别为增删改查提供了各自的 xml 标签,即 selectinsertupdatedelete,select 标签可以说是其中最复杂的一个了,它的返回值我们后面再详细说明;先来解决 insert、update、delete 语句的返回值问题;这些标签对应的接口方法的返回值不需要也不能在 mapper.xml 中声明,mybatis 会自动根据方法签名来返回对应的值。通常,这些方法都是返回一个 int、long、boolean、java.lang.Integer、java.lang.Long、java.lang.Boolean、void 类型,void 类型其实就是不返回任何东西,所以这里就单独介绍 int、long、boolean 返回值类型。

int/long 返回值的意义是一样的,表示当前语句执行后,数据库中受影响的记录数目(affectedRows),而 boolean 返回值比较少用,一般来说,它的意义和 int/long 也是一样的,返回受影响的记录数,但因为是布尔类型,所以只要这个记录数大于 0,就会返回 true,如果等于 0,就返回 false。

其实你也注意到了,truncateEmployeeTable() 方法的返回值我设置的是 void,为什么不设置为 int 来获取受影响的记录数呢?好吧其实我一开始是这样干的,但是 truncate table `table_name` 语句的作用就是清空某张表,没有所谓受影响记录数,因为执行完之后表中就没有记录了,而即使你将返回值设为 int,返回的也是 0,所以干脆将其设为 void,免得有什么歧义。

OK,再来说说最复杂也是最常用的 select 方法的返回值,前面我们已经多次演示过返回 EmployeeList<Employee>,注意无论是返回 Employee 还是 List<Employee>,对应的 select 标签的 resultType 都是 com.zfl9.bean.Employee,没错,对于 list 返回类型也是一样的,因为如果你将 resultType 设为 List 也没有实际意义,因为 mybatis 可以通过方法签名知道它的返回值类型,而只有传递集合元素的类型才有意义(因为 Java 的泛型信息在运行期间会被擦除,即所谓的“类型擦除”),所以我们需要告诉具体的元素类型是什么,不然 mybatis 只能知道这是一个 Object 元素类型,而不是我们想要的 Employee 元素类型。

但其实我们还能将 select 方法的返回值设为 Map,当然这里面有可以细分为两种情况,第一种情况是:如果 select 语句只会返回一条记录,那么我们就不需要做什么特殊操作,只要将 select 标签的 resultType 设为 Map,然后将方法的返回值类型设为 Map<String, Object> 就行了,其中 key 就是这条记录的字段名,而 value 就是对应的字段值,什么意思呢?来看一个简单的例子:

输出结果:

另一种情况则是,select 语句返回多条 Employee 记录,这种情况下,我希望将 Employee 中的某个 property 作为 key,然后 employee 对象作为 value 存储在一个 map 中,然后返回给调用者。做法也很简单,但是我们需要使用一个 @MapKey("propertyNameAsMapKeyName") 注解标注一下我们的这个查询方法,好告诉 mybatis 我们需要将 employee 对象中的指定属性作为 map 的 key,进行封装,例子如下:

说到这里,我就顺便提一下 select 查询的封装问题,在前面我们没有接触 resultMap 的时候,都是使用 resultType 来告诉 mybatis 该语句的返回值类型是什么,但其实上我们也知道,这个 resultType 其实就是 resultMap 的语法糖,因为 mybatis 内部其实会将这个 resultType 转换为一个 resultMap,而里面的 column 和 property 都是这个实体类里面的属性名称,但是我们知道,java 命名规范推荐的是使用 驼峰命名法,而数据库命名规范推荐的则是 下划线命名法,比较难对应,所以没有匹配上的 property 是不会被赋值的,仍然为 null 或 0,这时候我们有 3 种办法来解决这个问题,第一种方法就是使用 sql 的 as aliasName 方式,定义字段的别名,让这个别名与实体类的属性名相同,这样使用 resultType 就能直接对应上了;第二种方法则是在 mybatis-config.xml 中启用 mapUnderscoreToCamelCase 属性(settings 标签),将其改为 true,来启用下划线命名到驼峰命名的自动映射功能,即从 A_COLUMN 字段名到 aColumn 属性名的自动映射,只要我们的命名规范没问题,仍然可以只使用 resultType 来进行自动映射;而第三种方法就是使用 resultMap 来手动指定 column 到 property 的映射关系,毕竟在做一些复杂查询时,resultMap 还是不可避免的。

sql 语句的重用

在写某些复杂的 select 语句时,通常会定义一些很长的字段列表,而且还可能在多个地方使用这些字段列表,这时候我们就可以使用 sql 标签和 include 标签来将这个冗长的字段列表提取出来,放到一个 sql 标签中,然后在 select 语句内部使用 include 标签来引用这个 sql 语句,实现代码重用,来看几个简单的例子(注意,sql 标签可以在 select、insert、update、delete 标签中使用 include 来引用):

可以在 sql 标签内部使用变量,语法为 ${},而变量的值则是通过 include 标签的 property 标签指定的,属于静态替换,并且还可以嵌套替换。

selectKey 的用法

在前面我们都是使用 jdbc 3.0 的 GeneratedKeys 方法,对于不支持 jdbc 获取主键自增值的数据库,也可以使用 selectKey 来手动回填主键。需要注意的是 selectKey 的 order 属性,有 BEFORE 和 AFTER 两个取值,BEFORE 表示在 insert 之前先获取 id 值,而 AFTER 则表示在 insert 之后再获取 id 值(MySQL 的 select last_insert_id() 就是这种情况),例子:

调用函数和存储过程

本质上,mybatis 就是 jdbc api 的简单封装,前面我们也介绍了 mybatis 中的三种语句类型,即 STATEMENT、PREPARED、CALLABLE,默认是 PREPARED,所以如果需要调用函数或存储过程,就需要显式的将 statementType 属性改为 CALLABLE,然后使用 jdbc 的固定语法:

  • {? = call function_name[(arg1, arg2, ...)]}:调用函数,其中 argN 可以使用 ? 占位。
  • {call procedure_name[(arg1, arg2, ...)]}:调用存储过程,其中 argN 可以使用 ? 占位。

因为 mybatis 中的 #{} 表达式就是 ? 占位符,所以我们需要使用 #{} 表达式来替换上面出现的 ?,语法为:
#{parameterName, mode=OUT, jdbcType=INTEGER}
必须指定 mode,这个 mode 是参数类型的意思,有 IN、OUT、INOUT 三个取值,然后就是 jdbcType,也要明确指定。

多数据库支持

mybatis 3.1.1 起,提供多数据库的无缝支持。首先你要在mybatis.xml文件中添加如下配置:

name 是数据库厂商名,value 是自定义的数据库标识。如果不知道数据库的 name,可以这样来获取:

然后,在 sql 映射文件中的 select/insert/update/delete 标签中添加 databaseId 属性,注意 id 相同:

这样 mybatis 就会自动获取 dataSource 对应的 DatabaseProductName,并转换为 databaseId,然后找到与该 databaseId 匹配的 sql 语句(就是 select、insert、update、delete 这些)。如果没有与之匹配的 databaseId 语句,则使用默认的没有标注 databaseId 的语句,如果都没有,那么就会报错,注意,如果找到了一个带 databaseId 的语句和一个没带 databaseId 的语句,那么前者的优先级更高,后者会被舍弃(应该很好理解吧)。

sql 语句中的特殊字符

也许你也注意到了上一节中的 <![CDATA[ ... ]]> 标签,有些时候我们的 sql 语句中会有一些 xml 特殊字符(主要是 <>& 这三个),这时候就需要进行一些特殊处理了,否则在构造 SqlSessionFactory 的时候就会报错,因为对应的 xml 文档是有问题的,无法解析,有两种解决办法,第一种是像上面那样,使用 CDATA 标签包住我们的 sql 语句,这样里面有什么特殊字符都不怕了;第二种方式则是使用 xml 的字符实体来转义这三个特殊字符,分别对应:&lt;&gt;&amp;

mybatis 日志配置 log4j

使用 mybatis 的时候,有时候我希望能够看到 mybatis 实际发出的 sql 和对应的参数是什么,能做到吗?当然是可以的,这是官网的介绍:

MyBatis 的内置日志工厂提供日志功能,内置日志工厂将日志交给以下其中一种工具作代理:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

MyBatis 在运行时会自动选择合适的日志工具。它会使用第一个查找得到的工具(按上文列举的顺序查找)。如果一个都未找到,日志功能就会被禁用。

为了简单,这里就使用 log4j 来作为例子演示,首先在 pom.xml 中添加 log4j 的依赖,scope 只需要是 runtime 就可以了,因为只有运行时才会用到:

然后在 resources 目录下创建 log4j.properties 文件,注意我们将 rootLogger 的日志级别设为了 INFO,而 mapper 包下的日志级别则设为了 TRACE:

运行你的程序,你将会看到 com.zfl9.mapper 包下面的映射器打印的调试级别的日志,可以看到一些有很有用的调试信息,如执行的 sql、传入的参数。

动态 SQL

MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形。动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。

你可以将动态 sql 标签看作 jstl 中的标签,动态 sql 标签可写在 sql、select、insert、update、delete 标签中。需要学习的标签很少,就 6 个:

  • if:根据 ognl 表达式作为测试条件,做动态的判断;
  • choose:类似 java 的 switch,按照顺序动态的选择第一个匹配的元素;
  • trim:可以给整个字符串加上前缀后缀(prefix、suffix),并且可以去除整个字符串首尾多余的字符;
  • where:就是 sql 中的 where 条件的封装,本质就是 trim 的语法糖,即加上前缀 where,去掉开头多余的 and/or
  • set:就是 sql 中 update 语句的 set 关键字封装,和 where 一样也是 trim 的语法糖,即加上前缀 set,去掉结尾多余的 ,
  • foreach:类似 jstl 中的 foreach 标签,用来遍历集合,如 array、list、set、map、collection,使用 #{item变量名称} 可访问元素。

由于都比较简单,这里就不详细演示了,直接照搬官方文档中的例子过来(OGNL 和 EL 表达式很像,但 OGNL 还能调用对象的方法,语法同 Java)。

if
test 表达式中的变量都是从方法参数中读取的,不需要带什么 #{},总之和 JSTL 很像就是了。

choose
只会匹配其中的一个,如果都没有匹配,则使用 otherwise(当然这个元素也可以没有)。

where/set/trim
注意我们前面的 if 示例,如果是这样的动态语句,并且假设我一个都没匹配上,会怎样?

拼装出来的 sql 是这样的,多了一个 where,不是合法的 sql 语句,会报错:

如果仅匹配第二个条件,又会怎样?开头多出一个 and,也不是合法的 sql,报错:

针对这些情况,mybatis 提供了一个 where 标签,专门表示 where 条件子句:

注意我们去掉了手写的 where,而是使用 <where> 标签来替代它,where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入 WHERE 子句。而且,若语句的开头为 ANDOR,where 元素也会将它们去除。注意到几个特性,如果 where 标签内部执行之后,整个字符串内容是空的(空字符串),则不会插入 where 关键字,相当于没有这个语句;如果至少有一个字符,那么就会在整个字符串的前面插入一个 where 关键字,并且去除整个字符串开头可能的 andor 关键字(防止 sql 语法错误)。

这实际上和下面这个 trim 标签的作用和特征是完全一样的,因为 where 就是 trim 的一颗语法糖(反过来也是成立的,trim 也有上述 where 的特性):

注意,AND 和 OR 后面的空格也是必要的,使用 | 可以指定多个需要去除的字符串前缀,prefixOverrides、suffixOverrides 表达式不区分大小写。

类似于 where,在 update 语句中,我们也常常需要进行动态生成 sql,即 set columnA = valueA, columnB = valueB,对应的标签叫做 <set>

set 会动态的设置 SET 关键字(啰嗦一下,如果是空字符串,就相当于没有这个元素),同时会自动去除字符串尾部多余的逗号,它相当于这个 trim:

foreach
动态 SQL 的另外一个常用的操作需求是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:

item 是指定当前遍历的元素的变量名,index 则是索引变量名,collection 则是 ognl 表达式,list 就是 #{list} 参数,它是一个 java.util.List 集合,open 则是整个字符串的开始字符串(注意是字符串,这意味着可以添加任意字符),close 和 open 相似,是结束字符串,而 separator 则是元素的分隔符,末尾的多余分隔符会自动去除;注意,和 trim、where、set 一样,如果集合中没有一个元素,则直接返回空串,相当于没有这个 foreach 标签,特别注意哦。还有就是里面使用 #{item} 来访问当前遍历的元素,使用 #{index} 来获取当前的元素索引值,另外,如果是遍历 map,则这个 index 是 key,item 是 value,如果是遍历的 list、array,那么 index 就是元素索引,item 就是元素本身,set 遍历不能使用 index 属性。

两个内置参数
_parameter
代表整个参数,如果是单个参数,那就是这个参数的值;如果是多个参数,则代表参数列表(就是 Map),通过 _parameter.paramN 访问各参数。

_databaseId
如果配置了多数据库支持,即 mybatis-config.xml 中配置了 <databaseIdProvider> 标签,那么这个 _databaseId 变量就是当前数据库的 ID 标识。

bind 标签
bind 标签和 jstl 的 set 标签类似,绑定一个变量到上下文中,可以在其它地方引用它,比如传入查询条件的时候,让 mybatis 自动插入 % % 前后缀:

注意不能使用 name like '%${name}%' 来添加前后缀,会报错,但是我们可以将 #{} 换为 ${},就不会报错,但是 ${} 有被 sql 注入的风险。

MyBatis 缓存

mybatis 提供两种类型的缓存,一级缓存二级缓存。注意,这里说的缓存都是指 查询缓存(select),insert、update、delete 不需要缓存。

一级缓存(本地缓存)
SqlSession 级别的缓存,所谓缓存大多数情况下都是一个 Map(假设存储在内存中),mybatis 也是如此,一级缓存可以看作 SqlSession 实例中的一个成员变量(实际上并不是),这也是为什么叫做一级缓存的原因。可以知道,一级缓存的生命周期和 SqlSession 实例的生命周期是一样的。简单例子:

运行结果如下:

可以看到,虽然我们调用了两次 employeeMapper.getEmployeeById(1) 方法,但是日志显示只查询了一次数据库,而且 emp01 == emp02 的结果居然是 true,也就是说它们是同一个对象(内存地址相同)。mybatis 缓存了第一次查询的结果(emp01),然后当我们再次查询 id 为 1 的员工时,它发现查询语句和查询条件是相同的,为了减轻数据库的负担,mybatis 直接将缓存中结果对象返回给调用者,这也就是为什么它们的引用是相同的了。

一级缓存默认是一直开启的,mybatis 早期版本甚至无法关闭一级缓存,mybatis 3.x 提供了一个 localCacheScope setting 选项,它有两个取值,SESSION|STATEMENT,SESSION 为默认值,表示一级缓存的作用范围为 session,如果将其设置为 STATEMENT,则表示作用范围为 statement 级别,也就是关闭一级缓存。每个 sqlSession 对象都有属于自己的一级缓存,不同 sqlSession 之间的一级缓存 map 是不一样的,不同 sqlSession 中的一级缓存数据也互不影响。注意,一级缓存的数据是存储在一个 hashmap 中的,mybatis 没有对这个 hashmap 进行容量限制,因为一级缓存只会在会话级别使用,而一个会话的生命周期通常都不会太长,所以没必要限制容量。

一级缓存失效的几种情况:

  • sqlSession 不同(因为不同会话的缓存对象是不同的)。
  • sqlSession 相同,但查询条件不同(显然,因为缓存中没有这条数据)。
  • sqlSession 相同,但调用了 insert、update、delete 更新方法(因为这次增删改操作可能会影响当前缓存的数据,即脏数据)。
  • sqlSession 相同,但调用了 close()、commit()、clearCache() 方法(close 很简单不解释,commit 是提交事务的意思,因为事务里面都是增删改操作,所以同上,后者就更明显了,显式调用 sqlSession 的 clearCache() 方法来清空一级缓存中的数据,自然就会失效了)。

二级缓存(全局缓存)
二级缓存虽然叫做全局缓存,但是实际上每个 namespace 的二级缓存也是不同的,互不影响,所谓 namespace 就是命名空间(不同的 xml 文件、不同类型的 Mapper 接口),这样看来,不同 xml 之间的二级缓存也是不同的,当然 mybatis 允许你在一个 namespace 中使用 <cache-ref> 标签引用另外一个 namespace 中的二级缓存标签,这样两个命名空间使用的就是相同的二级缓存,即共享了。注意,二级缓存默认情况下是没有开启的,需要自己在 mapper.xml 中配置 <cache> 元素来开启,而一级缓存是默认启用的。

那么二级缓存中的数据是从哪里来的呢?答案是从一级缓存中来的,每当关闭或提交一个 sqlSession 对象(调用 close()commit() 方法),mybatis 就会将该 sqlSession 的一级缓存中的数据转移到当前 namespace 的二级缓存中(当然这里假设同时开启了一级缓存和二级缓存)。当我们开启二级缓存后,mybatis 查询到的数据会先放到一级缓存中,当我们提交或关闭会话后,mybatis 会将一级缓存中的数据转移到二级缓存中,注意必须提交或关闭后其它会话才能读取到缓存中的对象!此时数据的查询顺序为:二级缓存 -> 一级缓存 -> 数据库。

开启二级缓存的步骤

  1. 修改 mybatis.xml,启用 cacheEnabled setting,虽然默认为 true,但是为了防止版本更新改变默认值,建议还是显式的配置为 true。
  2. 修改 mapper.xml,在开头添加 <cache> 元素,最小配置可以不指定任何属性,即 <cache/> 就可以启用当前 namespace 的二级缓存。

cache 有几个常用的 xml 属性:

  • type:Cache 自定义实现类的全类名,即允许使用第三方的 Cache 接口实现,如 redis 缓存等。
  • size:缓存的大小,即最多缓存多少个对象,默认是 1024,超过的话会执行 eviction 策略。
  • eviction:缓存的回收策略,默认是 LRU,允许的取值为:LRU、FIFO、SOFT、WEAK。
  • flushInterval:缓存的刷新间隔,默认不刷新,注意刷新间隔的时间单位为毫秒。
  • readOnly:交给调用者的缓存对象是否为只读的,默认为 false,即 mybatis 认为调用者可能会修改叫出去的缓存对象,为了防止调用者修改缓存中的源对象,mybatis 会使用序列化和反序列化技术克隆 Map 中的缓存对象,然后将这个克隆出来的对象交给调用者,这样调用者无论如何修改都不会影响缓存 map 中的对象,显然,这要求 POJO 实现 java.io.Serializable 标记接口,当然序列化和反序列化是需要额外开销的,如果可能,尽量将 readOnly 为 true,这样 mybatis 交给你的就是 map 中的对象引用,注意不要修改里面的对象数据!

为什么一级缓存交给我们的是缓存对象的引用,而不是序列化反序列化出来的克隆对象?
因为这里提到了二级缓存的 readOnly 属性,默认情况下这个属性的值为 false,即交给我们的缓存对象是克隆出来的,但是你想过没有,为什么一级缓存中我们执行 emp01 == emp02 的结果是 true,也就是说 mybatis 一级缓存的 readOnly 是为 true 的,而且无法更改,这难道是 mybatis 的 bug?其实稍微想想也能够知道,一级缓存始终是同一个线程中使用(因为 sqlSession 实例不是线程安全的),通常我们修改 pojo 的数据都是为了更新到数据库中,即稍后我们通常都会调用更新方法,而更新之后 mybatis 会刷新一级缓存中的数据,所以通常我们都不会得到脏数据(但这要求你需要谨慎修改返回的 pojo 对象,如果你稍后不更新它,那么将会导致意想不到的 bug)!

缓存相关的属性、方法

  • select 标签的 useCache 属性:影响二级缓存的使用,默认是 true,即该 select 查询会使用二级缓存;若为 false 则表示该 select 查询不会使用二级缓存,但一级缓存仍然可用。
  • select 标签的 flushCache 属性:影响一级缓存、二级缓存,默认为 false,表示该 select 查询不会清空一级和二级缓存,如果置为 true,那么 mybatis 每次调用该查询都会清空一级缓存和二级缓存。
  • 增删改标签的 flushCache 属性:影响二级缓存:默认为 true,表示该更新操作会清空二级缓存,注意有些文档说它会影响一级缓存的数据,但其实对于一级缓存来说,无论该选项的值如何,只要进行了增删改操作,mybatis 就会清空一级缓存,因为更新方法总是会调用 clearCache() 方法!
  • 调用 sqlSession 的 clearCache() 方法:影响一级缓存,也就是它只会清空一级缓存中的数据,不会影响二级缓存中的数据。
  • <cache-ref namespace="com.zfl9.mapper.BarMapper"/> 用来引用其它命名空间中的二级缓存实例,即共享二级缓存。

总之,要想使某条 Select 查询使用二级缓存,你需要保证这三个条件:

  1. MyBatis 支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
  2. 该 select 语句所在的 Mapper 配置了 <cache><cached-ref> 节点
  3. 该 select 语句的 xml 标签属性 useCache=true

调用增删改操作后,二级缓存也会被清空,但是该策略是可以通过 flushCache xml 属性进行配置的

注释掉 employeeMapper2.addEmployee() 方法(二级缓存不清空):

取消注释 employeeMapper2.addEmployee() 方法(二级缓存被清空):

如何使用 ehcache 作为 mybatis 的二级缓存实现?
mybatis 的 cache 标签有一个 type 属性,用于指定第三方的 Cache 实现类,步骤如下(ehcache 也是 Hibernate 的默认缓存实现):

  1. 获取 ehcache 的 jar 包(ehcache-core 等),并配置好 ehcache 的运行环境,如 ehcache.xml 配置文件;
  2. 获取 ehcache 的 mybatis 适配包(Cache 接口实现包),不用自己写,mybatis github 仓库有提供,直接下载;
  3. 配置 mapper.xml 中的 cache 标签,只需要指定 type 属性,如果需要改变 ehcache 运行时属性只需在 cache 标签中使用 property 进行配置。

mybatis 缓存的原理浅析
这里说的都是 mybatis 一级缓存和二级缓存的默认实现,暂不讨论第三方二级缓存。mybatis 的一二级缓存的数据结构基本都是 java.util.Map(实现类都是 PerpetualCache,即 HashMap 的简单封装),思考一下,这个 map 的 key 和 value 分别都是什么数据类型?答案是 Map<CacheKey, QueryResult>,CacheKey 就是用来唯一标识一条 mybatis 查询的 key,而 QueryResult 就是该查询的结果对象,一级缓存和二级缓存除了生命周期不同之外,工作原理什么的基本都是相同的。

那么这个 CacheKey 缓存键是什么东西呢?mybatis 又是如何判断两次查询是“相同”的呢(一二级缓存的判断条件是一样的)?

  1. <select> 标签所在的 Mapper 的 namespace + <select> 标签的 id 属性值相同;
  2. RowBounds 的 offset 和 limit 相同,RowBounds 是 MyBatis 用于处理分页的一个类;
  3. <select> 标签中定义的 sql 语句相同(准确的说应该是运算之后的 sql 语句相同);
  4. 传递给 select 语句的查询参数相同。

即只要两次查询满足以上条件且没有定义 flushCache="true",那么第二次查询会直接从一级缓存中读取数据。

一级缓存和二级缓存的使用注意事项(脏数据)
先来看一级缓存的脏数据例子:

session1 和 session2 同时读取了 id 为 1 的员工记录,然后我们在 session2 中更新了该条记录,将 name 从 zfl9 改为了 hqm8,再次调用 session1 和 session2 的读取操作,发现 session1 读取的仍然是一级缓存中的记录,即读取到了脏数据,而 session2 因为在两次查询之间调用了更新操作,所以一级缓存被清空了,读取的是最新数据。

针对这种情况的脏读,建议在需要查询最新数据前显式的调用一下 clearCache() 方法,清空当前会话的一级缓存再来查询。

再来看一个二级缓存的脏读例子:

执行结果就不贴出来了,因为这个例子是从网上拿过来的,我们来回顾一下一级缓存的脏读发生条件,一级缓存的作用域是当前 session,所以如果分别在两个 session 中读取了同一条记录,假设为 session1 和 session2,那么当我们在 session1 中更新该记录后,mybatis 会清空 session1 中的一级缓存,但是并不会清空 session2 中的一级缓存,所以当我们在 session2 中再次读取该记录时就会读取到之前一级缓存中的数据,发生脏读。

二级缓存的脏读发生条件也是差不多的,二级缓存本质上和一级缓存没什么不同,只不过作用范围大了点,二级缓存的作用范围为当前 namespace;假设存在两个 namespace 不同的 session,如 namespace1->session1、namespace2->session2,namespace1 的查询会涉及到 namespace2 中的表(典型的多表查询),那么当我们在 namespace2 中更新了缓存在 namespace1 中的二级缓存数据时,mybatis 只会清空 namespace2 中的二级缓存,而不会清空 namespace1 中的二级缓存,所以当我们在 namespace1 中再次读取该数据时就会发生脏读。

那么如何防范这种类型的二级缓存脏读呢?对于上面这个例子,可以在 classMapper 这个 xml 中使用 <cache-ref> 来引用 studentMapper 里面的 cache 节点,这样两个 mapper 使用的就是同一个 namespace 了;对于这种多表查询的场景,建议将同一个表的 sql 语句都放到同一个 namespace 中,更进一步的说,只要是可能会一起用到的查询和数据表都组织到同一个 namespace 中,这样只要更新了,这个 namespace 的二级缓存就会被清空,可以很好的避免二级缓存脏数据。

一级缓存图解
一级缓存图示
一级缓存图示 - 类的继承关系
一级缓存图示 - 缓存工作流程

对于一个查询,根据 statementId、sql、params、rowBounds 来构建一个 cacheKey,根据这个 cacheKey 去一级缓存中取出对应的缓存结果:

  • 判断从 Cache 中根据特定的 key 值取的数据数据是否为空,即是否命中;
  • 如果命中,则直接将缓存结果返回;
  • 如果没命中:
    • 去数据库中查询数据,得到查询结果;
    • 将 key 和查询到的结果分别作为 key、value对存储到 Cache 中;
    • 然后再将查询结果返回给调用者;

二级缓存图解
二级缓存图示

如上图所示,当开一个会话时,一个 SqlSession 对象会使用一个 Executor 对象来完成会话操作,MyBatis 的二级缓存机制的关键就是对这个 Executor 对象做文章。如果用户配置了 "cacheEnabled=true",那么 MyBatis 在为 SqlSession 对象创建 Executor 对象时,会对 Executor 对象加上一个装饰者:CachingExecutor,这时 SqlSession 使用 CachingExecutor 对象来完成操作请求。CachingExecutor 对于查询请求,会先判断该查询请求在 namespace 级别的二级缓存中是否有缓存结果,如果有则直接返回缓存结果;如果没有,才会交给真正的 Executor 对象来完成查询操作(该 Executor 对象中还有一个一级缓存),之后 CachingExecutor 会将 Executor 返回的查询结果放置到二级缓存中,然后再返回给调用者。

MyBatis 枚举

mybatis 的查询流程(类型处理器)
mybatis 的类型处理器

顶层的代理对象就是指我们通过 sqlSession 对象的 getMapper() 方法获取的 mapper 接口对象,因为我们只定义了 mapper 接口,并未定义 mapper 接口的实现,所以这个对象实际上是 mybatis 为我们动态生成的 jdk 代理对象;mybatis 在设置参数的时候,会调用 ParameterHandler 来设置参数,在封装结果集的时候会调用 ResultSetHandler 来处理结果,而 ParameterHandler、ResultSetHandler 内部其实都是调用的 TypeHandler 来处理 java 类型和数据库类型之间的类型转换工作,而 TypeHandler 操作的就是原生 jdbc 代码了(Statement、ResultSet 等 jdbc 对象)。

那么请你思考一下,当我们往数据库中存储一个 java.lang.Enum 枚举对象的时候,mybatis 是如何处理的呢?我们知道 Enum 类型有两个基本属性,一个是 enum.name() 枚举的名字(字面名称),另一个是 enum.ordinal() 枚举的索引(从 0 开始),那 mybatis 是存储的枚举名还是枚举索引呢?答案是:mybatis 默认存储的是枚举的名字,如果需要存储枚举的索引值,则需要在 mybatis-config.xml 中配置 typeHandlers 标签,指定处理枚举类型时要用到的 TypeHandler 类型处理器。

mybatis 默认为我们定义了两个关于枚举类型的 TypeHandler,即 EnumTypeHandlerEnumOrdinalTypeHandler,前者是使用枚举的名字,后者是使用枚举的索引。比如我们现在有一个 UserStatus 枚举类型,需要存储到数据库中,并且我们希望 mybatis 在存储该枚举类型的时候使用 EnumOrdinalTypeHandler 类型处理器,就可以这样配置 mybatis-config.xml:

注意,如果不指定 javaType,那么就是所有枚举类型都使用 EnumOrdinalTypeHandler 类型处理器,这可能不是你想要的。

我们来在 employee 表中添加一个字段,就叫做 status,类型就使用 mysql 的 enum 类型:

在这之前,我们先来复习一下 java 枚举类型的一些知识,枚举是 jdk 1.5 引入的数据类型,目的是为了替代 public static final ... 这样的非类型安全的枚举定义(大多数情况下人们会将这种枚举的数据类型定义为 int),枚举其实是一个继承 java.lang.Enum 的 final 类,而里面定义的枚举常量是一个引用类型,不再是非类型安全的 int 数值,同时我们还可以在定义枚举常量的时候调用当前枚举类的 private、package 构造方法,来传递各自的属性信息,需要注意的是,枚举常量的定义需要放在枚举类的开头,并且格式为 LOGIN, LOGOUT, REMOVE;,枚举常量之间使用逗号分隔,最后以分号结束,当然也可以调用对应的构造器,如 LOGIN(100, "登录"), LOGOUT(200, "登出"), REMOVE(300, "删除");,这个构造器在当前枚举类中定义,但是它的访问权限需要为 private 或 package,不能为 public、protected。现在我们来定义 EmployeeStatus 枚举类型:

然后修改 EmployeeMapper.xml,在 insert 标签中添加我们的 status 字段:

修改我们的测试方法,添加我们的 status 属性:

运行没问题,然后刷新我们的数据库,可以看到存储的就是 LOGIN 字符串:

OK,我们现在来修改 mybatis-config.xml 配置文件,改为存储枚举的索引值:

注意因为我们刚才设置的 status 字段的数据类型为 enum,所以要改为一个 int 类型:

然后再次运行,看看数据库中的新纪录是什么样子的:

注意 id 为 7 的记录,status 为 0,而 0 正好是 EmployeeStatus.LOGIN 的 ordinal 索引值,没问题。

自定义枚举类型的 TypeHandler
有些时候我们既不想存储枚举常量的名字也不想存储枚举常量的索引,而是想存储 EmployeeStatus 枚举常量的 code,怎么办呢?简单,我们自己来实现一个 TypeHandler 接口就行了,来看这个接口定义了哪些方法:

当然 mybatis 也提供了一个 BaseTypeHandler 抽象基类,方便我们实现自定义的类型处理器,这里就直接以实现接口为例:

因为我们在封装结果集的时候需要根据 code 来获取对应的 EmployeeStatus 枚举常量,所以定义一个静态查找方法:

现在我们修改 mybatis-config.xml,将 EmployeeStatus 的类型处理器改为我们自己的类型处理器:

现在我们再来添加一个员工,看看存储的是什么东西:

可以看到存储的是 EmployeeStatus.LOGIN 的 code 状态码,100,那么我们来调用查询方法,看看:

没问题,获取出来的就是 200 状态码对应的 EmployeeStatus.LOGIN,说明我们定义的类型处理器生效了。

MyBatis 插件

MyBatis 插件又称拦截器,下文中出现的拦截器都表示插件,MyBatis 采用责任链模式,通过动态代理组织多个插件(拦截器),通过这些插件可以改变 MyBatis 的默认行为(如流行的 PageHelper 分页插件),由于插件会深入到 MyBatis 的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件。

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor:(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler:(getParameterObject, setParameters)
  • ResultSetHandler:(handleResultSets, handleOutputParameters)
  • StatementHandler:(prepare, parameterize, batch, update, query)

总体概括为:

  • 拦截执行器的方法
  • 拦截参数的处理
  • 拦截结果集的处理
  • 拦截 sql 语句的处理

mybatis 的插件机制能够拦截 mybatis 的四大核心对象(也就上面列出来的四个接口的实现类),所以如果需要编写自己的插件,或者希望自己对插件机制能够有比较深刻的了解,就需要先来了解 mybatis 的四大核心对象。
mybatis 的四大对象,运行原理
上图 MyBatis 框架的整个执行过程。MyBatis 插件能够对则四大对象进行拦截,可以包含到了 MyBatis 一次会话的所有操作。可见插件之强大。

  • SqlSession 是 MyBatis 对外提供的一个门面接口,SqlSession 内部调用的实际上是 Executor。
  • Executor 是 MyBatis 的内部执行器,负责调用 StatementHandler 操作数据库,同时还处理二级缓存。
  • StatementHandler 是 MyBatis 语句执行对象,即原生 JDBC API 的封装,另外它也实现了 MyBatis 的一级缓存。
  • ParameterHandler 是 MyBatis 实现 Sql 入参设置的对象。插件可以改变我们 Sql 的参数默认设置,如动态的改变传入的参数。
  • ResultSetHandler 是 MyBatis 把 ResultSet 集合映射成 POJO 的接口对象。我们可以定义插件对 MyBatis 的结果集自动映射进行修改。

StatementHandler 会调用 ParameterHandler 来处理 SQL 预编译参数,然后在查询数据库之后会调用 ResultSetHandler 来封装结果集。

那么 mybatis 插件是如何实现的呢?如果你研究过 mybatis 的源码就会知道,mybatis 的四大对象在创建的时候,并非直接返回给调用者,而是都会在最后调用一下这个方法:interceptorChain.pluginAll(target),最后才会被返回,那么这个 pluginAll() 方法内部做了什么操作呢?我们来看看:

pluginAll() 方法内部会获取所有的 Interceptor 拦截器,Interceptor 是一个接口,这里获取的当然是 Interceptor 接口的实例,而一个实现了 Interceptor 接口的对象就是所谓的 mybatis 插件,mybatis 会遍历所有的拦截器,分别调用它们的 plugin() 方法来包装我们的四大对象,最后返回包装后的对象;那么我们来看看 interceptor.plugin() 方法内部又做了什么操作,由于调用的是接口定义的方法,所以来看一下 Interceptor 接口的声明:

  • plugin() 方法用来包装目标对象,官方示例为 return Plugin.wrap(target, this),即创建目标对象的代理对象;
  • setProperties() 方法则用来获取在 mybatis-config.xml 全局配置文件中传递给 plugin 的 property 参数;
  • intercept() 方法则是我们的插件方法,在里面可以拦截目标方法的执行,做一些 AOP 操作。

mybatis 插件和 Spring AOP 很像,都是创建目标对象的代理对象,然后做一些 Before、After 增强;来看看 Plugin.wrap(target, this) 方法:

如果插件没有声明要拦截当前目标对象的方法,则不会创建它的代理,这样可以避免不必要的开销,因为每个对象创建都会调用 pluginAll() 方法。

插件配置注解 @Intercepts
MyBatis 的每个插件都必须使用 @Intercepts 注解来指定要拦截哪个对象的哪个方法。来看一下这个注解的声明:

只有一个 value 属性,类型为 Signature[] 数组,Signature 其实就是要拦截的目标方法的具体签名,声明如下:

官方推荐的插件开发例子:

注册插件到 mybatis-config.xml 全局配置文件中
当你写好一个 Interceptor 实现类(插件),并使用 @Intercepts 注解插件要拦截的目标方法之后,MyBatis 是不知道你有这个插件的,你必须将这个插件注册到 mybatis-config.xml 全局配置文件中,告诉 mybatis 我有一个插件,如下,其中 plugin 元素的 interceptor 属性指定的是插件的全类名,而里面的 property 则是传递给插件的属性(当然这个 property 标签是可选的,这里仅仅是告诉你可以这样传递相关的属性给对应的插件):

那么如果我们同时注册了多个插件,它们都拦截同一个方法,执行顺序是如何的呢?我们来测试一下,编写两个简单的插件:

MyFirstPlugin.java

MySecondPlugin.java

然后在 mybatis 全局配置文件中注册这两个插件:

然后调用测试类中的 testGetEmployeeById() 方法:

执行结果如下:

可以看到,mybatis 会按照插件声明的顺序,实例化插件对象,然后在创建四大对象的时候,也是按照声明的顺序依次调用插件的 plugin() 方法来进行包装,即先调用 MyFirstPlugin、再调用 MySecondPlugin 的 plugin 方法,因为这两个插件都是拦截的同一个对象的同一个方法,而又因为是 MyFirstPlugin 先包装,MySecondPlugin 后包装,所以最外层的是 MySecondPlugin 插件,中间的是 MyFirstPlugin 插件,最内部才是真正的目标方法,从最后打印的日志中也能看的出来,这个有点类似 Java EE 中的 Filter 执行顺序。

现在我们来在 MyFirstPlugin 插件中做一些有意义的事,比如动态的修改调用者传入进来的查询参数,比如上面查询的是 1 号员工,我们来将其动态的替换为 2 号员工(一个简单的偷梁换柱,将查询 ID 加一),如下:

然后再次执行 testGetEmployeeById() 测试方法,输出如下:

可以看到,虽然我们传递给 Mapper 对象的查询参数为 1,但是实际上向数据库查询的 Id 却是 2,说明插件正常生效。

MyBatis 中一个流行的分页插件,PageHelper
配置 pom.xml,引入 PageHelper 插件的依赖:

然后配置 mybatis-config.xml,注册分页插件:

PageHelper 的 github 主页:https://github.com/pagehelper/Mybatis-PageHelper
PageHelper 的中文文档:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/README_zh.md
PageHelper 的使用方法:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md
PageHelper 的重要提示:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Important.md

只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的(内部使用了 ThreadLocal,所以是线程安全的)。

PageHelper 的 startPage()、offsetPage() 方法仅对它后面的第一个 mybatis 查询方法生效,所以如果需要多次分页,请多次调用这些方法。

PageHelper 的基本用法

注意,PageHelper 查询出来的结果对象其实是 Page<E> 类型,看文档可知,Page 是 ArrayList 的子类,所以可以将其赋值给 List<Country> 类型。
PageHelper 返回的 Page 对象中封装了一些分页相关的信息,比如当前的页码,总共有多少页,每页有多少条记录(即页面大小)等,我们来测试一下:

输出结果:

当然也可以将 Page 结果对象封装为 PageInfo 对象,PageInfo 对象提供了更多分页相关的信息,如当前是不是第一页,是不是最后一页等:

执行结果:

MyBatis MBG

mybatis mbg 又称为 mybatis 逆向工程,mbg 全称 MyBatis Generator,是 mybatis 官方提供的一个 mybatis 代码生成器,mbg 可以为所有版本的 MyBatis 以及 2.2.0 之后的 iBATIS 生成代码(pojo 类、mapper 接口、mapper 文件)。MBG 用于生成简单的 CRUD(增删改查)代码,所以你仍然需要手写复杂的连接查询、存储过程调用等 sql 代码以及对象代码,但不管怎么说,mbg 的出现极大的减轻了 mybatis 繁杂的初始化工作,对 mybatis 的发展产生了重大影响。

mbg 是根据一个 xml 配置文件来生成 pojo 类、mapper 接口、mapper 文件的,所以我们需要了解 mbg 的配置文件,这是官方给出的例子:

要使用 mybatis generator,需要编辑 pom.xml,导入 mybatis-generator-core 包:

OK,配置还是比较简单的,我们来编写一个 employees 数据表的 mbg 配置文件 generatorConfig.xml

然后编写一个运行方法,使用 Java 代码来生成 mybatis 代码:

然后运行,可以看到对应目录下生成了 pojo 类、mapper 接口、mapper 文件:

然后我们来编写几个测试方法,看看生成的 mybatis 代码是否正确:

注意 xxxByExample() 都是所谓的按条件查询,这个 example 就是条件,如果为 null 表示不传递条件,即查询所有:

发的 sql 没问题,查询出来的记录也是正确的,但是 mbg 生成的 pojo 类并未重写 toString() 方法,我们自己来添加一个:

然后再次编写我们的测试方法,获取全部 Department,以及所有 lastName 以 'Bain%' 开头的 Employee:

运行结果如下:

发现第二个查询太多记录了,我们来筛选一下,将查询条件改为 where lastName like 'Bain%' and firstName like 'Go%'

注意我们可以链式调用 andXxx() 方法来增加查询条件,然后运行,可以看到这次查出来的记录就比较少了,就一条:

那么如果我们想再添加一个 or 查询条件,即 where (...) or (empNo between 10000 and 10003 and gender = 'F')

运行结果如下,可以看到发的 sql 是没问题的,即 where ( ... ) or ( ... )

总结:mybatis generator 虽然能够比较好的生成 mybatis curd 代码,但是貌似并不提供常用的分页功能,如果需要实现分页,需要在 mbg 运行完之后,修改生成的 mapper 接口和 mapper 文件,添加自己的分页逻辑,加上 limit 语句,不建议直接使用 mybatis 提供的分页功能,因为它那个实际是逻辑分页,即从数据库中先查出所有的记录,再调用 jdbc api 进行分页,所以性能不好,最好的方式还是自己使用动态 sql 来添加 limit 语句,实现物理分页,这样性能才是最好的。

更新:PageHelper 插件可以很好的配合 MyBatis Generator 进行分页,例子:

运行结果如下,可以发现正常注入了 limit 语句,查出来的记录也是 10 条,没问题:

在实际查询之前,PageHelper 发了一个查询有多少条记录的 sql,select count(0) from employees where ...,那么第二次查询会怎样呢:

运行结果如下,可以发现 PageHelper 很聪明,第二次查询并没有发送统计记录总数的 sql,而是直接发送带有 limit 子句的查询:

MBG 插件
和 mybatis 一样,mybatis-generator 也支持 plugin 插件,并且 mybatis-generator 官方也提供了好些个插件(内置插件),比如生成 model 类的 toString() 方法,就可以使用 ToStringPlugin 插件;配置 mbg 的插件很简单,只需要在 context 元素的开头添加 plugin 元素,该元素只有一个 type 属性,用来指定插件的全类名,例如这里配置了 4 个比较常用的内置插件: