MyBatis 映射文件之结果集映射总结

774 阅读21分钟

1. 简单结果映射

1.1. 使用 Map

直接将返回结果映射到一个map中:

<select id="selectUsers" resultType="map">
  SELECT id, username, hashedPassword FROM some_table WHERE id = #{id}
</select>

上述语句只是简单地将所有的列映射到 HashMap 的键上,这由resultType属性指定。虽然在大部分情况下都够用,但是 HashMap 不是一个很好的领域模型。

使用Map有以下注意事项:

(1)值为NULL的字段将不会返回,该字段名称(key)在结果map中将不存在。

(2)主键字段无论用的是int还是bigint类型,其值在结果map中都是java.lang.Long类型。

(3)非主键字段:

  • tinyintsmallintmediumintint类型字段,值在结果map中都是java.lang.Integer类型;
  • bigint类型字段,值在结果map中是java.lang.Long类型;
  • charvarchar类型字段,值在结果map中都是java.lang.String类型;
  • datetimetimestamp类型字段,值在结果map中都是java.sql.Timestamp类型。
  • float类型字段,值在结果map中是java.lang.Float类型;
  • double类型字段,值在结果map中是java.lang.Double类型;

(4)tinyint(1)类型字段无论存储的是多位数字还是一位数字,则该字段值在结果map中默认都会转为java.lang.Boolean类型,并且非0值为true0值为false。如果想避免这个,可以使用下面几种方法解决:

  • 在 SQL 上对该字段使用函数,则该字段值在结果map中会变为java.lang.Long,比如使用IFNULL(tinyint1_column_name, 0)
  • 在数据库连接的url后添加&tinyInt1isBit=false。这样 MyBatis 就不会对该类型数据进行转换了。

最好避免使用tinyint(1)类型的字段存储表示布尔型数据之外的其他数据。直接在 JavaBean 中使用java.lang.Boolean类型字段接收该类型字段的值,非0值为true0值为false

(5)SQL 中如果使用了COUNT操作,则对应的结果字段在结果map中是java.lang.Long

1.2. 使用 JavaBean

使用 JavaBean 或 POJO(Plain Old Java Objects,普通 Java 对象)作为领域模型。比如有下面这个 JavaBean:

package com.example.demo.model;
import lombok.Data;
@Data
public class User {
    private int id;
    private String username;
    private String hashedPassword;
}

将查询结果映射到这个 JavaBean:

<select id="selectUsers" resultType="com.example.demo.model.User">
  SELECT id, username, hashedPassword FROM some_table WHERE id = #{id}
</select>

如果列名和属性名没有精确匹配,可以在SELECT语句中对列使用别名(这是一个基本的 SQL 特性)来匹配标签。比如:

<select id="selectUsers" resultType="com.example.demo.model.User">
  SELECT
    user_id             AS id,
    user_name           AS userName,
    hashed_password     AS hashedPassword
  FROM some_table WHERE id = #{id}
</select>

2. 高级结果映射

高级结果映射是通过resultMap实现的。

resultMap标签的属性列表:

属性描述
id当前命名空间中的一个唯一标识,用于标识一个结果映射。
type类的完全限定名, 或者一个类型别名。
autoMapping如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性autoMappingBehavior。默认值为未设置(unset)。

resultMap标签的子标签列表:

标签描述
id一个ID结果;标记出作为ID的结果可以帮助提高整体性能。
result注入到字段或 JavaBean 属性的普通结果。
constructor用于在实例化类时,注入结果到构造方法中。
association用来处理一对一的关联。关联本身可以是一个resultMap元素,或者从别处引用一个。
collection用来处理一对多的关联。集合本身可以是一个resultMap元素,或者从别处引用一个。
discriminator使用结果值来决定使用哪个resultMap

下面分别讲一下各个子标签的使用。

2.1. id & result

这是结果映射最基本的内容。idresult元素都是将一个列的值映射到一个简单数据类型(String, int, double, Date等)的字段上。这两者之间的唯一区别是,id元素表示的结果将是对象的标识属性,这会在比较对象实例时用到。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。

idresult标签的属性列表:

属性描述
property映射到列结果的字段或属性。如果用来匹配的 JavaBean 存在给定名字的属性,那么它将会被使用。否则 MyBatis 将会寻找给定名称的字段。无论是哪一种情形,你都可以使用通常的点式分隔形式进行复杂属性导航。
比如,你可以这样映射一些简单的东西:"username",或者映射到一些复杂的东西上:"address.street.number"
column数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName)方法的参数一样。
javaType一个 Java 类的完全限定名,或一个类型别名。如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定javaType来保证行为与期望的相一致。
jdbcTypeJDBC类型,所支持的JDBC类型参见附录。只需要在可能执行插入、更新和删除的且允许空值的列上指定JDBC类型。这是JDBC的要求而非 MyBatis 的要求。如果你直接面向JDBC编程,你需要对可能存在空值的列指定这个类型。
typeHandler类型处理器。使用这个属性,你可以覆盖默认的类型处理器。这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。

使用idresult标签可以直接解决列名不匹配的情况,先定义一个resultMap

<resultMap id="userResultMap" type="com.example.demo.model.User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>

这里直接使用即可:

<select id="selectUsers" resultMap="userResultMap">
  SELECT user_id, user_name, hashed_password FROM some_table WHERE id = #{id}
</select>

2.2. constructor

通过修改对象属性的方式,可以满足大多数的数据传输对象(Data Transfer Object, DTO)以及绝大部分领域模型的要求。但有些情况下你想使用不可变类。 一般来说,很少改变或基本不变的包含引用或数据的表,很适合使用不可变类。 构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出各个属性的公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入。constructor标签就是为此而生的。

constructor标签的子标签列表:

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

idArgarg标签的属性列表:

属性描述
column数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName)方法的参数一样。
javaType一个 Java 类的完全限定名,或一个类型别名。如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定javaType来保证行为与期望的相一致。
jdbcTypeJDBC类型,所支持的JDBC类型参见附录。只需要在可能执行插入、更新和删除的且允许空值的列上指定JDBC类型。这是JDBC的要求而非 MyBatis 的要求。如果你直接面向JDBC编程,你需要对可能存在空值的列指定这个类型。
typeHandler类型处理器。使用这个属性,你可以覆盖默认的类型处理器。这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。
select用于加载复杂类型属性的映射语句的ID,它会从column属性中指定的列检索数据,作为参数传递给此select语句。具体请参考association标签。
resultMap结果映射的ID,可以将嵌套的结果集映射到一个合适的对象树中。它可以作为使用额外select语句的替代方案。它可以将多表连接操作的结果映射成一个单一的ResultSet。这样的ResultSet将会将包含重复或部分数据重复的结果集。为了将结果集正确地映射到嵌套的对象树中,MyBatis 允许你“串联”结果映射,以便解决嵌套结果集的问题。想了解更多内容,请参考下面的association标签。
name构造方法形参的名字。从3.4.3版本开始,通过指定具体的参数名,你可以以任意顺序写入arg元素。

看看下面这个类中的构造方法:

package com.example.demo.model;
import org.apache.ibatis.annotations.Param;
public class Author {
    public Author( Long id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }
    private Long id;
    private String username;
    private String password;
    private String email;
    private String bio;
}

为了将结果注入构造方法,MyBatis 需要通过某种方式定位相应的构造方法。在下面的例子中,MyBatis 搜索一个声明了三个形参的的构造方法,参数类型的顺序是java.lang.Integer, java.lang.Stringjava.lang.String 。会按照构造方法中的形参顺序进行赋值。

<select id="getAuthor" resultMap="buildAuthorByCustomer">
    SELECT * FROM Author WHERE id = 1
</select>
<resultMap id="buildAuthorByCustomer" type="com.example.demo.model.Author">
    <constructor>
        <idArg column="id" javaType="long" name="id"/>
        <arg column="username" javaType="string" />
        <arg column="password" javaType="string" />
    </constructor>
</resultMap>

当处理一个带有多个形参的构造方法时,很容易搞乱arg元素的顺序。 从版本3.4.3开始,可以在指定参数名称的前提下,以任意顺序编写arg元素。为了通过名称来引用构造方法的参数,你可以在参数上添加@Param 注解,或者使用'-parameters'编译选项并启用 useActualParamName选项(默认开启)来编译项目。

构造方法修改如下,添加@Param注解:

package com.example.demo.model;
import org.apache.ibatis.annotations.Param;
public class Author {
    public Author(@Param("id") Long id,
                  @Param("username") String username,
                  @Param("password") String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }
    private Long id;
    private String username;
    private String password;
    private String email;
    private String bio;
}

调换usernamepassword的顺序,如果没有添加@Param注解和name属性,则会赋值顺序错误,导致usernamepassword的值互换。下面虽然调换了arg顺序,但是结果是正确的:

<resultMap id="buildAuthorByCustomer" type="com.example.demo.model.Author">
    <constructor>
        <idArg column="id" javaType="long" name="id"/>
        <arg column="password" javaType="string" name="password"/>
        <arg column="username" javaType="string" name="username"/>
    </constructor>
</resultMap>

2.3. association

关联标签用于处理一对一类型的关系。比如,一个博客对应一个用户。MyBatis 有两种不同的方式加载关联:

(1)嵌套select查询:通过执行另外一个 SQL 映射语句来返回预期的复杂类型。

(2)嵌套结果映射:使用嵌套结果映射来处理重复的联合结果的子集。

属性描述
property映射到列结果的字段或属性。如果用来匹配的 JavaBean 存在给定名字的属性,那么它将会被使用。否则 MyBatis 将会寻找给定名称的字段。无论是哪一种情形,你都可以使用通常的点式分隔形式进行复杂属性导航。
比如,你可以这样映射一些简单的东西:"username",或者映射到一些复杂的东西上:"address.street.number"
javaType一个 Java 类的完全限定名,或一个类型别名。如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定javaType来保证行为与期望的相一致。
jdbcTypeJDBC类型,所支持的JDBC类型参见附录。只需要在可能执行插入、更新和删除的且允许空值的列上指定JDBC类型。这是JDBC的要求而非 MyBatis 的要求。如果你直接面向JDBC编程,你需要对可能存在空值的列指定这个类型。
typeHandler类型处理器。使用这个属性,你可以覆盖默认的类型处理器。这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。

下面进行举例说明,首先定义出下面内容。

JavaBean 的内容:

@Data
public class Blog {
    private Long id;
    private String title;
    private Author author;
}

@Data
public class Author {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String bio;
}

Controller层代码:

public class BlogController {
    @Autowired
    private BlogMapper blogMapper;

    @GetMapping("/selectBlog")
    public Blog selectBlog(@RequestParam("id") Long blogId){
        return blogMapper.selectBlog(blogId);
    }
}

Mapper接口:

@Mapper
public interface BlogMapper {
    Blog selectBlog(Long id);
}

访问URL

http://localhost:8080/selectBlog?id=1

查询结果:

{
	"id": 1,
	"title": "测试博客",
	"author": {
		"id": 2,
		"username": "张三",
		"password": "123456",
		"email": null,
		"bio": null
	}
}

2.3.1. 嵌套 select 查询

属性描述
column数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName)方法的参数一样。注意:在使用复合主键的时候,你可以使用column="{prop1=col1,prop2=col2}"这样的语法来指定多个传递给嵌套select查询语句的列名。这会使得prop1prop2作为参数对象,被设置为对应嵌套select语句的参数。
select用于加载复杂类型属性的映射语句的ID,它会从column属性指定的列中检索数据,作为参数传递给目标select语句。具体请参考下面的例子。注意:在使用复合主键的时候,你可以使用column="{prop1=col1,prop2=col2}"这样的语法来指定多个传递给嵌套select查询语句的列名。这会使得prop1prop2作为参数对象,被设置为对应嵌套 select语句的参数。
fetchType可选的。有效值为lazyeager。指定属性后,将在映射中忽略全局配置参数lazyLoadingEnabled,使用该属性的值。
<select id="selectBlog" resultMap="blogResult">
    SELECT * FROM Blog WHERE id = #{id}
</select>

<resultMap id="blogResult" type="Blog">
    <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

<select id="selectAuthor" resultType="Author">
  SELECT * FROM Author WHERE id = #{id}
</select>

上面的示例会发送两条SQL

SELECT * FROM Blog WHERE id = 1
SELECT * FROM Author WHERE id = 2

我们有两个select查询语句:一个用来加载博客(Blog),另外一个用来加载作者(Author),而且博客的结果映射描述了应该使用 selectAuthor语句加载它的author属性。其它属性将会被自动加载,只要它们的列名和属性名相匹配即可

N+1问题:

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

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

这个问题可能会导致成百上千的SQL语句被执行。我们不希望产生这样的后果。好消息是 MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来。然而,如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕。所以这种情况下就需要采用另外一种方法。

2.3.2. 嵌套结果映射

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

现在我们将博客表和作者表连接在一起,使用一条SQL语句,而不是执行两个独立的查询语句:

<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
  FROM Blog B LEFT OUTER JOIN Author A ON B.author_id = A.id
  WHERE B.id = #{id}
</select>

嵌套结果映射:

<resultMap id="blogResult" type="Blog">
    <id property="id" column="blog_id" />
    <result property="title" column="blog_title"/>
    <association property="author" 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>

在上面的例子中,你可以看到,博客(Blog)作者(Author)的关联元素委托名为authorResult的结果映射来加载作者对象的实例。

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

上面的示例使用了外部的结果映射元素来映射关联。这使得Author的结果映射可以被重用。如果你不打算重用它,或者你更喜欢将你所有的结果映射放在一个具有描述性的结果映射元素中。你可以直接将Author的结果映射作为子元素嵌套在内。下面给出使用这种方式的等效例子:

<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>

columnPrefixs 使用示例:

如果博客(blog)有一个作者(author),还有一个共同作者(co-author)。这时select语句看起来会是这样的:

<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>

Author的结果映射定义如下:

<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>

由于结果中的列名与结果映射中的列名不同。你需要指定columnPrefix以便重复使用该结果映射来映射co-author的结果。

<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>

2.3.3. 多结果集

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

从版本3.2.3开始,MyBatis 提供了另一种解决N+1查询问题的方法。

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

在例子中,存储过程执行下面的查询并返回两个结果集。第一个结果集会返回博客(Blog)的结果,第二个则返回作者(Author)的结果。

SELECT * FROM Blog WHERE id = #{id}
SELECT * FROM Author WHERE id = #{id}

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

<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
  {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>

指定使用authors结果集的数据来填充author关联:

<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>

2.3.4. 简单映射(推荐使用)

官方文档没有提及的一种映射方式,这是最简单的实现,不需要使用resultMap,直接使用resultType即可。

<select id="selectBlog" resultType="Blog">
  SELECT
    B.id,
    B.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"
  FROM Blog B LEFT OUTER JOIN Author A ON B.author_id = A.id
  WHERE B.id = #{id}
</select>

2.4. collection

集合标签用于处理一对多类型的关系。继续上面的示例,一个博客(Blog)只有一个作者(Author),但一个博客有很多文章(Post)。现在准备如下model

@Data
public class Blog {
    private Long id;
    private String title;
    private Author author;
    private List<Post> posts;
}

@Data
public class Post {
    private Long id;
    private String subject;
    private String body;
    private Long blog_id;
}

查询出的结果:

{
	"id": 1,
	"title": "测试博客",
	"author": null,
	"posts": [{
		"id": 1,
		"subject": "博文1",
		"body": "博文1的正文",
		"blog_id": 1
	}, {
		"id": 2,
		"subject": "博文2",
		"body": "博文2的正文",
		"blog_id": 1
	}]
}

2.4.1. 嵌套 select 查询

属性描述
ofType这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。
<select id="selectBlog" resultMap="blogResult">
    SELECT * FROM Blog WHERE id = #{id}
</select>

<resultMap id="blogResult" type="Blog">
    <id property="id" column="id" />
    <collection property="posts" column="id"  javaType="ArrayList" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectPostsForBlog" resultType="Post">
  SELECT * FROM Post WHERE blog_id = #{id}
</select>

上面集合collection中的javaType属性可以省略,MyBatis 一般可以推断出来。集合可以读为posts 是一个存储 Post 类型的 ArrayList 集合

这里还需要注意,因为columnid的列赋值给了属性posts,所以属性id就不会被自动赋值了,会变为null,这里添加上<id property="id" column="id" />,属性id才会被赋值。(这里指的是Blogid)。

2.4.2. 嵌套结果映射(推荐使用)

SQL 语句:

<select id="selectBlog" resultMap="blogResult">
  SELECT
  B.id        AS blog_id,
  B.title     AS blog_title,
  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" 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>

内部嵌套:

<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>

2.4.3. 多结果集

像关联标签那样,我们可以通过执行存储过程实现,它会执行两个查询并返回两个结果集,一个是博客的结果集,另一个是文章的结果集:

SELECT * FROM Blog WHERE id = #{id}
SELECT * FROM Post WHERE blog_id = #{id}

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

<select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
  {call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
</select>

我们指定“posts”集合将会使用存储在“posts”结果集中的数据进行填充:

<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>

注意:对关联或集合的映射,并没有深度、广度或组合上的要求。但在映射时要留意性能问题。

2.5. discriminator

discriminator标签的子标签列表:

元素描述
case基于某些值的结果映射。case本身可以是一个resultMap元素,因此可以具有相同的结构和元素,或者从别处引用一个。

一个数据库查询可能会返回多个不同的结果集。 鉴别器(discriminator)标签就是被设计来应对这种情况的,另外也能处理其它情况,例如类的继承层次结构。鉴别器的概念很好理解——它很像 Java 语言中的switch语句。

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

<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>

MyBatis 通过从结果集中比较每条记录的vehicle_type值,来判断该条记录匹配鉴别器的哪个case,如果匹配到某一个case,就会直接使用这个case指定的结果映射。也就是说,剩余的结果映射将被忽略(除非它是扩展的,我们将在稍后讨论它)。如果不能匹配任何一个case,MyBatis 就只会使用鉴别器块外部定义的结果映射。

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

例如匹配到了value=1casecarResult的声明如上,那么只有doorCount属性会被加载,其他 case指定的结果映射不会被加载,鉴别器块外部定义的结果映射也不会被加载。

我们知道carsvehicles之间是有关系的,Car是一个Vehicle。因此,我们希望剩余的属性也能被加载。这里只需要添加extends属性即可。这样vehicleResultcarResult的属性就都会被加载了。

<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>

3. 自动映射

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

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

有三种自动映射等级:

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

默认值是PARTIAL,这是有原因的。当对连接查询的结果使用FULL时,连接查询会在同一行中获取多个不同实体的数据,因此可能导致非预期的映射。无论设置的自动映射等级是哪种,都可以通过在结果映射上设置autoMapping属性来为指定的结果映射设置启用 / 禁用自动映射。

4. 附录

4.1. 支持的 JDBC 类型列表

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

abcdef
BITFLOATCHARTIMESTAMPOTHERUNDEFINED
TINYINTREALVARCHARBINARYBLOBNVARCHAR
SMALLINTDOUBLELONGVARCHARVARBINARYCLOBNCHAR
INTEGERNUMERICDATELONGVARBINARYBOOLEANNCLOB
BIGINTDECIMALTIMENULLCURSORARRAY