在Mybatis映射器之select解析这篇文章最后我们只是简单的介绍了一下ResultMap的使用,而ResultMap功能非常多,他也包含了很多子元素,本篇我们将详细的介绍下。
resultMap元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets数据提取代码中解放出来,
并在一些情形下允许你做一些 JDBC 不支持的事情。
实际上,在对复杂语句进行联合映射的时候,它很可能可以代替数千行的同等功能的代码。
ResultMap 的设计思想是,简单的语句不需要明确的结果映射,而复杂一点的语句只需要描述它们的关系就行了。
你已经见过简单映射语句的示例了,但没有明确的 resultMap。比如:
<select id="selectUsers" resultType="map">
select id, username, hashedPassword
from some_table
where id = #{id}
select>
上述语句只是简单地将所有的列映射到 HashMap 的键上,这由 resultType 属性指定。虽然在大部分情况下都够用,但是 HashMap 不是一个很好的领域模型。你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通 Java 对象)作为领域模型。
MyBatis 对两者都支持。看看下面这个 JavaBean:
public class User {
private int id;
private String username;
private String hashedPassword;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getHashedPassword() {
return hashedPassword;
}
public void setHashedPassword(String hashedPassword) {
this.hashedPassword = hashedPassword;
}
}
基于 JavaBean 的规范,上面这个类有 3 个属性:id,username 和 hashedPassword。这些属性会对应到 select 语句中的列名。
这样的一个 JavaBean 可以被映射到 ResultSet,就像映射到 HashMap 一样简单。
<select id="selectUsers" resultType="com.yhl.mybatis.model.User">
select id, username, hashedPassword
from some_table
where id = #{id}
select>
类型别名是你的好帮手。使用它们,你就可以不用输入类的完全限定名称了。比如:
<typeAlias type="com.yhl.mybatis.model.User" alias="User"/>
<select id="selectUsers" resultType="User">
select id, username, hashedPassword
from some_table
where id = #{id}
select>
这些情况下,MyBatis 会在幕后自动创建一个 ResultMap,再基于属性名来映射列到
JavaBean 的属性上。如果列名和属性名没有精确匹配,可以在 SELECT 语句中对列使用别名(这是一个基本的 SQL 特性)来匹配标签。比如:
<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>
ResultMap最优秀的地方在于,虽然你已经对它相当了解了,但是根本就不需要显式地用到他们。上面这些简单的示例根本不需要下面这些繁琐的配置。
出于示范的原因,让我们来看看最后一个示例中,如果使用外部的 resultMap 会怎样,这也是解决列名不匹配的另外一种方式。
id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
引用它的语句使用 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
select>
如果世界总是这么简单就好了。
MyBatis 创建的一个想法是:数据库不可能永远是你所想或所需的那个样子。
我们希望每个数据库都具备良好的第三范式或 BCNF 范式,可惜它们不总都是这样。
如果有一个独立且完美的数据库映射模式,所有应用程序都可以使用它,那就太好了,但可惜也没有。ResultMap 就是 MyBatis 对这个问题的答案。
比如,我们如何映射下面这个语句?
-- Very Complex Statement -->
你可能想把它映射到一个智能的对象模型,这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签。我们来看看下面这个完整的例子,它是一个非常复杂的 ResultMap(假设作者,博客,博文,评论和标签都是类型的别名)。不用紧张,我们会一步一步来说明。虽然它看起来令人望而生畏,但其实非常简单。
<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
(1)constructor - 用于在实例化类时,注入结果到构造方法中
- idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
- arg - 将被注入到构造方法的一个普通结果
(2)id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
(3)result – 注入到字段或 JavaBean 属性的普通结果
(4)association – 一个复杂类型的关联;许多结果将包装成这种类型
- 嵌套结果映射 – 关联可以指定为一个 resultMap 元素,或者引用一个
(5)collection – 一个复杂类型的集合
- 嵌套结果映射 – 集合可以指定为一个 resultMap 元素,或者引用一个
(6)discriminator – 使用结果值来决定使用哪个 resultMap
- case – 基于某些值的结果映射
- 嵌套结果映射 – 一个 case 也是一个映射它本身的结果,因此可以包含很多相 同的元素,或者它可以参照一个外部的 resultMap。
最佳实践:
最好一步步地建立结果映射。单元测试可以在这个过程中起到很大帮助。如果你尝试一次创建一个像上面示例那样的巨大的结果映射, 那么很可能会出现错误而且很难去使用它来完成工作。 从最简单的形态开始,逐步进化。而且别忘了单元测试!使用框架的缺点是有时候它们看上去像黑盒子(无论源代码是否可见)。 为了确保你实现的行为和想要的一致,最好的选择是编写单元测试。提交 bug 的时候它也能起到很大的作用。
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
这些是结果映射最基本的内容。id 和 result 都将一个列的值映射到一个简单数据类型(字符串,整型,双精度浮点数,日期等)的属性或字段。
这两者之间的唯一不同是, id 表示的结果将是对象的标识属性,这会在比较对象实例时用到。
这样可以提高整体的性能,尤其是缓存和嵌套结果映射(也就是联合映射)的时候。
两个元素都有一些属性,如下图:
为了未来的参考,MyBatis 通过包含的 jdbcType 枚举型,支持下面的 JDBC 类型。
通过修改对象属性的方式,可以满足大多数的数据传输对象(Data Transfe
r Object,DTO)以及绝大部分领域模型的要求。但有些情况下你想使用不可变类。
通常来说,很少或基本不变的、包含引用或查询数据的表,很适合使用不可变类。
构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBeans 属性来达到这个目的,但有一些人更青睐于构造方法注入。constructor 元素就是为此而生的。
看看下面这个构造方法:
public class User {
//...
public User(Integer id, String username, int age) {
//...
}
//...
}
为了将结果注入构造方法,MyBatis需要通过某种方式定位相应的构造方法。
在下面的例子中,MyBatis搜索一个声明了三个形参的的构造方法,以 java.lang.Integer, java.lang.String and int 的顺序排列。
<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
<arg column="age" javaType="_int"/>
constructor>
当你在处理一个带有多个形参的构造方法时,很容易在保证 arg 元素的正确顺序上出错。 从版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。 为了通过名称来引用构造方法参数,你可以添加 @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>
如果类中存在名称和类型相同的属性,那么可以省略 javaType 。
剩余的属性和规则和普通的 id 和 result 元素是一样的。
property="author" column="blog_author_id" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
关联元素处理“有一个”类型的关系。比如,在我们的示例中,一个博客有一个用户。
关联映射就工作于这种结果之上。你指定了目标属性,来获取值的列,属性的 java 类型(很多情况下 MyBatis 可以自己算出来),如果需要的话还有 jdbc 类型,如果你想覆盖或获取的结果值还需要类型控制器。
关联中不同的是你需要告诉 MyBatis 如何加载关联。MyBatis 在这方面会有两种不同的
方式:
(1)嵌套查询:通过执行另外一个 SQL 映射语句来返回预期的复杂类型。
(2)嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集。首先,然让我们来查看这个元素的属性。所有的你都会看到,它和普通的只由 select 和 resultMap 属性的结果映射不同。
"blogResult" type="Blog">
property="author" column="author_id" javaType="Author" select="selectAuthor"/>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
select>
<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
select>
我们有两个查询语句:一个来加载博客,另外一个来加载作者,而且博客的结果映射描
述了“selectAuthor”语句应该被用来加载它的 author 属性。其他所有的属性将会被自动加载,假设它们的列和属性名相匹配。这种方式很简单,但是对于大型数据集合和列表将不会表现很好。问题就是我们熟知的“N+1 查询问题”。概括地讲,N+1 查询问题可以是这样引起的:
(1)你执行了一个单独的 SQL 语句来获取结果列表(就是“+1”)。 (2)对返回的每条记录,你执行了一个查询语句来为每个加载细节(就是“N”)。
这个问题会导致成百上千的 SQL 语句被执行。这通常不是期望的。
MyBatis 能延迟加载这样的查询就是一个好处,因此你可以分散这些语句同时运行的消
耗。然而,如果你加载一个列表,之后迅速迭代来访问嵌套的数据,你会调用所有的延迟加
载,这样的行为可能是很糟糕的。
所以还有另外一种方法。
在上面你已经看到了一个非常复杂的嵌套关联的示例。
下面这个是一个非常简单的示例
来说明它如何工作。代替了执行一个分离的语句,我们联合博客表和作者表在一起,就像:
注意这个联合查询, 以及采取保护来确保所有结果被唯一而且清晰的名字来重命名。
这使得映射非常简单。现在我们可以映射这个结果:
id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
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"/>
在上面的示例中你可以看到博客的作者关联代表着“authorResult”结果映射来加载作
者实例。
非常重要: id元素在嵌套结果映射中扮演着非常重要的角色。你应该总是指定一个或多个可以唯一标识结果的属性。实际上如果你不指定它的话,MyBatis仍然可以工作,但是会有严重的性能问题。在可以唯一标识结果的情况下,尽可能少的选择属性。主键是一个显而易见的选择(即使是复合主键)。
现在,上面的示例用了外部的结果映射元素来映射关联。这使得 Author 结果映射可以
重用。然而,如果你不需要重用它的话,或者你仅仅引用你所有的结果映射合到一个单独描述的结果映射中。你可以嵌套结果映射。这里给出使用这种方式的相同示例:
id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
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"/>
如果blog有一个co-author怎么办? 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中的列名不同。你需要指定
id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
property="author"
resultMap="authorResult" />
property="coAuthor"
resultMap="authorResult"
columnPrefix="co_" />
上面你已经看到了如何处理“有一个”类型关联。但是“有很多个”是怎样的?下面这
个部分就是来讨论这个主题的。
property="posts" ofType="domain.blog.Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
集合元素的作用几乎和关联是相同的。实际上,它们也很相似,文档的异同是多余的。
所以我们更多关注于它们的不同。
我们来继续上面的示例,一个博客只有一个作者。但是博客有很多文章。在博客类中,
这可以由下面这样的写法来表示:
private List posts;
要映射嵌套结果集合到 List 中,我们使用集合元素。就像关联元素一样,我们可以从
连接中使用嵌套查询,或者嵌套结果。
首先,让我们看看使用嵌套查询来为博客加载文章。
"blogResult" type="Blog">
property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
<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(或字段)属性类型和集合包含的类型来说是很重要的。所以你可以读出下面这个映射:
property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
读作: “在 Post 类型的 ArrayList 中的 posts 的集合。”
javaType 属性是不需要的,因为 MyBatis 在很多情况下会为你算出来。所以你可以缩短
写法:
property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>
至此,你可以猜测集合的嵌套结果是如何来工作的,因为它和关联完全相同,除了它应
用了一个“ofType”属性,首先, 让我们看看 SQL:
我们又一次联合了博客表和文章表,而且关注于保证特性,结果列标签的简单映射。现
在用文章映射集合映射博客,可以简单写为:
id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
同样,要记得 id 元素的重要性,如果你不记得了,请阅读上面的关联部分。
同样,如果你引用更长的形式允许你的结果映射的更多重用,
你可以使用下面这个替代的映射:
id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
id="blogPostResult" type="Post">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
注意: 这个对你所映射的内容没有深度,广度或关联和集合相联合的限制。当映射它们
时你应该在大脑中保留它们的表现。你的应用在找到最佳方法前要一直进行的单元测试和性能测试。好在 myBatis 让你后来可以改变想法,而不对你的代码造成很小(或任何)影响。
高级关联和集合映射是一个深度的主题。文档只能给你介绍到这了。加上一点联系,你
会很快清楚它们的用法。
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
discriminator>
有时一个单独的数据库查询也许返回很多不同(但是希望有些关联)数据类型的结果集。
鉴别器元素就是被设计来处理这个情况的,还有包括类的继承层次结构。鉴别器非常容易理解,因为它的表现很像 Java 语言中的 switch 语句。
定义鉴别器指定了 column 和 javaType 属性。
(1)column 是 MyBatis 查找比较值的地方。
(2)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>
在这个示例中,MyBatis 会从结果集中得到每条记录,然后比较它的 vehicle 类型的值。
如果它匹配任何一个鉴别器的实例,那么就使用这个实例指定的结果映射。换句话说,这样做完全是剩余的结果映射被忽略(除非它被扩展,这在第二个示例中讨论)。如果没有任何一个实例相匹配,那么 MyBatis 仅仅使用鉴别器块外定义的结果映射。所以,如果 carResult按如下声明:
<resultMap id="carResult" type="Car">
<result property="doorCount" column="door_count" />
resultMap>
那么只有 doorCount 属性会被加载。这步完成后完整地允许鉴别器实例的独立组,尽管
和父结果映射可能没有什么关系。这种情况下,我们当然知道 cars 和 vehicles 之间有关系,如 Car 是一个 Vehicle 实例。因此,我们想要剩余的属性也被加载。我们设置的结果映射的简单改变如下。
<resultMap id="carResult" type="Car" extends="vehicleResult">
<result property="doorCount" column="door_count" />
resultMap>
现在 vehicleResult 和 carResult 的属性都会被加载了。
尽管曾经有些人会发现这个外部映射定义会多少有一些令人厌烦之处。
因此还有另外一种语法来做简洁的映射风格。比如:
<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>
要记得这些都是结果映射,如果你不指定任何结果,那么 MyBatis 将会为你自动匹配列
和属性。所以这些例子中的大部分是很冗长的,而其实是不需要的。也就是说,很多数据库是很复杂的,我们不太可能对所有示例都能依靠它。