深入了解 MyBatis :ResultMap

这是我的 MyBatis 学习笔记的第三篇:深入了解 MyBatis 之 ResultMap 。


使用 Map 接收结果集

resultMap 元素是 MyBatis 中最重要最强大的元素。

它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。

实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的数千行代码。

ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

之前你已经见过简单映射语句的示例,它们没有显式指定 resultMap。比如:

1
2
3
4
5
<select id="getUser" resultType="map">
SELECT id, username, email
FROM t_user
WHERE id = #{id}
</select>

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

你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 对象)作为领域模型。MyBatis 对两者都提供了支持。


使用 JavaBean 接收结果集

看看下面这个 JavaBean:

1
2
3
4
5
6
7
package com.peterxx.entity;
public class User {
private int id;
private String username;
private String email;
// 忽略 setter 和 getter ...
}

基于 JavaBean 的规范,上面这个类有 3 个属性:id,username 和 email。这些属性会对应到 select 语句中的列名。

这样的一个 JavaBean 可以被映射到 ResultSet,就像映射到 HashMap 一样简单。

1
2
3
4
5
<select id="getUser" resultType="com.peterxx.entity.User">
SELECT id, username, email
FROM t_user
WHERE id = #{id}
</select>

类型别名是你的好帮手。使用它们,你就可以不用输入类的全限定名了。比如:

1
2
3
4
5
6
7
8
9
<!-- mybatis-config.xml 中 -->
<typeAlias type="com.peterxx.entity.User" alias="User"/>

<!-- SQL 映射 XML 中 -->
<select id="getUser" resultType="User">
SELECT id, username, email
FROM t_user
WHERE id = #{id}
</select>

对于属性名(java层)和字段名(jdbc层)一致的情况下,仍然能够保持和使用 map 一样简单。


自定义 ResultMap 处理结果集

如果属性名和字段名不一样怎么办?一个较为愚蠢的方法是在 SQL 中使用别名:

1
2
3
4
5
6
7
8
<select id="getUser" resultType="User">
SELECT
user_id as "id",
user_name as "username",
user_email as "email"
FROM t_user
WHERE id = #{id}
</select>

虽然似乎利用了 SQL 的别名来避免使用 ResultMap ,但其实在底层并没有绕过 ResultMap

只要使用 JavaBean 来接收结果集,MyBatis 会在幕后自动创建一个 ResultMap

这就是 ResultMap 的优秀之处:你完全可以不用显式地配置它们。

那么如果需要显式使用 ResultMap 是怎么样的呢?假如我们不想通过 SQL 的别名来解决问题。

我们可以如下声明一个 ResultMap

1
2
3
4
5
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="email" column="user_email"/>
</resultMap>

然后再使用 resultMap 来替换 resultType

1
2
3
4
5
<select id="getUser" resultMap="userResultMap">
SELECT user_id, user_name, user_email
FROM t_user
WHERE id = #{id}
</select>


高级结果映射

为了更好的帮助理解,不列出复杂的业务场景。这里引入官方文档的例子进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- 非常复杂的语句 -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
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,
A.favourite_section as author_favourite_section,
P.id as post_id,
P.blog_id as post_blog_id,
P.author_id as post_author_id,
P.created_on as post_created_on,
P.section as post_section,
P.subject as post_subject,
P.draft as draft,
P.body as post_body,
C.id as comment_id,
C.post_id as comment_post_id,
C.name as comment_name,
C.comment as comment_text,
T.id as tag_id,
T.name as tag_name
FROM Blog B
left outer join Author A on B.author_id = A.id
left outer join Post P on B.id = P.blog_id
left outer join Comment C on P.id = C.post_id
left outer join Post_Tag PT on PT.post_id = P.id
left outer join Tag T on PT.tag_id = T.id
WHERE B.id = #{id}
</select>

我们想把它映射到一个智能的对象模型,这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签。

我们先来看看下面这个完整的例子,它是一个非常复杂的结果映射(假设作者,博客,博文,评论和标签都是类型别名)。

不用紧张,我们会一步一步地来说明。虽然它看起来令人望而生畏,但其实非常简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- 非常复杂的结果映射 -->
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<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"/>
<result property="favouriteSection" column="author_favourite_section"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>

resultMap 元素有很多子元素和一个值得深入探讨的结构。 下面是resultMap 元素的概念视图。

结果映射(resultMap)

  • constructor :用于在实例化类时,注入结果到构造方法中
    • idArg :ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • arg :将被注入到构造方法的一个普通结果
  • id :一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
  • result :注入到字段或 JavaBean 属性的普通结果
  • association :一个复杂类型的关联;许多结果将包装成这种类型
    • 嵌套结果映射:关联可以是 resultMap 元素,或是对其它结果映射的引用
  • collection :一个复杂类型的集合
    • 嵌套结果映射:集合可以是 resultMap 元素,或是对其它结果映射的引用
  • discriminator :使用结果值来决定使用哪个 resultMap
    • case :基于某些值的结果映射
      • 嵌套结果映射: case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射
属性 描述
id 当前命名空间中的一个唯一标识,用于标识一个结果映射。
type 类的完全限定名, 或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。
autoMapping 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。
这个属性会覆盖全局的属性 autoMappingBehavior。
默认值:未设置(unset)。

id & result

1
2
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>

这些元素是结果映射的基础。

idresult 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。

这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。

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

关联

1
2
3
4
<association property="author" column="blog_author_id" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>

关联(association)元素处理“有一个”类型的关系。

比如,在我们的示例中,一个博客有一个用户。关联结果映射和其它类型的映射工作方式差不多。 你需要指定目标属性名以及属性的javaType(很多时候 MyBatis 可以自己推断出来),在必要的情况下你还可以设置 JDBC 类型,如果你想覆盖获取结果值的过程,还可以设置类型处理器。

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

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

首先,先让我们来看看这个元素的属性。只列出变化部分

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

关联的嵌套 Select 查询

1
2
3
4
5
6
7
8
9
10
11
<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>

相关属性

属性 描述
column 数据库中的列名,或者是列的别名。
一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。
注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。
这会使得 prop1prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
select 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。
注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。
这会使得 prop1prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。

就是这么简单。我们有两个 select 查询语句:一个用来加载博客(Blog),另外一个用来加载作者(Author),而且博客的结果映射描述了应该使用 selectAuthor 语句加载它的 author 属性。

其它所有的属性将会被自动加载,只要它们的列名和属性名相匹配。

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

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

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

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

所以还有另外一种方法:使用链接语句进行查询,而不是使用多次单表查询

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>

注意查询中的连接,以及为确保结果能够拥有唯一且清晰的名字,我们设置的别名。 这使得进行映射非常简单。现在我们可以映射这个结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>

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

非常重要: id 元素在嵌套结果映射中扮演着非常重要的角色。你应该总是指定一个或多个可以唯一标识结果的属性。

虽然,即使不指定这个属性,MyBatis 仍然可以工作,但是会产生严重的性能问题。 只需要指定可以唯一标识结果的最少属性。显然,你可以选择主键(复合主键也可以)。

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

1
2
3
4
5
6
7
8
9
10
11
<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>

那如果博客(blog)有一个共同作者(co-author)该怎么办?select 语句看起来会是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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 的结果映射定义如下:

1
2
3
4
5
6
7
<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 的结果。

1
2
3
4
5
6
7
8
9
<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>

集合

1
2
3
4
5
<collection property="posts" ofType="domain.blog.Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>

集合元素和关联元素几乎是一样的,它们相似的程度之高,以致于没有必要再介绍集合元素的相似部分。 所以让我们来关注它们的不同之处吧。

我们来继续上面的示例,一个博客(Blog)只有一个作者(Author)。但一个博客有很多文章(Post)。 在博客类中,这可以用下面的写法来表示:

1
private List<Post> posts;

要像上面这样,映射嵌套结果集合到一个 List 中,可以使用集合元素。 和关联元素一样,我们可以使用嵌套 Select 查询,或基于连接的嵌套结果映射集合。

集合的嵌套 Select 查询

首先,让我们看看如何使用嵌套 Select 查询来为博客加载文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<resultMap id="blogResult" type="Blog">
<collection property="posts"
javaType="ArrayList"
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>

你可能会立刻注意到几个不同,但大部分都和我们上面学习过的关联元素非常相似。

首先,你会注意到我们使用的是集合元素。 接下来你会注意到有一个新的 “ofType” 属性。这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。 所以你可以按照下面这样来阅读映射:

1
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>

读作: “posts 是一个存储 Post 的 ArrayList 集合”

在一般情况下,MyBatis 可以推断 javaType 属性,因此并不需要填写。所以很多时候你可以简略成:

1
<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>

集合的嵌套结果映射

现在你可能已经猜到了集合的嵌套结果映射是怎样工作的——除了新增的 “ofType” 属性,它和关联的完全相同。

首先, 让我们看看对应的 SQL 语句:

1
2
3
4
5
6
7
8
9
10
11
12
<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>

我们再次连接了博客表和文章表,并且为每一列都赋予了一个有意义的别名,以便映射保持简单。 要映射博客里面的文章集合,就这么简单:

1
2
3
4
5
6
7
8
9
<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>

再提醒一次,要记得上面 id 元素的重要性,如果你不记得了,请阅读关联部分的相关部分。

如果你喜欢更详略的、可重用的结果映射,你可以使用下面的等价形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>


相关阅读

MyBatis 快速入门

深入了解 MyBatis :详解配置

深入了解 MyBatis :动态 SQL

深入了解 MyBatis :缓存

为什么说高并发越来越重要了 深入了解 MyBatis :详解配置

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×