本篇内容参考官方文档:mybatis – MyBatis 3 | XML 映射器

1 IDEA 设置 SQL 映射文件模板

21_在 idea 中设置映射文件的模板_哔哩哔哩_bilibili

2 映射器元素

映射器是 Mybatis 中最复杂并且是最重要的组件。它由一个接口和 xml 映射文件 (或者注解) 组成。在映射器中我们可以配置各类 SQL、动态 SQL、缓存、存储过程、级联等复杂的内容。并且通过简易的映射规则映射到指定的 POJO 或者其它对象上,映射器能有效的消除 JDBC 的底层代码。在 Mybatis 的应用程序开发中,映射器的开发工作量占全部工作量的 80%,可见其重要性。

映射文件的作用是用来配置 SQL 映射语句,根据不同的 SQL 语句性质,使用不同的标签,其中常用的标签有:<select><insert><update><delete>。下面列出了 SQL 映射文件的几个顶级元素(按照应被定义的顺序列出):

元素描述
cache该命名空间的缓存配置(会在缓存部分进行讲解)
cache-ref引用其它命名空间的缓存配置
resultMap描述如何从数据库结果集中加载对象,它是最复杂也是最强大的元素
parameterMap定义参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射 parameType。所以本文中不会介绍此元素
sql可被其它语句引用的可重用语句块
select映射查询语句
insert映射插入语句
update映射更新语句
delete映射删除语句

3 select 元素

Select 元素表示 SQL 的 select 语句,用于查询,而查询语句是我们日常中用的最多的,使用的多就意味它有着强大和复杂的功能,所以我们先来看看 select 元素的属性有哪些(加粗为最常用的)。

属性属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句
parameterType将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置(unset)
parameterMap这是引用外部 parameterMap 的已经被废弃的方法。请使用内联参数映射和 parameterType 属性
resultType从这条语句中返回的期望类型的类的完全限定名或别名。注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。可以使用 resultType 或 resultMap,但不能同时使用
resultMap外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。可以使用 resultMap 或 resultType,但不能同时使用
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false
useCache将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)
fetchSize这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。默认值为未设置(unset)(依赖驱动)
statementTypeSTATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
resultSetTypeFORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖驱动)
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略
resultOrderedresultOrdered:这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false
resultSets这个设置仅对多结果集的情况适用。它将列出语句执行后返回的结果集并给每个结果集一个名称,名称是逗号分隔的

实际用的最多的是 id、parameterType、resultType、resultMap 这四个。如果还要设置缓存的话,还会使用到 flushCache 和 useCache,而其它的属性是不常用的。FlushCache 和 useCache 会在后面的缓存部分进行介绍。

下面使用 select 元素来举一个例子,这个例子我们前面看到过,就是根据用户 Id 来查找用户的信息,代码如下:

<!-- 通过Id查询一个用户 -->
<select id="selectUserById" parameterType="int" resultType="com.thr.entity.User">
    select * from t_user where id = #{id};
</select>

这条 SQL 语句非常的简单,但是现在的目的是为了举一个例子,看看在实际开发中如何使用映射文件,这个例子只是让我们认识 select 元素的常用属性及用法,而在以后的开发中我们所遇到的问题要比这条 SQL 复杂得多,可能有十几行甚至更长。

注意:没有设置的属性全都采用默认值,你不配置不代表这个属性没有用到。

4 insert 元素

Insert 元素表示插入数据,它可以配置的属性如下。

属性属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句
parameterType将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)
parameterMap用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)
statementType可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
useGeneratedKeys(仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false
keyProperty(仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。不能和 keyColumn 连用
keyColumn(仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。不能和 keyProperty 连用
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略

下面是 insert 元素的简单应用,在执行完 SQL 语句后,会返回一个整数来表示其影响的记录数。代码如下:

<!-- 添加用户-->
<insert id="insertUser" parameterType="com.thr.entity.User">
    insert into t_user(username, age ,sex ,address) values (#{username},#{age},#{sex},#{address});
</insert>

4.1 主键回填

在 insert 元素中,有一个非常重要且常用的属性——useGeneratedKeys,它的作用的主键回填,就是将当前插入数据的主键返回。例如上面的插入语句中,我们并没有插入主键 Id 列,因为在 mysql 数据库中将它设置为自增主键,数据库会自动为其生成对应的主键,所以没必要插入这个列。但是有些时候我们还需要继续使用这个主键,用以关联其它业务,所以十分有必要获取它。比如在新增用户的时候,首先会插入用户的数据,然后插入用户和角色关系表,而插入用户时如果没办法取到用户的主键,那么就没有办法插入用户和角色关系表了,因此这个时候需要拿到对应的主键,以方便关联表的操作。

在 JDBC 中,使用 Statement 对象执行插入的 SQL 语句后,可以通过 getGeneratedKeys 方法来获取数据库生成的主键。而在 insert 元素中也设置了一个对应的属性 useGeneratedKeys,它的默认值为 false。当我们把这个属性设置为 true 时,还需要配置 keyProperty 或 keyColumn(它二者不能同时使用),告诉系统把生成的主键放入哪个属性中,如果存在多个主键,就要用逗号隔开。

我们将上面 xml 配置文件中的 insert 语句进行更改,更改后代码如下:

<!-- 添加用户-->
<insert id="insertUser" parameterType="com.thr.entity.User" useGeneratedKeys="true" keyProperty="id">
    insert into t_user(username, age ,sex ,address) values (#{username},#{age},#{sex},#{address});
</insert>

UseGeneratedKeys 设置为 true 表示将会采用 jdbc 的 Statement 对象的 getGeneratedKeys 方法返回主键,因为 Mybatis 的底层始终是 jdbc 的代码。设置 keyProperty 对于 id 表示用这个 pojo 对象的属性去匹配这个主键,它会将数据库返回的主键值赋值为这个 pojo 对象的属性。测试代码如下:

//添加一个用户数据
@Test
public void testInsertUser1(){
    String statement = "com.thr.mapper.UserMapper.insertUser";
    User user = new User();
    user.setUsername("张三");
    user.setAge(30);
    user.setSex("男");
    user.setAddress("中国北京");
    sqlSession.insert(statement, user);
    //提交插入的数据
    sqlSession.commit();
    sqlSession.close();
    //输出返回的主键只
    System.out.println("插入的主键值为:"+user.getId());
}

4.2 自定义主键

自定义主键,就是自己定义返回的主键值。有时候我们的不想按照数据库自增的规则,例如每次插入主键+2,又或者随机生成数据。

代码如下:

<insert id="insertUser" parameterType="com.thr.entity.User">
    <selectKey keyProperty="id" resultType="int" order="BEFORE">
        select ROUND(RAND()*1000)
    </selectKey>
    insert into t_user(username, age ,sex ,address) values (#{username},#{age},#{sex},#{address});
</insert>

执行的流程是:首先通过 select ROUND(RAND()*1000) 得到主键值,然后将得到的值设置到 user 对象的 id 中,再最后进行 insert 操作。

下面再来介绍一下相关的标签:

  • keyProperty :将查询到的主键设置到 parameterType 指定到对象的那个属性。
  • select ROUND(RAND()*1000) :得到一个随机主键的 id 值,ROUND() 表示获取到小数点后几位 (默认为 0),RAND()*100 表示获取 [0 , 100 ) 之间的任意数字。
  • resultType :指定 select ROUND(RAND()*1000) 的结果类型
  • order :BEFORE,表示在 SQL 语句之前执行还之后执行,可以设置为 BEFORE 或 AFTER。这里是 BEFORE,则表示先执行 select ROUND(RAND()*1000)

5 update 和 delete 元素

Update 元素和 delete 元素在使用上比较简单,所以这里把它们放在一起论述。它们和 insert 元素的属性差不多,执行完后也会返回一个整数,用来表示该 SQL 语句影响了数据库的记录行数。它们二者的使用代码如下所示:

<update id="updateUser" parameterType="com.thr.entity.User">
    update t_user set username = #{username},age = #{age},sex = #{sex},address = #{address} where id = #{id}
</update>
<delete id="deleteUser" parameterType="int">
    delete from t_user where id = #{id}
</delete>

6 sql 元素

Sql 元素是用来定义可重用的 sql 代码片段,这样在字段比较多的时候,以便在其它语句中使用。

<!--定义sql代码片段-->
<sql id="userCols">
    id,username,age,sex,address
</sql>
<!-- 查询所有用户 -->
<select id="selectAllUser" resultType="com.thr.entity.User">
    select <include refid="userCols"/> from t_user
</select>
<!-- 添加用户-->
<insert id="insertUser" parameterType="com.thr.entity.User">
    <selectKey keyProperty="id" resultType="int" order="BEFORE">
        select ROUND(RAND()*1000)
    </selectKey>
    insert into t_user(<include refid="userCols"/>) values (#{id},#{username},#{age},#{sex},#{address});
</insert>

Sql 元素还支持变量的传递,这种方式简单了解即可,代码如下。

<!--定义sql代码片段-->
<sql id="userCols">
    ${alias}.id,${alias}.username,${alias}.age,${alias}.sex,${alias}.address
</sql>
<!-- 查询所有用户 -->
<select id="selectAllUser" resultType="com.thr.entity.User">
    select <include refid="userCols">
              <property name="alias" value="u"/>
           </include>
    from t_user u
</select>

在 include 元素中定义了一个命名为 alias 的变量,其值是表 t_user 的别名 u,然后 sql 元素就能自动识别到对于表的变量名,例如 u.idu.usernameu.age。这种方式对于多表查询很有用,但也用的不多。

7 输入映射 parameterType

7.1 映射基本数据类型

(即八大基本数据类型 + String,比如 int,boolean,long 等类型)

根据 id 查询一个用户:selectUserById,那么传入的就应该是 int 类型的值。所以使用别名 int 来映射传入的值。

<!-- 通过Id查询一个用户 -->
<select id="selectUserById" parameterType="int" resultType="com.thr.entity.User">
    select * from t_user where id = #{id};
</select>

7.2 映射 pojo 类型

(即普通的对象,比如 user 的 java bean 对象)

添加用户:insertUser。这里传入的就是一个 pojo 类型。

<!-- 添加用户-->
<insert id="insertUser" parameterType="com.thr.entity.User">
    insert into t_user(id,username,age,sex,address) values (#{id},#{username},#{age},#{sex},#{address});
</insert>

7.3 包装 pojo 类型

(即内部属性为对象引用,集合等)

那什么是包装 pojo 类型呢?比如如下的代码:

public class QueryVo {
    //有个对象引用,可能是普通的pojo,也有可能是集合
    private User user;
    public User getUser() { return user;}
    public void setUser(User user) { this.user = user;}
}

根据用户名和年龄查询用户信息:selectUserByUserNameAndAge。传入一个包装 pojo 类型,其内部有个属性是 user 的引用。

<!-- 通过username和age查询一个用户 -->
<select id="selectUserByUserNameAndAge" parameterType="com.thr.entity.QueryVo" resultType="com.thr.entity.User">
    select * from t_user where username = #{user.username} and age = #{user.age};
</select>

测试代码:

@Test
public void testSelectUserByUserNameAndAge(){
    String statement = "com.thr.mapper.UserMapper.selectUserByUserNameAndAge";
    QueryVo vo = new QueryVo();
    User user = new User();
    user.setUsername("马保国");
    user.setAge(30);
    vo.setUser(user);
    List<User> listUser = sqlSession.selectList(statement, vo);
    for(User u : listUser){
        System.out.println(u);
    }
    sqlSession.close();
}

注意:user.username 这个属性的获取,因为 QueryVO 是一个包装 pojo,其中有 user 的引用。而 user 中又有 username 的属性,那么这样一层层取过来用即可。

7.4 映射 map 集合

比如返回数据没有合适的数据映射时,可以使用 map 来进行映射。

这个也很简单,理解了前面的,这个不难。就是通过 map 集合设置 key 和 value 的值,然后在映射文件中获取对应的 key 即可 #{key}

<!-- 通过username和age查询一个用户 -->
<select id="selectUserByMap" parameterType="hashmap" resultType="com.thr.entity.User">
    select * from t_user where username = #{username} and age = #{age};
</select>

测试代码:

@Test
public void testSelectUserByMap(){
    String statement = "com.thr.mapper.UserMapper.selectUserByMap";
    HashMap<String, Object> map = new HashMap<>();
    map.put("username","马保国");
    map.put("age",30);
    List<User> listUser = sqlSession.selectList(statement, map);
    for(User u : listUser){
        System.out.println(u);
    }
    sqlSession.close();
}

注意:这里的 hashmap 使用的是别名,mybatis 中内置了。

8 结果映射 resultType

ResultType 为输出结果集类型,同样支持基本数据类型、pojo 类型及 map 集合类型。SQL 语句查询后返回的结果集会映射到配置标签的输出映射属性对应的 Java 类型上。

数据库表中的字段名和表对应实体类的属性名称一致才行 (包括驼峰原则)

输出映射有两种配置,分别是 resultType 和 resultMap,注意两者不能同时使用。

public class User {
  private int id;
  private String username;
  private String hashedPassword;
}

8.1 映射基本数据类型

<select id="countUsers" resultType="int">
    select count(1) from t_user
</select>

8.2 映射 pojo 对象

返回一个的话,使用 selectOne 方法。

<select id="selectUser1ById" parameterType="int" resultType="com.thr.entity.User">
    select * from t_user where id = #{id};
</select>
<select id="selectUser2ById" parameterType="int" resultType="com.thr.entity.User">
    select id, username, hashedPassword from t_user where id = #{id};
</select>

返回多个的话,使用 selectList 方法。

<select id="selectAllUser" resultType="com.thr.entity.User">
    select * from t_user
</select>

使用类别别名简化全限定名多次输入。

<!-- mybatis-config.xml 中 -->
<typeAlias type="com.someapp.model.User" alias="User"/>
 
<!-- SQL 映射 XML 中 -->
<select id="selectUsers" resultType="User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

8.3 映射 hashmap

<select id="selectAllUser" resultType="hashmap">
    select * from t_user
</select>

查询一条记录方法,结果有多条时出错。

Map<Object,Object> retMap = studentDao.selectReturnMap(1002);

查询多条记录方法。

List<HashMap<String, Object>> listUser = sqlSession.selectList(statement);

9 结果映射 resultMap

public class Blog {
    private int id;
    private String title;
    private Author author;
    private List<Post> posts;
}
public class Author implements Serializable {
    protected int id;
    protected String username;
    protected String password;
    protected String email;
    protected String bio;
    protected Section favouriteSection;
}

解决数据库表中的字段名和表对应实体类的属性名称不一致问题注意,不与 resultType 同时使用

9.1 resultMap 使用

<mapper namespace="com.thr.mapper.UserMapper">
    <!-- 配置查询结果的列名和实体类的属性名的对应关系 -->
    <!--id:唯一标识,type:需要映射的java类型-->
    <resultMap id="userMap" type="com.thr.entity.User">
        <!-- 与主键字段的对应,  property对应实体属性,  column对应表字段 -->
        <id property="userId" column="id"/>
        <!-- 与非主键字段的对应,  property对应实体类属性,  column对应表字段 -->
        <result property="userName" column="username"/>
        <result property="userAge" column="age"/>
        <result property="userSex" column="sex"/>
        <result property="userAddress" column="address"/>
    </resultMap>
    <!-- 查询所有用户,返回集为resultMap类型,resultMap的value上面配置的id=userMap要一致-->
    <select id="selectAllUser" resultMap="userMap">
        select id, username, age, sex, address from t_user
    </select>
</mapper>

9.2 使用 SQL as 进行同类替代

as 可以省略?

<select id="selectUsers" resultType="User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from some_table
  where id = #{id}
</select>

9.3 配置映射关系

若字段名和实体类中的属性名不一致,但是字段名符合数据库的规则(使用_),实体类中的属性名符合 Java 的规则(使用驼峰)。此时也可通过以下两种方式处理字段名和实体类中的属性的映射关系

  1. 可以通过为字段起别名的方式,保证和实体类中的属性名保持一致
    <!--List<Emp> getAllEmp();-->
    <select id="getAllEmp" resultType="Emp">
    	select eid,emp_name empName,age,sex,email from t_emp
    </select>
  2. 可以在 MyBatis 的核心配置文件中的 setting 标签中,设置一个全局配置信息 mapUnderscoreToCamelCase,可以在查询表中数据时,自动将_类型的字段名转换为驼峰,例如:字段名 user_name,设置了 mapUnderscoreToCamelCase,此时字段名就会转换为 userName。核心配置文件详解
    `xml
`

9.4 resultMap 元素属性简介

额外,resultMap 还有高级映射功能,还可以实现将查询结果映射为复杂类型的 pojo 类型,比如在查询结果映射对象中包括 pojo 和 list 实现一对一查询和一对多查询,这个会在后面单独进行详细的介绍,因为这个点非常非常非常重要,所以这里不多说。我们下面来详细介绍一下 resultMap 元素。

<resultMap id="" type="" extends="" autoMapping="">
    <constructor><!--构造器注入属性值-->
        <idArg/>
        <arg/>
    </constructor>
    <id/><!--主键的映射规则-->
    <result/><!--非主键的映射规则-->
    <association/><!--高级映射-->
    <collection /><!--高级映射-->
    <discriminator>
        <case/>
    </discriminator><!--根据返回的字段的值封装不同的类型-->
</resultMap>

ResultMap 的属性

  • id:该封装规则的唯一标识。
  • type:类的完全限定名, 或者一个类型别名。
  • autoMapping:如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。
  • extends:继承其他封装规则,和 Java 中的继承一样。

ResultMap 子元素

  • constructor - 用于在实例化类时,注入结果到构造方法中
  • id – 主键映射
  • result – 非主键映射
  • association – 一对一类型
    • 嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的引用
  • collection – 一对多类型
    • 嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的引用
  • discriminator – 使用结果值来决定使用哪个 resultMap
    • case – 基于某些值的结果映射
      • 嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射

9.5 constructor 子元素

构造方法。有时候可能想要生成一个不可改变类,通过构造方法的形式传入私有值,不暴露公有方法。

constructor 的子元素

  • idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
  • arg - 将被注入到构造方法的一个普通结果

idArg & arg 的属性:

  • column & name & javaType & typeHandler & select & resultMap
public class User {
   public User(Integer id, String username, int age) {  }
}
<constructor>
   <idArg column="id" javaType="int"/>
   <arg column="username" javaType="String"/>
   <arg column="age" javaType="_int"/>
</constructor>

3.4.3 开始,通过 name 来引用构造方法参数,你可以添加 @Param 注解,或者使用 ‘-parameters’ 编译选项并启用 useActualParamName 选项(默认开启)来编译项目。

<constructor>
   <idArg column="id" javaType="int" name="id" />
   <arg column="age" javaType="_int" name="age" />
   <arg column="username" javaType="String" name="username" />
</constructor>

9.6 id & result 子元素

id – 主键映射
result – 非主键映射

属性:

  • property :指定 javabean 的属性名
  • column :指定数据库字段名或者其别名(这个别名是数据库起的,如 username as name)
  • jdbcType :映射 java 的类型
  • javaType :映射数据库类型
  • typeHandler :数据库与 Java 类型匹配处理器(可以参考前面的 TypeHandler 部分)
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
<result property="favouriteSection" column="author_favourite_section"/>

9.7 association 子元素

MyBatis 关联映射

关联(association)元素处理一对一映射规则。比如,在我们的示例中,一个博客有一个用户。

关联需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:

  • 嵌套 select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
  • 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。

association 子元素的属性

  • property & javaType & jdbcType & typeHandler

9.7.1 select 嵌套查询/分步查询

补充属性

  • column关联查询语句所需要的列名。 一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
  • select。关联 SQL 语句的 id 编号,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。具体请参考下面的例子。注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
  • fetchType。可选的。有效值为 lazyeager。指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值。

代码示例

<resultMap id="blogResult" type="Blog">
  <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
 
<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>
 
<select id="selectAuthor" resultType="Author">
  SELECT * FROM AUTHOR WHERE ID = #{id}
</select>

查员工信息,再查部门信息(不同 mapper 文件的示例):

<resultMap id="empAndDeptByStepResultMap" type="Emp">
	<id property="eid" column="eid"></id>
	<result property="empName" column="emp_name"></result>
	<result property="age" column="age"></result>
	<result property="sex" column="sex"></result>
	<result property="email" column="email"></result>
	<association property="dept"
				 select="com.atguigu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo"
				 column="did"></association>
</resultMap>
<!--Emp getEmpAndDeptByStepOne(@Param("eid") Integer eid);-->
<select id="getEmpAndDeptByStepOne" resultMap="empAndDeptByStepResultMap">
	select * from t_emp where eid = #{eid}
</select>
 
<!--此处的resultMap仅是处理字段和属性的映射关系-->
<resultMap id="EmpAndDeptByStepTwoResultMap" type="Dept">
	<id property="did" column="did"></id>
	<result property="deptName" column="dept_name"></result>
</resultMap>
<!--Dept getEmpAndDeptByStepTwo(@Param("did") Integer did);-->
<select id="getEmpAndDeptByStepTwo" resultMap="EmpAndDeptByStepTwoResultMap">
	select * from t_dept where did = #{did}
</select>

实际分析

这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为“N+1 查询问题”。概括地讲,N+1 查询问题是这样子的:

  • 你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。
  • 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。

缺点:这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。

优点:好消息是,MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来。然而,如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕。

9.7.2 嵌套结果映射

补充属性

  • resultMap。结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet。这样的 ResultSet 有部分数据是重复的。为了将结果集正确地映射到嵌套的对象树中, MyBatis 允许你“串联”结果映射,以便解决嵌套结果集的问题。使用嵌套结果映射的一个例子在表格以后。
  • columnPrefix。当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。详细说明请参考后面的例子。
  • notNullColumn。默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列非空时才创建一个子对象。可以使用逗号分隔来指定多个列。默认值:未设置(unset)。
  • autoMapping。如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。这个属性会覆盖全局的属性 autoMappingBehavior。注意,本属性对外部的结果映射无效,所以不能搭配 selectresultMap 元素使用。默认值:未设置(unset)。

代码示例

一个表 Blog,记录博客的信息(博客 id、标题、作者 id)
一个表 Author,记录作者信息(作者 id、用户名、密码、邮箱等)
Java 对象 Blog,记录博客的信息(博客 id、标题、Author 类)

目的:寻找 Blog id 为特定值的结果,将其保存在 Java 对象 Blog 中

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>
 
<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>
 
<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    B.author_id     as blog_author_id,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>

非常重要: id 元素在嵌套结果映射中扮演着非常重要的角色。你应该总是指定一个或多个可以唯一标识结果的属性。虽然,即使不指定这个属性,MyBatis 仍然可以工作,但是会产生严重的性能问题。只需要指定可以唯一标识结果的最少属性。显然,你可以选择主键(复合主键也可以)。

不喜欢重用 authorResult 的 resultMap,或者喜欢直接嵌套编写,可以使用下述方法。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
  </association>
</resultMap>

同一类型的多个对象问题

一篇博客有作者 A 和作者 B,即存在共同作者,但是并非使用集合的方式来存储数据的。那么可以使用 columnPrefix 属性来实现。

public class Blog {
  private Integer id;
  private String title;
  private Author author;
  private Author coAuthor;
}
<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author"
    resultMap="authorResult" />
  <association property="coAuthor"
    resultMap="authorResult"
    columnPrefix="co_" />
</resultMap>
 
<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio,
    CA.id           as co_author_id,
    CA.username     as co_author_username,
    CA.password     as co_author_password,
    CA.email        as co_author_email,
    CA.bio          as co_author_bio
  from Blog B
  left outer join Author A on B.author_id = A.id
  left outer join Author CA on B.co_author_id = CA.id
  where B.id = #{id}
</select>

9.7.3 多结果集(ResultSet)

某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。

补充属性

  • column。当使用多个结果集时,该属性指定结果集中用于与 foreignColumn 匹配的列(多个列名以逗号隔开),以识别关系中的父类型与子类型。
  • foreignColumn。指定外键对应的列名,指定的列将与父类型中 column 的给出的列进行匹配。
  • resultSet。指定用于加载复杂类型的结果集名字。

在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <result property="email" column="email"/>
    <result property="bio" column="bio"/>
  </association>
</resultMap>
 
<!--映射语句-->
<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
  {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>

9.8 collection 子元素

MyBatis 关联映射

collection 子元素用于处理一对多的关系。collection 元素和 association 元素几乎是一样的,只介绍不同点。

private List<Post> posts;

9.8.1 集合的嵌套 select 查询

基于关联属性,补充属性

  • ofType。集合中存储的对象。

代码示例

在一般情况下,MyBatis 可以推断 javaType 属性,因此并不需要填写。

<resultMap id="blogResult" type="Blog">
  <collection property="posts" column="id" javaType="ArrayList" ofType="Post" select="selectPostsForBlog"/>
  <!--<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>-->
</resultMap>
 
<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>
 
<select id="selectPostsForBlog" resultType="Post">
  SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

9.8.2 集合的嵌套结果映射

<select id="selectBlog" resultMap="blogResult">
  select
  B.id as blog_id,
  B.title as blog_title,
  B.author_id as blog_author_id,
  P.id as post_id,
  P.subject as post_subject,
  P.body as post_body,
  from Blog B
  left outer join Post P on B.id = P.blog_id
  where B.id = #{id}
</select>
 
<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <result property="body" column="post_body"/>
  </collection>
</resultMap>

将集合嵌套部分分开写也可以:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>
 
<resultMap id="blogPostResult" type="Post">
  <id property="id" column="id"/>
  <result property="subject" column="subject"/>
  <result property="body" column="body"/>
</resultMap>

9.8.3 集合的多结果集(ResultSet)

在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。

<select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
  {call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
</select>
 
<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">
    <id property="id" column="id"/>
    <result property="subject" column="subject"/>
    <result property="body" column="body"/>
  </collection>
</resultMap>

注意:对关联或集合的映射,并没有深度、广度或组合上的要求。但在映射时要留意性能问题。在探索最佳实践的过程中,应用的单元测试和性能测试会是你的好帮手。而 MyBatis 的好处在于,可以在不对你的代码引入重大变更(如果有)的情况下,允许你之后改变你的想法。

9.9 discriminator 子元素

鉴别器(discriminator)元素就是被设计来应对,数据库查询可能会返回多个不同的结果集;另外也能处理其它情况,例如类的继承层次结构。鉴别器类似于 Java 中的 switch 语句。

一个鉴别器的定义需要指定 columnjavaType 属性。column 指定了 MyBatis 查询被比较值的地方。而 javaType 用来确保使用正确的相等测试(虽然很多情况下字符串的相等测试都可以工作)。

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultMap="carResult"/>
    <case value="2" resultMap="truckResult"/>
    <case value="3" resultMap="vanResult"/>
    <case value="4" resultMap="suvResult"/>
  </discriminator>
</resultMap>

剩余的结果映射将被忽略(除非它是扩展的,我们将在稍后讨论它)。如果不能匹配任何一个 case,MyBatis 就只会使用鉴别器块外定义的结果映射。

<resultMap id="carResult" type="Car"> <result property="doorCount" column="door_count" /> </resultMap>

那么只有 doorCount 属性会被加载。这是为了即使鉴别器的 case 之间都能分为完全独立的一组,尽管和父结果映射可能没有什么关系。在上面的例子中,我们当然知道 cars 和 vehicles 之间有关系,也就是 Car 是一个 Vehicle。因此,我们希望剩余的属性也能被加载。而这只需要一个小修改。现在 vehicleResult 和 carResult 的属性都会被加载了。

<resultMap id="carResult" type="Car" extends="vehicleResult">
  <result property="doorCount" column="door_count" />
</resultMap>

也可以使用嵌套来完成上述内容。

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultType="carResult">
      <result property="doorCount" column="door_count" />
    </case>
    <case value="2" resultType="truckResult">
      <result property="boxSize" column="box_size" />
      <result property="extendedCab" column="extended_cab" />
    </case>
    <case value="3" resultType="vanResult">
      <result property="powerSlidingDoor" column="power_sliding_door" />
    </case>
    <case value="4" resultType="suvResult">
      <result property="allWheelDrive" column="all_wheel_drive" />
    </case>
  </discriminator>
</resultMap>

9.10 自动映射属性

当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性。

通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true。

甚至在提供了结果映射后,自动映射也能工作。在这种情况下,对于每一个结果映射,在 ResultSet 出现的列,如果没有设置手动映射,将被自动映射。在自动映射处理完毕后,再处理手动映射。在下面的例子中,iduserName 列将被自动映射,hashed_password 列将根据配置进行映射。

<select id="selectUsers" resultMap="userResultMap">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password
  from some_table
  where id = #{id}
</select>
 
<resultMap id="userResultMap" type="User">
  <result property="password" column="hashed_password"/>
</resultMap>

有三种自动映射等级:

  • NONE - 禁用自动映射。仅对手动映射的属性进行映射。
  • PARTIAL - 默认。对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射。
  • FULL - 自动映射所有属性。

使用 FULL 来进行自动映射时,如果 A 和 B 表关联,A 表中存在列 id,B 表中同样存在列 id,那么会出现问题。

<resultMap id="blogResult" type="Blog">
  <association property="author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
  <result property="username" column="author_username"/>
</resultMap>
 
<select id="selectBlog" resultMap="blogResult">
  select
    B.id,
    B.title,
    A.username,
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>

在结果映射上设置 autoMapping 属性来为指定的结果映射设置启用/禁用自动映射。

<resultMap id="userResultMap" type="User" autoMapping="false">
  <result property="password" column="hashed_password"/>
</resultMap>

10 cache 元素

具体演示,可以查看 MyBatis 缓存

默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:<cache/>

这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

属性介绍

  • eviction 可用的清除策略有:
    • LRU – 最近最少使用:默认。移除最长时间不被使用的对象。
    • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
    • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
  • flushInterval (刷新间隔)属性-以毫秒为单位的任意正整数。默认情况是不设置,即没有刷新间隔,缓存仅仅会在调用语句时刷新。
  • size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
  • readOnly (只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。速度上会慢一些,但是更安全,因此默认值是 false。

提示:二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

自定义缓存

<cache type="com.domain.something.MyCustomCache"/>

type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器。

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}

为了对你的缓存进行配置,只需要简单地在你的缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值,例如,下面的例子将在你的缓存实现上调用一个名为 setCacheFile(String file) 的方法:

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。你也可以使用占位符(如 ${cache.file} ),以便替换成在配置文件属性中定义的值。

从版本 3.4.2 开始,MyBatis 已经支持在所有属性设置完毕之后,调用一个初始化方法。如果想要使用这个特性,在自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject 接口。

public interface InitializingObject {
  void initialize() throws Exception;
}

上一节中对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存。

11 cache-ref 元素

在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

12 支持的 JDBC 类型

为了以后可能的使用场景,MyBatis 通过内置的 jdbcType 枚举类型支持下面的 JDBC 类型。

BITFLOATCHARTIMESTAMPOTHERUNDEFINED
TINYINTREALVARCHARBINARYBLOBNVARCHAR
SMALLINTDOUBLELONGVARCHARVARBINARYCLOBNCHAR
INTEGERNUMERICDATELONGVARBINARYBOOLEANNCLOB
BIGINTDECIMALTIMENULLCURSORARRAY