java最佳实践-mybatis

概述

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

原理

@mybatis

1.读取xml文件,建立连接
MyConfiguration负责与人交互。待读取xml后,将属性和连接数据库的操作封装在MyConfiguration对象中供后面的组件调用
首先Mybaitis自己的一个Resources类会去调用一个叫getResourceAsStream()的方法
加载配置MyBatis的核心配置文件,得到一个流对象。(本质上还是走的java.lang包下的ClassLoader类加载器的getResourceAsStream)
会先去创建一个SqlSessionFactoryBuilder对象(构建者对象),调用build()方法,传入第一步我们得到的那个流对象。
在这个过程中,首先,MyBatis会先去创建一个XMLConfigBuilder解析器对象
解析我们刚才传进来的那个流对象
解析完成之后,返回一个configuration对象,这个对象里面封装了解析之后我们配置的信息
正好这个configuration对象,就是我们这个方法创建(I)SqlSessionFactory类型对象需要的参数

2.创建SqlSession,搭建Configuration和Executor之间的桥梁
一个Session仅拥有一个对应的数据库连接。类似于一个前段请求Request,它可以直接调用exec(SQL)来执行SQL语句。从流程图中的箭头可以看出,MySqlSession的成员变量中必须得有MyExecutor和MyConfiguration去集中做调配,箭头就像是一种关联关系。我们自己的MySqlSession将有一个getMapper方法,然后使用动态代理生成对象后,就可以做数据库的操作了
有了这个SqlSesionFactory实现类的对象之后 我们调用openSession()的方法,这个过程中需 要 通 过TransactionFactory 生成 Transaction 对象, 并且, 还需要创建核心执行器 Executor 对象,基于这些条件,
最终创建了实现SqlSession接口的DefaultSqlSession对象

3.创建Executor,封装JDBC操作数据库
通过 SqlSession 对象执行相应的操作, 如果执行成功, 调用 commit 方法提交事务; 如果失败, 调用rollback 方法事务回滚. 最后, 调用 close 方法关闭session 资源.
Executor是一个执行器,负责SQL语句的生成和查询缓存(缓存还没完成)的维护,也就是jdbc的代码将在这里完成

4.创建MapperProxy,使用动态代理生成Mapper对象
我们只是希望对指定的接口生成一个对象,使得执行它的时候能运行一句sql罢了,而接口无法直接调用方法,所以这里使用动态代理生成对象,在执行时还是回到MySqlSession中调用查询,最终由MyExecutor做JDBC查询。这样设计是为了单一职责,可扩展性更强。

读取核心配置文件并返回InputStream流对象。

根据InputStream流对象解析出Configuration对象,然后创建SqlSessionFactory工厂对象

根据一系列属性从SqlSessionFactory工厂中创建SqlSession

从SqlSession中调用Executor执行数据库操作&&生成具体SQL指令

对执行结果进行二次封装

提交与事务

1.读取xml文件,建立连接
MyConfiguration负责与人交互。待读取xml后,将属性和连接数据库的操作封装在MyConfiguration对象中供后面的组件调用
首先Mybaitis自己的一个Resources类会去调用一个叫getResourceAsStream()的方法
加载配置MyBatis的核心配置文件,得到一个流对象。(本质上还是走的java.lang包下的ClassLoader类加载器的getResourceAsStream)
会先去创建一个SqlSessionFactoryBuilder对象(构建者对象),调用build()方法,传入第一步我们得到的那个流对象。

在这个过程中,首先,MyBatis会先去创建一个XMLConfigBuilder解析器对象
解析我们刚才传进来的那个流对象
解析完成之后,返回一个configuration对象,这个对象里面封装了解析之后我们配置的信息

正好这个configuration对象,就是我们这个方法创建(I)SqlSessionFactory类型对象需要的参数

2.创建SqlSession,搭建Configuration和Executor之间的桥梁
一个Session仅拥有一个对应的数据库连接。类似于一个前段请求Request,它可以直接调用exec(SQL)来执行SQL语句。从流程图中的箭头可以看出,MySqlSession的成员变量中必须得有MyExecutor和MyConfiguration去集中做调配,箭头就像是一种关联关系。我们自己的MySqlSession将有一个getMapper方法,然后使用动态代理生成对象后,就可以做数据库的操作了

有了这个SqlSesionFactory实现类的对象之后 我们调用openSession()的方法,这个过程中需 要 通 过TransactionFactory 生成 Transaction 对象, 并且, 还需要创建核心执行器 Executor 对象,基于这些条件,
最终创建了实现SqlSession接口的DefaultSqlSession对象

3.创建Executor,封装JDBC操作数据库
通过 SqlSession 对象执行相应的操作, 如果执行成功, 调用 commit 方法提交事务; 如果失败, 调用rollback 方法事务回滚. 最后, 调用 close 方法关闭session 资源.
Executor是一个执行器,负责SQL语句的生成和查询缓存(缓存还没完成)的维护,也就是jdbc的代码将在这里完成

4.创建MapperProxy,使用动态代理生成Mapper对象
我们只是希望对指定的接口生成一个对象,使得执行它的时候能运行一句sql罢了,而接口无法直接调用方法,所以这里使用动态代理生成对象,在执行时还是回到MySqlSession中调用查询,最终由MyExecutor做JDBC查询。这样设计是为了单一职责,可扩展性更强

入门

要使用 MyBatis, 只需将 mybatis-x.x.x.jar 文件置于类路径(classpath)中即可。
如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中
https://github.com/mybatis/mybatis-3/releases
从 XML 中构建 SqlSessionFactory

构建 SqlSessionFactory

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

xml方式
从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。后面会再探讨 XML 配置文件的详细内容,这里先给出一个简单的示例:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

当然,还有很多可以在 XML 文件中配置的选项,上面的示例仅罗列了最关键的部分。 注意 XML 头部的声明,它用来验证 XML 文档的正确性。environment 元素体中包含了事务管理和连接池的配置。mappers 元素则包含了一组映射器(mapper),这些映射器的 XML 映射文件包含了 SQL 代码和映射定义信息。

代码方式
如果你更愿意直接从 Java 代码而不是 XML 文件中创建配置,或者想要创建你自己的配置建造器,MyBatis 也提供了完整的配置类,提供了所有与 XML 文件等价的配置项。

DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

注意该例中,configuration 添加了一个映射器类(mapper class)。映射器类是 Java 类,它们包含 SQL 映射注解从而避免依赖 XML 文件。不过,由于 Java 注解的一些限制以及某些 MyBatis 映射的复杂性,要使用大多数高级映射(比如:嵌套联合映射),仍然需要使用 XML 配置。有鉴于此,如果存在一个同名 XML 配置文件,MyBatis 会自动查找并加载它(在这个例子中,基于类路径和 BlogMapper.class 的类名,会加载 BlogMapper.xml)。具体细节稍后讨论。

从 SqlSessionFactory 中获取 SqlSession

既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如:

try (SqlSession session = sqlSessionFactory.openSession()) {
  Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}

诚然,这种方式能够正常工作,对使用旧版本 MyBatis 的用户来说也比较熟悉。但现在有了一种更简洁的方式——使用和指定语句的参数和返回值相匹配的接口(比如 BlogMapper.class),现在你的代码不仅更清晰,更加类型安全,还不用担心可能出错的字符串字面值以及强制类型转换。

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
}

探究已映射的 SQL 语句

现在你可能很想知道 SqlSession 和 Mapper 到底具体执行了些什么操作,但 SQL 语句映射是个相当广泛的话题,可能会占去文档的大部分篇幅。 但为了让你能够了解个大概,这里会给出几个例子。

在上面提到的例子中,一个语句既可以通过 XML 定义,也可以通过注解定义。我们先看看 XML 定义语句的方式,事实上 MyBatis 提供的所有特性都可以利用基于 XML 的映射语言来实现,这使得 MyBatis 在过去的数年间得以流行。如果你用过旧版本的 MyBatis,你应该对这个概念比较熟悉。 但相比于之前的版本,新版本改进了许多 XML 的配置,后面我们会提到这些改进。这里给出一个基于 XML 映射语句的示例,它应该可以满足上个示例中 SqlSession 的调用。

<?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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

为了这个简单的例子,我们似乎写了不少配置,但其实并不多。在一个 XML 映射文件中,可以定义无数个映射语句,这样一来,XML 头部和文档类型声明部分就显得微不足道了。文档的其它部分很直白,容易理解。 它在命名空间 “org.mybatis.example.BlogMapper” 中定义了一个名为 “selectBlog” 的映射语句,这样你就可以用全限定名 “org.mybatis.example.BlogMapper.selectBlog” 来调用映射语句了,就像上面例子中那样:

Blog blog = (Blog) session.selectOne(“org.mybatis.example.BlogMapper.selectBlog”, 101);

你可能会注意到,这种方式和用全限定名调用 Java 对象的方法类似。这样,该命名就可以直接映射到在命名空间中同名的映射器类,并将已映射的 select 语句匹配到对应名称、参数和返回类型的方法。因此你就可以像上面那样,不费吹灰之力地在对应的映射器接口调用方法,就像下面这样:

BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);

第二种方法有很多优势,首先它不依赖于字符串字面值,会更安全一点;其次,如果你的 IDE 有代码补全功能,那么代码补全可以帮你快速选择到映射好的 SQL 语句

命名空间

在之前版本的 MyBatis 中,命名空间(Namespaces)的作用并不大,是可选的。 但现在,随着命名空间越发重要,你必须指定命名空间。

命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了你上面见到的接口绑定。就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。 长远来看,只要将命名空间置于合适的 Java 包命名空间之中,你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis。

命名解析:为了减少输入量,MyBatis 对所有具有名称的配置元素(包括语句,结果映射,缓存等)使用了如下的命名解析规则。

全限定名(比如 “com.mypackage.MyMapper.selectAllThings)将被直接用于查找及使用。
短名称(比如 “selectAllThings”)如果全局唯一也可以作为一个单独的引用。 如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名。

对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 示例可以被替换成如下的配置:

package org.mybatis.example;
public interface BlogMapper {
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}

使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。

选择何种方式来配置映射,以及认为是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松的在基于注解和 XML 的语句映射方式间自由移植和切换。

作用域(Scope)和生命周期

理解我们之前讨论过的不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。

提示 对象生命周期和依赖注入框架

依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。 如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目。
SqlSessionFactoryBuilder

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession

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

try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}

在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。

映射器实例

映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样:

try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
// 你的应用逻辑代码
}

动态 SQL

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。

if

使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。

如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员精选的 Blog)。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

trim、where、set

前面几个例子已经方便地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的 “if” 示例,这次我们将 “state = ‘ACTIVE’” 设置成动态条件,看看会发生什么。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
    state = #{state}
  </if>
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:

SELECT * FROM BLOG
WHERE

这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。

MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。

来看看与 set 元素等价的自定义 trim 元素吧:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

注意,我们覆盖了后缀值设置,并且自定义了前缀值。
foreach

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!

提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

至此,我们已经完成了与 XML 配置及映射文件相关的讨论。下一章将详细探讨 Java API,以便你能充分利用已经创建的映射配置。
script

要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。比如:

    @Update({"<script>",
      "update Author",
      "  <set>",
      "    <if test='username != null'>username=#{username},</if>",
      "    <if test='password != null'>password=#{password},</if>",
      "    <if test='email != null'>email=#{email},</if>",
      "    <if test='bio != null'>bio=#{bio}</if>",
      "  </set>",
      "where id=#{id}",
      "</script>"})
    void updateAuthorValues(Author author);

bind

bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

多数据库支持

如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句。比如下面的例子:

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'db2'">
      select nextval for seq_users from sysibm.sysdummy1"
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

动态 SQL 中的插入脚本语言

MyBatis 从 3.2 版本开始支持插入脚本语言,这允许你插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。

可以通过实现以下接口来插入一种语言:

public interface LanguageDriver {
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

实现自定义语言驱动后,你就可以在 mybatis-config.xml 文件中将它设置为默认语言:

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

或者,你也可以使用 lang 属性为特定的语句指定语言:

<select id="selectBlog" lang="myLanguage">
  SELECT * FROM BLOG
</select>

或者,在你的 mapper 接口上添加 @Lang 注解:

public interface Mapper {
  @Lang(MyLanguageDriver.class)
  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}

提示 可以使用 Apache Velocity 作为动态语言,更多细节请参考 MyBatis-Velocity 项目。

你前面看到的所有 xml 标签都由默认 MyBatis 语言提供,而它由语言驱动 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver(别名为 xml)所提供。

SQL 语句构建器

ava 程序员面对的最痛苦的事情之一就是在 Java 代码中嵌入 SQL 语句。这通常是因为需要动态生成 SQL 语句,不然我们可以将它们放到外部文件或者存储过程中。如你所见,MyBatis 在 XML 映射中具备强大的 SQL 动态生成能力。但有时,我们还是需要在 Java 代码里构建 SQL 语句。此时,MyBatis 有另外一个特性可以帮到你,让你从处理典型问题中解放出来,比如加号、引号、换行、格式化问题、嵌入条件的逗号管理及 AND 连接。确实,在 Java 代码中动态生成 SQL 代码真的就是一场噩梦。
MyBatis 3 提供了方便的工具类来帮助解决此问题。借助 SQL 类,我们只需要简单地创建一个实例,并调用它的方法即可生成 SQL 语句。让我们来用 SQL 类重写上面的例子:

private String selectPersonSql() {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
    SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
    FROM("PERSON P");
    FROM("ACCOUNT A");
    INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
    INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
    WHERE("P.ID = A.ID");
    WHERE("P.FIRST_NAME like ?");
    OR();
    WHERE("P.LAST_NAME like ?");
    GROUP_BY("P.ID");
    HAVING("P.LAST_NAME like ?");
    OR();
    HAVING("P.FIRST_NAME like ?");
    ORDER_BY("P.ID");
    ORDER_BY("P.FULL_NAME");
  }}.toString();
}

这个例子有什么特别之处吗?仔细看一下你会发现,你不用担心可能会重复出现的 “AND” 关键字,或者要做出用 “WHERE” 拼接还是 “AND” 拼接还是不用拼接的选择。SQL 类已经为你处理了哪里应该插入 “WHERE”、哪里应该使用 “AND” 的问题,并帮你完成所有的字符串拼接工作。

SQL 类

// 匿名内部类风格
public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

// Builder / Fluent 风格
public String insertPersonSql() {
  String sql = new SQL()
    .INSERT_INTO("PERSON")
    .VALUES("ID, FIRST_NAME", "#{id}, #{firstName}")
    .VALUES("LAST_NAME", "#{lastName}")
    .toString();
  return sql;
}

// 动态条件(注意参数需要使用 final 修饰,以便匿名内部类对它们进行访问)
public String selectPersonLike(final String id, final String firstName, final String lastName) {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
    FROM("PERSON P");
    if (id != null) {
      WHERE("P.ID like #{id}");
    }
    if (firstName != null) {
      WHERE("P.FIRST_NAME like #{firstName}");
    }
    if (lastName != null) {
      WHERE("P.LAST_NAME like #{lastName}");
    }
    ORDER_BY("P.LAST_NAME");
  }}.toString();
}

public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

public String insertPersonSql() {
  return new SQL() {{
    INSERT_INTO("PERSON");
    VALUES("ID, FIRST_NAME", "#{id}, #{firstName}");
    VALUES("LAST_NAME", "#{lastName}");
  }}.toString();
}

public String updatePersonSql() {
  return new SQL() {{
    UPDATE("PERSON");
    SET("FIRST_NAME = #{firstName}");
    WHERE("ID = #{id}");
  }}.toString();
}

注意,SQL 类将原样插入 LIMIT、OFFSET、OFFSET n ROWS 以及 FETCH FIRST n ROWS ONLY 子句。换句话说,类库不会为不支持这些子句的数据库执行任何转换。 因此,用户应该要了解目标数据库是否支持这些子句。如果目标数据库不支持这些子句,产生的 SQL 可能会引起运行错误。

从版本 3.4.2 开始,你可以像下面这样使用可变长度参数:

public String selectPersonSql() {
  return new SQL()
    .SELECT("P.ID", "A.USERNAME", "A.PASSWORD", "P.FULL_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
    .FROM("PERSON P", "ACCOUNT A")
    .INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID", "COMPANY C on D.COMPANY_ID = C.ID")
    .WHERE("P.ID = A.ID", "P.FULL_NAME like #{name}")
    .ORDER_BY("P.ID", "P.FULL_NAME")
    .toString();
}

public String insertPersonSql() {
  return new SQL()
    .INSERT_INTO("PERSON")
    .INTO_COLUMNS("ID", "FULL_NAME")
    .INTO_VALUES("#{id}", "#{fullName}")
    .toString();
}

public String updatePersonSql() {
  return new SQL()
    .UPDATE("PERSON")
    .SET("FULL_NAME = #{fullName}", "DATE_OF_BIRTH = #{dateOfBirth}")
    .WHERE("ID = #{id}")
    .toString();
}

从版本 3.5.2 开始,你可以像下面这样构建批量插入语句:

public String insertPersonsSql() {
  // INSERT INTO PERSON (ID, FULL_NAME)
  //     VALUES (#{mainPerson.id}, #{mainPerson.fullName}) , (#{subPerson.id}, #{subPerson.fullName})
  return new SQL()
    .INSERT_INTO("PERSON")
    .INTO_COLUMNS("ID", "FULL_NAME")
    .INTO_VALUES("#{mainPerson.id}", "#{mainPerson.fullName}")
    .ADD_ROW()
    .INTO_VALUES("#{subPerson.id}", "#{subPerson.fullName}")
    .toString();
}

从版本 3.5.2 开始,你可以像下面这样构建限制返回结果数的 SELECT 语句,

public String selectPersonsWithOffsetLimitSql() {
  // SELECT id, name FROM PERSON
  //     LIMIT #{limit} OFFSET #{offset}
  return new SQL()
    .SELECT("id", "name")
    .FROM("PERSON")
    .LIMIT("#{limit}")
    .OFFSET("#{offset}")
    .toString();
}

public String selectPersonsWithFetchFirstSql() {
  // SELECT id, name FROM PERSON
  //     OFFSET #{offset} ROWS FETCH FIRST #{limit} ROWS ONLY
  return new SQL()
    .SELECT("id", "name")
    .FROM("PERSON")
    .OFFSET_ROWS("#{offset}")
    .FETCH_FIRST_ROWS_ONLY("#{limit}")
    .toString();
}

项目目录结构

/my_application
/bin
/devlib
/lib <-- MyBatis *.jar 文件在这里。
/src
/org/myapp/
/action
/data <-- MyBatis 配置文件在这里,包括映射器类、XML 配置、XML 映射文件。
/mybatis-config.xml
/BlogMapper.java
/BlogMapper.xml
/model
/service
/view
/properties <-- 在 XML 配置中出现的属性值在这里。
/test
/org/myapp/
/action
/data
/model
/service
/view
/properties
/web
/WEB-INF
/web.xml

API

SqlSession

使用 MyBatis 的主要 Java 接口就是 SqlSession。你可以通过这个接口来执行命令,获取映射器示例和管理事务。在介绍 SqlSession 接口之前,我们先来了解如何获取一个 SqlSession 实例。SqlSessions 是由 SqlSessionFactory 实例创建的。SqlSessionFactory 对象包含创建 SqlSession 实例的各种方法。而 SqlSessionFactory 本身是由 SqlSessionFactoryBuilder 创建的,它可以从 XML、注解或 Java 配置代码来创建 SqlSessionFactory。

提示 当 Mybatis 与一些依赖注入框架(如 Spring 或者 Guice)搭配使用时,SqlSession 将被依赖注入框架创建并注入,所以你不需要使用 SqlSessionFactoryBuilder 或者 SqlSessionFactory,可以直接阅读 SqlSession 这一节。请参考 Mybatis-Spring 或者 Mybatis-Guice 手册以了解更多信息。

SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 有五个 build() 方法,每一种都允许你从不同的资源中创建一个 SqlSessionFactory 实例。

SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)

第一种方法是最常用的,它接受一个指向 XML 文件(也就是之前讨论的 mybatis-config.xml 文件)的 InputStream 实例。可选的参数是 environment 和 properties。environment 决定加载哪种环境,包括数据源和事务管理器。比如:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
        ...
    <dataSource type="POOLED">
        ...
  </environment>
  <environment id="production">
    <transactionManager type="MANAGED">
        ...
    <dataSource type="JNDI">
        ...
  </environment>
</environments>

如果你调用了带 environment 参数的 build 方法,那么 MyBatis 将使用该环境对应的配置。当然,如果你指定了一个无效的环境,会收到错误。如果你调用了不带 environment 参数的 build 方法,那么就会使用默认的环境配置(在上面的示例中,通过 default=“development” 指定了默认环境)。

如果你调用了接受 properties 实例的方法,那么 MyBatis 就会加载这些属性,并在配置中提供使用。绝大多数场合下,可以用 ${propName} 形式引用这些配置值。

回想一下,在 mybatis-config.xml 中,可以引用属性值,也可以直接指定属性值。因此,理解属性的优先级是很重要的。在之前的文档中,我们已经介绍过了相关内容,但为了方便查阅,这里再重新介绍一下:

如果一个属性存在于下面的多个位置,那么 MyBatis 将按照以下顺序来加载它们:

首先,读取在 properties 元素体中指定的属性;
其次,读取在 properties 元素的类路径 resource 或 url 指定的属性,且会覆盖已经指定了的重复属性;
最后,读取作为方法参数传递的属性,且会覆盖已经从 properties 元素体和 resource 或 url 属性中加载了的重复属性。

因此,通过方法参数传递的属性的优先级最高,resource 或 url 指定的属性优先级中等,在 properties 元素体中指定的属性优先级最低。

总结一下,前四个方法很大程度上是相同的,但提供了不同的覆盖选项,允许你可选地指定 environment 和/或 properties。以下给出一个从 mybatis-config.xml 文件创建 SqlSessionFactory 的示例:

String resource = "org/mybatis/builder/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);

注意,这里我们使用了 Resources 工具类,这个类在 org.apache.ibatis.io 包中。Resources 类正如其名,会帮助你从类路径下、文件系统或一个 web URL 中加载资源文件。在略读该类的源代码或用 IDE 查看该类信息后,你会发现一整套相当实用的方法。这里给出一个简表:

URL getResourceURL(String resource)
URL getResourceURL(ClassLoader loader, String resource)
InputStream getResourceAsStream(String resource)
InputStream getResourceAsStream(ClassLoader loader, String resource)
Properties getResourceAsProperties(String resource)
Properties getResourceAsProperties(ClassLoader loader, String resource)
Reader getResourceAsReader(String resource)
Reader getResourceAsReader(ClassLoader loader, String resource)
File getResourceAsFile(String resource)
File getResourceAsFile(ClassLoader loader, String resource)
InputStream getUrlAsStream(String urlString)
Reader getUrlAsReader(String urlString)
Properties getUrlAsProperties(String urlString)
Class classForName(String className)

最后一个 build 方法接受一个 Configuration 实例。Configuration 类包含了对一个 SqlSessionFactory 实例你可能关心的所有内容。在检查配置时,Configuration 类很有用,它允许你查找和操纵 SQL 映射(但当应用开始接收请求时不推荐使用)。你之前学习过的所有配置开关都存在于 Configuration 类,只不过它们是以 Java API 形式暴露的。以下是一个简单的示例,演示如何手动配置 Configuration 实例,然后将它传递给 build() 方法来创建 SqlSessionFactory。

DataSource dataSource = BaseDataTest.createBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();

Environment environment = new Environment("development", transactionFactory, dataSource);

Configuration configuration = new Configuration(environment);
configuration.setLazyLoadingEnabled(true);
configuration.setEnhancementEnabled(true);
configuration.getTypeAliasRegistry().registerAlias(Blog.class);
configuration.getTypeAliasRegistry().registerAlias(Post.class);
configuration.getTypeAliasRegistry().registerAlias(Author.class);
configuration.addMapper(BoundBlogMapper.class);
configuration.addMapper(BoundAuthorMapper.class);

SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(configuration);

现在你就获得一个可以用来创建 SqlSession 实例的 SqlSessionFactory 了。

SqlSessionFactory

SqlSessionFactory 有六个方法创建 SqlSession 实例。通常来说,当你选择其中一个方法时,你需要考虑以下几点:

事务处理:你希望在 session 作用域中使用事务作用域,还是使用自动提交(auto-commit)?(对很多数据库和/或 JDBC 驱动来说,等同于关闭事务支持)
数据库连接:你希望 MyBatis 帮你从已配置的数据源获取连接,还是使用自己提供的连接?
语句执行:你希望 MyBatis 复用 PreparedStatement 和/或批量更新语句(包括插入语句和删除语句)吗?

基于以上需求,有下列已重载的多个 openSession() 方法供使用。

SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
Configuration getConfiguration();

默认的 openSession() 方法没有参数,它会创建具备如下特性的 SqlSession:

事务作用域将会开启(也就是不自动提交)。
将由当前环境配置的 DataSource 实例中获取 Connection 对象。
事务隔离级别将会使用驱动或数据源的默认设置。
预处理语句不会被复用,也不会批量处理更新。

相信你已经能从方法签名中知道这些方法的区别。向 autoCommit 可选参数传递 true 值即可开启自动提交功能。若要使用自己的 Connection 实例,传递一个 Connection 实例给 connection 参数即可。注意,我们没有提供同时设置 Connection 和 autoCommit 的方法,这是因为 MyBatis 会依据传入的 Connection 来决定是否启用 autoCommit。对于事务隔离级别,MyBatis 使用了一个 Java 枚举包装器来表示,称为 TransactionIsolationLevel,事务隔离级别支持 JDBC 的五个隔离级别(NONE、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE),并且与预期的行为一致。

你可能对 ExecutorType 参数感到陌生。这个枚举类型定义了三个值:

ExecutorType.SIMPLE:该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。
ExecutorType.REUSE:该类型的执行器会复用预处理语句。
ExecutorType.BATCH:该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。

提示 在 SqlSessionFactory 中还有一个方法我们没有提及,就是 getConfiguration()。这个方法会返回一个 Configuration 实例,你可以在运行时使用它来检查 MyBatis 的配置。

提示 如果你使用过 MyBatis 的旧版本,可能还记得 session、事务和批量操作是相互独立的。在新版本中则不是这样。上述三者都包含在 session 作用域内。你不必分别处理事务或批量操作就能得到想要的全部效果。

SqlSession

正如之前所提到的,SqlSession 在 MyBatis 中是非常强大的一个类。它包含了所有执行语句、提交或回滚事务以及获取映射器实例的方法。

SqlSession 类的方法超过了 20 个,为了方便理解,我们将它们分成几种组别。
语句执行方法

这些方法被用来执行定义在 SQL 映射 XML 文件中的 SELECT、INSERT、UPDATE 和 DELETE 语句。你可以通过名字快速了解它们的作用,每一方法都接受语句的 ID 以及参数对象,参数可以是原始类型(支持自动装箱或包装类)、JavaBean、POJO 或 Map。
T selectOne(String statement, Object parameter)
List selectList(String statement, Object parameter)
Cursor selectCursor(String statement, Object parameter)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)

selectOne 和 selectList 的不同仅仅是 selectOne 必须返回一个对象或 null 值。如果返回值多于一个,就会抛出异常。如果你不知道返回对象会有多少,请使用 selectList。如果需要查看某个对象是否存在,最好的办法是查询一个 count 值(0 或 1)。selectMap 稍微特殊一点,它会将返回对象的其中一个属性作为 key 值,将对象作为 value 值,从而将多个结果集转为 Map 类型值。由于并不是所有语句都需要参数,所以这些方法都具有一个不需要参数的重载形式。

游标(Cursor)与列表(List)返回的结果相同,不同的是,游标借助迭代器实现了数据的惰性加载。

try (Cursor<MyEntity> entities = session.selectCursor(statement, param)) {
   for (MyEntity entity:entities) {
      // 处理单个实体
   }
}

insert、update 以及 delete 方法返回的值表示受该语句影响的行数。

T selectOne(String statement)
List selectList(String statement)
Cursor selectCursor(String statement)
<K,V> Map<K,V> selectMap(String statement, String mapKey)
int insert(String statement)
int update(String statement)
int delete(String statement)

最后,还有 select 方法的三个高级版本,它们允许你限制返回行数的范围,或是提供自定义结果处理逻辑,通常在数据集非常庞大的情形下使用。

List selectList (String statement, Object parameter, RowBounds rowBounds)
Cursor selectCursor(String statement, Object parameter, RowBounds rowBounds)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowbounds)
void select (String statement, Object parameter, ResultHandler handler)
void select (String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)

RowBounds 参数会告诉 MyBatis 略过指定数量的记录,并限制返回结果的数量。RowBounds 类的 offset 和 limit 值只有在构造函数时才能传入,其它时候是不能修改的。

int offset = 100;
int limit = 25;
RowBounds rowBounds = new RowBounds(offset, limit);

数据库驱动决定了略过记录时的查询效率。为了获得最佳的性能,建议将 ResultSet 类型设置为 SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE(换句话说:不要使用 FORWARD_ONLY)。

ResultHandler 参数允许自定义每行结果的处理过程。你可以将它添加到 List 中、创建 Map 和 Set,甚至丢弃每个返回值,只保留计算后的统计结果。你可以使用 ResultHandler 做很多事,这其实就是 MyBatis 构建 结果列表的内部实现办法。

从版本 3.4.6 开始,ResultHandler 会在存储过程的 REFCURSOR 输出参数中传递使用的 CALLABLE 语句。

它的接口很简单:

package org.apache.ibatis.session;
public interface ResultHandler<T> {
  void handleResult(ResultContext<? extends T> context);
}

ResultContext 参数允许你访问结果对象和当前已被创建的对象数目,另外还提供了一个返回值为 Boolean 的 stop 方法,你可以使用此 stop 方法来停止 MyBatis 加载更多的结果。

使用 ResultHandler 的时候需要注意以下两个限制:

使用带 ResultHandler 参数的方法时,收到的数据不会被缓存。
当使用高级的结果映射集(resultMap)时,MyBatis 很可能需要数行结果来构造一个对象。如果你使用了 ResultHandler,你可能会接收到关联(association)或者集合(collection)中尚未被完整填充的对象。

立即批量更新方法

当你将 ExecutorType 设置为 ExecutorType.BATCH 时,可以使用这个方法清除(执行)缓存在 JDBC 驱动类中的批量更新语句。

List flushStatements()

事务控制方法

有四个方法用来控制事务作用域。当然,如果你已经设置了自动提交或你使用了外部事务管理器,这些方法就没什么作用了。然而,如果你正在使用由 Connection 实例控制的 JDBC 事务管理器,那么这四个方法就会派上用场:

void commit()
void commit(boolean force)
void rollback()
void rollback(boolean force)

默认情况下 MyBatis 不会自动提交事务,除非它侦测到调用了插入、更新或删除方法改变了数据库。如果你没有使用这些方法提交修改,那么你可以在 commit 和 rollback 方法参数中传入 true 值,来保证事务被正常提交(注意,在自动提交模式或者使用了外部事务管理器的情况下,设置 force 值对 session 无效)。大部分情况下你无需调用 rollback(),因为 MyBatis 会在你没有调用 commit 时替你完成回滚操作。不过,当你要在一个可能多次提交或回滚的 session 中详细控制事务,回滚操作就派上用场了。

提示 MyBatis-Spring 和 MyBatis-Guice 提供了声明式事务处理,所以如果你在使用 Mybatis 的同时使用了 Spring 或者 Guice,请参考它们的手册以获取更多的内容。
本地缓存

Mybatis 使用到了两种缓存:本地缓存(local cache)和二级缓存(second level cache)。

每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。

默认情况下,本地缓存数据的生命周期等同于整个 session 的周期。由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用。但是你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存。

注意,如果 localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。

你可以随时调用以下方法来清空本地缓存:

void clearCache()

确保 SqlSession 被关闭

void close()

对于你打开的任何 session,你都要保证它们被妥善关闭,这很重要。保证妥善关闭的最佳代码模式是这样的:

SqlSession session = sqlSessionFactory.openSession();
try (SqlSession session = sqlSessionFactory.openSession()) {
    // 假设下面三行代码是你的业务逻辑
    session.insert(...);
    session.update(...);
    session.delete(...);
    session.commit();
}

和 SqlSessionFactory 一样,你可以调用当前使用的 SqlSession 的 getConfiguration 方法来获得 Configuration 实例。
Configuration getConfiguration()

使用映射器
T getMapper(Class type)

上述的各个 insert、update、delete 和 select 方法都很强大,但也有些繁琐,它们并不符合类型安全,对你的 IDE 和单元测试也不是那么友好。因此,使用映射器类来执行映射语句是更常见的做法。

我们已经在之前的入门章节中见到过一个使用映射器的示例。一个映射器类就是一个仅需声明与 SqlSession 方法相匹配方法的接口。下面的示例展示了一些方法签名以及它们是如何映射到 SqlSession 上的。

public interface AuthorMapper {
  // (Author) selectOne("selectAuthor",5);
  Author selectAuthor(int id);
  // (List<Author>) selectList(“selectAuthors”)
  List<Author> selectAuthors();
  // (Map<Integer,Author>) selectMap("selectAuthors", "id")
  @MapKey("id")
  Map<Integer, Author> selectAuthors();
  // insert("insertAuthor", author)
  int insertAuthor(Author author);
  // updateAuthor("updateAuthor", author)
  int updateAuthor(Author author);
  // delete("deleteAuthor",5)
  int deleteAuthor(int id);
}

总之,每个映射器方法签名应该匹配相关联的 SqlSession 方法,字符串参数 ID 无需匹配。而是由方法名匹配映射语句的 ID。

此外,返回类型必须匹配期望的结果类型,返回单个值时,返回类型应该是返回值的类,返回多个值时,则为数组或集合类,另外也可以是游标(Cursor)。所有常用的类型都是支持的,包括:原始类型、Map、POJO 和 JavaBean。

提示 映射器接口不需要去实现任何接口或继承自任何类。只要方法签名可以被用来唯一识别对应的映射语句就可以了。

提示 映射器接口可以继承自其他接口。在使用 XML 来绑定映射器接口时,保证语句处于合适的命名空间中即可。唯一的限制是,不能在两个具有继承关系的接口中拥有相同的方法签名(这是潜在的危险做法,不可取)。

你可以传递多个参数给一个映射器方法。在多个参数的情况下,默认它们将会以 param 加上它们在参数列表中的位置来命名,比如:#{param1}、#{param2}等。如果你想(在有多个参数时)自定义参数的名称,那么你可以在参数上使用 @Param(“paramName”) 注解。

你也可以给方法传递一个 RowBounds 实例来限制查询结果。
映射器注解

设计初期的 MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,映射语句也是定义在 XML 中的。而在 MyBatis 3 中,我们提供了其它的配置方式。MyBatis 3 构建在全面且强大的基于 Java 语言的配置 API 之上。它是 XML 和注解配置的基础。注解提供了一种简单且低成本的方式来实现简单的映射语句。

提示 不幸的是,Java 注解的表达能力和灵活性十分有限。尽管我们花了很多时间在调查、设计和试验上,但最强大的 MyBatis 映射并不能用注解来构建——我们真没开玩笑。而 C# 属性就没有这些限制,因此 MyBatis.NET 的配置会比 XML 有更大的选择余地。虽说如此,基于 Java 注解的配置还是有它的好处的。

注解如下表所示:
https://mybatis.org/mybatis-3/zh/java-api.html#directoryStructure

映射注解示例

这个例子展示了如何使用 @SelectKey 注解来在插入前读取数据库序列的值:

@Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
@SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);

这个例子展示了如何使用 @SelectKey 注解来在插入后读取数据库自增列的值:

@Insert("insert into table2 (name) values(#{name})")
@SelectKey(statement="call identity()", keyProperty="nameId", before=false, resultType=int.class)
int insertTable2(Name name);

这个例子展示了如何使用 @Flush 注解来调用 SqlSession#flushStatements()@Flush
List<BatchResult> flush();

这些例子展示了如何通过指定 @Result 的 id 属性来命名结果集:

@Results(id = "userResult", value = {
  @Result(property = "id", column = "uid", id = true),
  @Result(property = "firstName", column = "first_name"),
  @Result(property = "lastName", column = "last_name")
})
@Select("select * from users where id = #{id}")
User getUserById(Integer id);

@Results(id = "companyResults")
@ConstructorArgs({
  @Arg(column = "cid", javaType = Integer.class, id = true),
  @Arg(column = "name", javaType = String.class)
})
@Select("select * from company where id = #{id}")
Company getCompanyById(Integer id);

这个例子展示了如何使用单个参数的 @SqlProvider 注解:

@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(String name);

class UserSqlBuilder {
  public static String buildGetUsersByName(final String name) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      if (name != null) {
        WHERE("name like #{value} || '%'");
      }
      ORDER_BY("id");
    }}.toString();
  }
}

这个例子展示了如何使用多个参数的 @SqlProvider 注解:

@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(
    @Param("name") String name, @Param("orderByColumn") String orderByColumn);

class UserSqlBuilder {

  // 如果不使用 @Param,就应该定义与 mapper 方法相同的参数
  public static String buildGetUsersByName(
      final String name, final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }

  // 如果使用 @Param,就可以只定义需要使用的参数
  public static String buildGetUsersByName(@Param("orderByColumn") final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }
}

This example shows usage that share an sql provider class to all mapper methods using global configuration(Available since 3.5.6):

Configuration configuration = new Configuration();
configuration.setDefaultSqlProviderType(TemplateFilePathProvider.class); // Specify an sql provider class for sharing on all mapper methods
// ...

// Can omit the type/value attribute on sql provider annotation
// If omit it, the MyBatis apply the class that specified on defaultSqlProviderType.
public interface UserMapper {

  @SelectProvider // Same with @SelectProvider(TemplateFilePathProvider.class)
  User findUser(int id);

  @InsertProvider // Same with @InsertProvider(TemplateFilePathProvider.class)
  void createUser(User user);

  @UpdateProvider // Same with @UpdateProvider(TemplateFilePathProvider.class)
  void updateUser(User user);

  @DeleteProvider // Same with @DeleteProvider(TemplateFilePathProvider.class)
  void deleteUser(int id);
}

以下例子展示了 ProviderMethodResolver(3.5.1 后可用)的默认实现使用方法:

@SelectProvider(UserSqlProvider.class)
List<User> getUsersByName(String name);

// 在你的 provider 类中实现 ProviderMethodResolver 接口
class UserSqlProvider implements ProviderMethodResolver {
  // 默认实现中,会将映射器方法的调用解析到实现的同名方法上
  public static String getUsersByName(final String name) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      if (name != null) {
        WHERE("name like #{value} || '%'");
      }
      ORDER_BY("id");
    }}.toString();
  }
}

This example shows usage the databaseId attribute on the statement annotation(Available since 3.5.5):

@Select(value = "SELECT SYS_GUID() FROM dual", databaseId = "oracle") // Use this statement if DatabaseIdProvider provide "oracle"
@Select(value = "SELECT uuid_generate_v4()", databaseId = "postgres") // Use this statement if DatabaseIdProvider provide "postgres"
@Select("SELECT RANDOM_UUID()") // Use this statement if the DatabaseIdProvider not configured or not matches databaseId
String generateId();

参考资料

https://mybatis.org/mybatis-3/zh/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值