笔记:MyBatis Mapper XML文件详解 - Result Maps
- Result Maps(结果集)
resultMap 元素是 MyBatis 中最重要最强大的元素。它就是让你远离 90%的需要从结果 集中取出数据的 JDBC 代码的那个东西, 而且在一些情形下允许你做一些 JDBC 不支持的事 情。 事实上, 编写相似于对复杂语句联合映射这些等同的代码, 也许可以跨过上千行的代码。 ResultMap 的设计就是简单语句不需要明确的结果映射,而很多复杂语句确实需要描述它们 的关系,你已经看到简单映射语句的示例了,但没有明确的 resultMap。比如:
<select id="selectUsers" resultType="map">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
这样一个语句简单作用于所有列被自动映射到 HashMap 的键上,这由 resultType 属性 指定。这在很多情况下是有用的,但是 HashMap 不能很好描述一个领域模型。那样你的应 用程序将会使用 JavaBeans 或 POJOs(Plain Old Java Objects,普通 Java 对象)来作为领域 模型。MyBatis 对两者都支持。看看下面这个 JavaBean:
package com.someapp.model;
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 可以被映射到结果集,就像映射到 HashMap 一样简单:
<select id="selectUsers" resultType="com.someapp.model.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 是什么样子的,这也是解决列名不匹配的另外一种方式。
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
引用它的语句使用 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>
下面是 resultMap 元素的概念视图
- constructor - 类在实例化时,用来注入结果到构造方法中
- idArg - ID 参数;标记结果作为 ID 可以帮助提高整体效能
- arg - 注入到构造方法的一个普通结果
- id – 一个 ID 结果;标记结果作为 ID 可以帮助提高整体效能
- result – 注入到字段或 JavaBean 属性的普通结果
- association – 一个复杂的类型关联;许多结果将包成这种类型
- 嵌入结果映射 – 结果映射自身的关联,或者参考一个
- collection – 复杂类型的集
- 嵌入结果映射 – 结果映射自身的集,或者参考一个
- discriminator – 使用结果值来决定使用哪个结果映射
- case – 基于某些值的结果映射
- 嵌入结果映射 – 这种情形结果也映射它本身,因此可以包含很多相 同的元素,或者它可以参照一个外部的结果映射。
- resultMap元素属性和描述:
属性 | 描述 |
id | 当前命名空间中的一个唯一标识,用于标识一个result map. |
type | 类的全限定名, 或者一个类型别名 (内置的别名可以参考上面的表格). |
autoMapping | 如果设置这个属性,MyBatis将会为这个ResultMap开启或者关闭自动映射。这个属性会覆盖全局的属性autoMappingBehavior。默认值为:unset。 |
- id & result元素属性和描述:
这些是结果映射最基本内容。id 和 result 都映射一个单独列的值到简单数据类型(字符 串,整型,双精度浮点数,日期等)的单独属性或字段。 这两者之间的唯一不同是 id 表示的结果将是当比较对象实例时用到的标识属性。这帮助来改进整体表现,特别是缓存和嵌入结果映射(也就是联合映射) ,属性描述:
属性 | 描述 |
property | 映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同 的 JavaBeans 的属性,那么就会使用。否则 MyBatis 将会寻找给定名称 property 的字段。这两种情形你可以使用通常点式的复杂属性导航。比如,你 可以这样映射一些东西: "username" ,或者映射到一些复杂的东西: "address.street.number" 。 |
column | 从数据库中得到的列名,或者是列名的重命名标签。这也是通常和会 传递给 resultSet.getString(columnName)方法参数中相同的字符串。 |
javaType | 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名 的列表) 。如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。 然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的行为。 |
jdbcType | 在这个表格之后的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅 仅需要对插入,更新和删除操作可能为空的列进行处理。这是 JDBC jdbcType 的需要,而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定 这个类型-但仅仅对可能为空的值。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。这个属性值是类的完全限定名或者是一个类型处理 器的实现,或者是类型别名。 |
为了未来的参考,MyBatis 通过包含的 jdbcType 枚举型,支持下面的 JDBC 类型:
BIT | FLOAT | CHAR | TIMESTAMP | OTHER | UNDEFINED |
TINYINT | REAL | VARCHAR | BINARY | BLOB | NVARCHAR |
SMALLINT | DOUBLE | LONGVARCHAR | VARBINARY | CLOB | NCHAR |
INTEGER | NUMERIC | DATE | LONGVARBINARY | BOOLEAN | NCLOB |
BIGINT | DECIMAL | TIME | NULL | CURSOR | ARRAY |
- constructor 元素属性和描述:
对于大多数数据传输对象(Data Transfer Object,DTO)类型,属性可以起作用,而且像 你绝大多数的领域模型, 指令也许是你想使用一成不变的类的地方。 通常包含引用或查询数 据的表很少或基本不变的话对一成不变的类来说是合适的。 构造方法注入允许你在初始化时 为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBeans 属 性来达到这个目的,但是一些人更青睐构造方法注入。构造方法元素支持这个。
属性 | 描述 |
column | 来自数据库的类名,或重命名的列标签。这和通常传递给 resultSet.getString(columnName)方法的字符串是相同的。 |
javaType | 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列表)。 如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。然而,如 果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的 行为。 |
jdbcType | 在这个表格之前的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅仅 需要对插入, 更新和删除操作可能为空的列进行处理。这是 JDBC 的需要, jdbcType 而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定这个类型-但 仅仅对可能为空的值。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的 类型处理器。 这个属性值是类的完全限定名或者是一个类型处理器的实现, 或者是类型别名。 |
select | 用于加载复杂类型属性的映射语句的ID,从column中检索出来的数据,将作为此select语句的参数。具体请参考Association标签。 |
resultMap | ResultMap的ID,可以将嵌套的结果集映射到一个合适的对象树中,功能和select属性相似,它可以实现将多表连接操作的结果映射成一个单一的ResultSet。这样的ResultSet将会将包含重复或部分数据重复的结果集正确的映射到嵌套的对象树中。为了实现它, MyBatis允许你 "串联" ResultMap,以便解决嵌套结果集的问题。想了解更多内容,请参考下面的Association元素。 |
注意:如果出现 java.lang.NoSuchMethodException 异常,无法找到构造函数,首先检查构造函数的参数类型和顺序,如果还有问题可以将 long、int 等替换为 Long、Integer 类
- association (关联)
关联元素处理"有一个"类型的关系。比如,在我们的示例中,一个博客有一个用户,关联映射就工作于这种结果之上。你指定了目标属性,来获取值的列,属性的 java 类型(很 多情况下 MyBatis 可以自己算出来),如果需要的话还有 jdbc 类型,如果你想覆盖或获取的结果值还需要类型控制器。
关联中不同的是你需要告诉 MyBatis 如何加载关联。MyBatis 在这方面会有两种不同的 方式:
- 嵌套查询:通过执行另外一个 SQL 映射语句来返回预期的复杂类型。
- 嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集。首先,然让我们来查看这个元素的属性。所有的你都会看到,它和普通的只由 select 和 resultMap 属性的结果映射不同。
公共属性如下:
属性 | 描述 |
property | 映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同的 property JavaBeans 的属性, 那么就会使用。 否则 MyBatis 将会寻找给定名称的字段。 这两种情形你可以使用通常点式的复杂属性导航。比如,你可以这样映射 一 些 东 西 :" username ", 或 者 映 射 到 一 些 复 杂 的 东 西 : "address.street.number" 。 |
javaType | 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列 表) 。如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。然而,如 javaType 果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的 行为。 |
jdbcType | 在这个表格之前的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅仅 需要对插入, 更新和删除操作可能为空的列进行处理。这是 JDBC 的需要, jdbcType 而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定这个类型-但 仅仅对可能为空的值。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的 typeHandler 类型处理器。 这个属性值是类的完全限定名或者是一个类型处理器的实现, 或者是类型别名。 |
- 嵌套查询
属性 | 描述 |
column | 来自数据库的类名或重命名的列标签。这和通常传递给 resultSet.getString(columnName)方法的字符串是相同的。 column 注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= " {prop1=col1,prop2=col2} " 这种语法来传递给嵌套查询语 句。这会引起 prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。 |
select | 另外一个映射语句的 ID,可以加载这个属性映射需要的复杂类型。获取的 在列属性中指定的列的值将被传递给目标 select 语句作为参数。表格后面 有一个详细的示例。 select 注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= " {prop1=col1,prop2=col2} " 这种语法来传递给嵌套查询语 句。这会引起 prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。 |
fetchType | 可选的。有效值为 lazy和eager。 如果使用了,它将取代全局配置参数lazyLoadingEnabled。 |
这种方式很简单, 但是对于大型数据集合和列表将不会表现很好。 问题就是我们熟知的 "N+1 查询问题"。概括地讲,N+1 查询问题可以是这样引起的:
- 你执行了一个单独的 SQL 语句来获取结果列表(就是"+1")。
- 对返回的每条记录,你执行了一个查询语句来为每个加载细节(就是"N")。
这个问题会导致成百上千的 SQL 语句被执行。这通常不是期望的,MyBatis 能延迟加载这样的查询就是一个好处,因此你可以分散这些语句同时运行的消 耗。然而,如果你加载一个列表,之后迅速迭代来访问嵌套的数据,你会调用所有的延迟加 载,这样的行为可能是很糟糕的,示例代码如下:
<!-- 查询实体的方法Map,其结果映射为 userInfoResult -->
<select id="getModel" parameterType="long" resultMap="userInfoResult">
SELECT
User_ID as "userId",
Name as "name",
Realname as "realname",
Email as "email",
Phone as "phone",
Create_Time as "createTime"
FROM User_Info
WHERE User_ID = #{userId}
</select>
<!-- userInfoResult 结果映射,在 Model 中增加了属性 userAccount 类型为 UserAccountModel -->
<resultMap id="userInfoResult" type="userInfoModel">
<!--
userInfoModel 有属性 userAccount 其关系列 column 设置为 UserAccountModel 类的 userId 属性通过该
属性进行一对一关系,userAccount类的数据来源为 select 配置的查询结果,fetchType 设置了延迟加载
-->
<association property="userAccount" column="userId" javaType="userAccountModel" select="selectUserAccount" fetchType="lazy"/>
</resultMap>
<!-- 获取 UserAccountModel 类的查询实体 SQL -->
<select id="selectUserAccount" resultType="userAccountModel" >
SELECT
User_ID as "userId",
Login_Account as "loginAccount",
Login_Password as "loginPassword",
Last_Login_Time as "lastLoginTime",
Create_Time as "createTime"
FROM User_Account
WHERE User_ID = #{userId}
</select>
- 嵌套结果
属性 | 描述 |
resultMap | 这是结果映射的 ID,可以映射关联的嵌套结果到一个合适的对象图中。这 是一种替代方法来调用另外一个查询语句。这允许你联合多个表来合成到 resultMap 一个单独的结果集。这样的结果集可能包含重复,数据的重复组需要被分解,合理映射到一个嵌套的对象图。为了使它变得容易,MyBatis 让你"链接"结果映射,来处理嵌套结果。 |
columnPrefix | 当连接多表时,你将不得不使用列别名来避免ResultSet中的重复列名。指定columnPrefix允许你映射列名到一个外部的结果集中。 请看后面的例子。 |
notNullColumn | 默认情况下,子对象仅在至少一个列映射到其属性非空时才创建。 通过对这个属性指定非空的列将改变默认行为,这样做之后Mybatis将仅在这些列非空时才创建一个子对象。 可以指定多个列名,使用逗号分隔。默认值:未设置(unset)。 |
autoMapping | 如果使用了,当映射结果到当前属性时,Mybatis将启用或者禁用自动映射。 该属性覆盖全局的自动映射行为。 注意它对外部结果集无影响,所以在select or resultMap属性中这个是毫无意义的。 默认值:未设置(unset)。 |
配置示例:
<select id="getFullModel" parameterType="long" resultMap="fullResult">
SELECT
ui.User_ID as "userId",
ui.Name as "name",
ui.Realname as "realname",
ui.Email as "email",
ui.Phone as "phone",
ui.Create_Time as "createTime",
ua.User_ID as "ua_userId",
ua.Login_Account as "ua_loginAccount",
ua.Login_Password as "ua_loginPassword",
ua.Create_Time as "ua_createTime"
FROM User_Info ui,User_Account ua
WHERE ui.User_ID = ua.User_ID AND ui.User_ID = #{userId}
</select>
<!-- 多表关联查询结果必须显示声明属性和列映射关系,autoMappingBehavior 配置默认值为 PARTIAL 不会自动映射嵌套结果集 -->
<resultMap id="fullResult" type="userInfoModel">
<id property="userId" column="userId"/>
<result property="name" column="name"/>
<result property="realname" column="realname"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<result property="createTime" column="createTime"/>
<association property="userAccount" column="userId" javaType="userAccountModel" columnPrefix="ua_" >
<id property="userId" column="userId"/>
<result property="loginAccount" column="loginAccount"/>
<result property="loginPassword" column="loginPassword"/>
<result property="createTime" column="createTime"/>
</association>
</resultMap>
- collection (集合)
前面所的关联都是一对一的关系,集合表示的是一对多的关系,其属性也和集合是一致的,集合也和关联一样存在嵌套查询和嵌套结果二种类型。需要在实体类中定义集合属性,类定义如下:
package org.mybatisExamples.simple;
import java.util.Date;
import java.util.List;
public class UserInfoModel {
private long userId;
private String name;
private String realname;
private String email;
private String phone;
private Date createTime;
// 定义集合类型属性
private List<UserCertifyRecordModel> certifys;
/* 省略getter和setter方法*/
}
- 嵌套查询
- Mapper文件增加集合关联
<!-- 集合嵌套查询 Begin -->
<select id="getModelAndCertify" parameterType="Long" resultMap="certifyResult">
SELECT
User_ID as userId,
Name as name,
Realname as realname,
Email as email,
Phone as phone,
Create_Time as createTime
FROM User_Info
WHERE User_ID = #{userId}
</select>
<!-- 结果集增加集合关联,ofType 设置集合元素类型 select 设置嵌入查询 -->
<resultMap id="certifyResult" type="userInfoModel">
<collection property="certifys" column="userId" ofType="userCertifyRecordModel" select="getCertifyByUserId"/>
</resultMap>
<select id="getCertifyByUserId" parameterType="Long" resultType="userCertifyRecordModel">
<![CDATA[
SELECT
Record_ID as recordId,
User_ID as userId,
Certify_Type as certifyType,
Verify_Code as vertifyCode,
Verify_Expire_Time as verifyExpireTime,
Status as status,
Deal_Time as dealTime,
Creat_Time as createTime
FROM User_Certify_Record
WHERE User_ID = #{userId}]]>
</select>
- 嵌套结果
<select id="getModelAndCertifyResultMap" parameterType="long" resultMap="userInfoCertifyCollection">
SELECT
ui.User_ID as userId,
ui.Name as name,
ui.Realname as realname,
ui.Email as email,
ui.Phone as phone,
ui.Create_Time as createTime,
ucr.Record_ID as ucr_recordId,
ucr.User_ID as ucr_userId,
ucr.Certify_Type as ucr_certifyType,
ucr.Verify_Code as ucr_verifyCode,
ucr.Verify_Expire_Time as ucr_verifyExpireTime,
ucr.Status as ucr_status,
ucr.Deal_Time as ucr_dealTime,
ucr.Creat_Time as ucr_createTime
FROM User_Info ui,User_Certify_Record ucr
WHERE ui.User_ID = ucr.User_ID and ui.User_ID = #{userId}
</select>
<!--
必须显示声明属性和列映射关系,autoMappingBehavior 配置默认值为 PARTIAL 不会自动映射嵌套结果集
-->
<resultMap id="userInfoCertifyCollection" type="userInfoModel">
<id property="userId" column="userId"/>
<result property="name" column="name"/>
<result property="realname" column="realname"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<collection property="certifys" column="userId" ofType="userCertifyRecordModel" columnPrefix="ucr_">
<id property="recordId" column="recordId"/>
<result property="userId" column="userId"/>
<result property="certifyType" column="certifyType"/>
<result property="verifyCode" column="verifyCode"/>
<result property="verifyExpireTime" column="verifyExpireTime"/>
<result property="status" column="status"/>
<result property="dealTime" column="dealTime"/>
<result property="createTime" column="createTime"/>
</collection>
</resultMap>
- discriminator (鉴别器)
有时一个单独的数据库查询也许返回很多不同 (但是希望有些关联) 数据类型的结果集。 鉴别器元素就是被设计来处理这个情况的, 还有包括类的继承层次结构。 鉴别器非常容易理 解,因为它的表现很像 Java 语言中的 switch 语句。定义鉴别器指定了 column 和 javaType 属性。 列是 MyBatis 查找比较值的地方。 JavaType 是需要被用来保证等价测试的合适类型,示例如下:
- 创建表
CREATE TABLE Car (
Record_ID BIGINT NOT NULL,
Car_Type VARCHAR(1) DEFAULT NULL,
Door_Size INT(11) DEFAULT NULL,
Box_Size INT(11) DEFAULT NULL,
Color VARCHAR(20) DEFAULT NULL,
PRIMARY KEY (Record_ID)
);
insert into Car values (1,'C',2,null,'红色');
insert into Car values (2,'C',4,null,'黑色');
insert into Car values (3,'T',null,1,'蓝色');
insert into Car values (4,'T',null,2,'蓝色');
commit;
- 实体类
- 基类
package org.mybatisExamples.simple;
public class VehicleModel {
private int recordId;
private String carType;
private String color;
/* 省略setter和getter方法 */
}
- 子类
package org.mybatisExamples.simple;
public class CarModel extends VehicleModel {
private int doorSize;
public int getDoorSize() {
return doorSize;
}
public void setDoorSize(int doorSize) {
this.doorSize = doorSize;
}
}
package org.mybatisExamples.simple;
public class TruckModel extends VehicleModel {
private int boxSize;
public int getBoxSize() {
return boxSize;
}
public void setBoxSize(int boxSize) {
this.boxSize = boxSize;
}
}
- Mapper 类
package org.mybatisExamples.simple;
import java.util.List;
public interface VehicleMapper {
List<VehicleModel> findAll();
}
- Mapper 配置
<resultMap id="vehicleMap" type="vehicleModel">
<id property="id" column="id" />
<result property="color" column="color" />
<result property="carType" column="Car_Type"/>
<!-- 鉴别器使用 Type 列,其类型为 String -->
<discriminator javaType="java.lang.String" column="type">
<!-- Type 列值为 T 时,返回对象结果类型为配置的 truckModel -->
<case value="T" resultType="truckModel">
<result property="boxSize" column="boxsize" />
</case>
<!-- Type 列值为 C 时,返回对象结果类型为配置的 carModel -->
<case value="C" resultType="carModel">
<result property="doorSize" column="doorsize" />
</case>
</discriminator>
</resultMap>
<select id="findAll" resultMap="vehicleMap">
select * from Car
</select>
- 继承 ResultMap
我们可以从从另外一个<resultMap>,继承出一个新的<resultMap>,这样,原先的属性映射可以继承过来,以实现新的ResultMap,示例代码如下:
<resultMap type="Student" id="StudentResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="phone" column="phone" />
</resultMap>
<resultMap type="Student" id="StudentWithAddressResult" extends="StudentResult">
<result property="address.addrId" column="addr_id" />
<result property="address.street" column="street" />
<result property="address.city" column="city" />
<result property="address.state" column="state" />
<result property="address.zip" column="zip" />
<result property="address.country" column="country" />
</resultMap>
我们可以使用圆点记法为内嵌的对象的属性赋值,如果你只想映射Student数据,你可以使用id为StudentResult的resultMap,如下所示:
<select id="findStudentById" parameterType="int" resultMap="StudentResult">
SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}
</select>
如果你想将映射Student数据和Address数据,你可以使用id为StudentWithAddressResult的resultMap:
<select id="selectStudentWithAddress" parameterType="int" resultMap="StudentWithAddressResult">
SELECT STUD_ID, NAME, EMAIL, PHONE, A.ADDR_ID, STREET, CITY,
STATE, ZIP, COUNTRY
FROM STUDENTS S LEFT OUTER JOIN ADDRESSES A ON
S.ADDR_ID=A.ADDR_ID
WHERE STUD_ID=#{studId}
</select>