在 Java 持久层框架 MyBatis 中,resultMap
是一个极其强大的工具,它允许开发者精确地控制 SQL 查询结果到 Java 对象的映射过程。尤其是在处理复杂的关联查询(如一对一、一对多)时,resultMap
的作用更是不可替代。本文将带你深入理解 resultMap
的核心概念、主要元素,并通过详细示例演示如何使用它来构建高效的关联查询。
一、为什么你需要 resultMap
?
MyBatis 的自动映射功能在简单的查询中表现出色,但当遇到以下场景时,resultMap
就变得必不可少:
-
数据库列名与 Java 属性名不一致: 数据库中通常使用
snake_case
(如user_name
),而 Java 中常用camelCase
(如userName
)。resultMap
可以显式地建立这种映射关系。 -
复杂的数据类型转换: 例如,将数据库的
TINYINT
转换为 Java 的Boolean
,或自定义类型处理器。 -
构建复杂的对象图: 这是
resultMap
最重要的应用场景。当你的一个 Java 对象包含其他对象(一对一)或对象集合(一对多)时,resultMap
能够帮助你将扁平化的 SQL 结果集映射为富有层次感的 Java 对象结构。 -
控制加载策略: 可以配置关联对象是即时加载(Eager Loading)还是懒加载(Lazy Loading)。
二、resultMap
的核心元素
一个 resultMap
通常由以下主要子元素构成:
-
<id>
:主键映射-
用于映射数据库表的主键列。
-
column
:数据库中的列名。 -
property
:Java 对象中的属性名。 -
jdbcType
:JDBC 类型(可选)。 -
javaType
:Java 类型(可选,MyBatis 大多可推断)。 -
重要性: MyBatis 依据
<id>
标签识别结果集中行的唯一性,尤其在处理一对多关系时,它能帮助 MyBatis 正确聚合数据。
-
-
<result>
:普通列映射-
用于映射数据库表的非主键列。
-
属性与
<id>
类似。
-
-
<association>
:一对一(One-to-One)关联映射-
用于映射 Java 对象中嵌套的单个对象属性。
-
property
:Java 对象中表示关联对象的属性名(例如User
对象中的Address address
,property
就是address
)。 -
javaType
:关联对象的 Java 类型(例如com.example.Address
)。 -
嵌套结果(即时加载)方式: 内部包含
<id>
和<result>
,直接映射JOIN
查询的结果集中的关联列。 -
嵌套查询(懒加载)方式:
column
(传递给子查询的参数)和select
(子查询的语句 ID)。
-
-
<collection>
:一对多(One-to-Many)关联映射-
用于映射 Java 对象中嵌套的对象集合属性。
-
property
:Java 对象中表示集合的属性名(例如User
对象中的List<Order> orders
,property
就是orders
)。 -
ofType
:这是关键! 指定集合中元素的 Java 类型(例如com.example.Order
)。 -
其他属性与
<association>
类似,也分为嵌套结果和嵌套查询两种方式。
-
三、关联查询策略详解
MyBatis 主要支持两种关联查询策略:
3.1 嵌套结果(Nested Results / Eager Loading / 即时加载)
-
原理: 只执行一次 SQL 查询,该查询包含
JOIN
语句,一次性从数据库中获取所有相关数据。MyBatis 随后会根据resultMap
的定义,将结果集中的扁平数据映射到主对象及其嵌套的关联对象和集合中。 -
优点:
-
高效: 减少数据库的往返次数(只需一次),降低网络延迟。
-
简单: 适用于大多数关联查询场景。
-
-
缺点:
-
对于一对多关系,主表的数据可能会在结果集中重复出现(MyBatis 会自动去重并聚合到集合中)。
-
如果关联数据量非常大,一次性加载所有数据可能会消耗更多内存。
-
-
适用场景: 关联数据通常会被一起查询,或者数据量相对较小。
3.2 嵌套查询(Nested Selects / Lazy Loading / 懒加载)
-
原理: 分多次执行 SQL 查询。首先执行一个主查询来加载主对象,然后,当代码实际访问主对象中的关联属性时(或者根据配置),MyBatis 才会执行额外的 SQL 查询来加载关联对象。
-
优点:
-
按需加载: 避免加载不必要的数据,在某些情况下可以减少内存消耗。
-
简化 SQL: 每个查询可以保持相对简单。
-
-
缺点:
-
N+1 查询问题: 如果你查询 N 个主对象,并且每个主对象都需要加载关联对象,那么可能会导致 N+1 次数据库查询(1次主查询 + N次关联查询),性能可能下降。
-
增加数据库往返次数。
-
-
适用场景: 关联数据量可能非常大,或者关联数据不经常被访问。
一般情况下,推荐使用 嵌套结果(即时加载)
来避免 N+1 查询问题。
四、实践案例:用户、创建人、修改人与角色列表
我们以一个典型的 TUser
类为例,它包含:
-
一对一关联:
createByDO
(创建人,也是TUser
类型),editByDO
(修改人,也是TUser
类型)。 -
一对多关联:
tRoleList
(用户角色列表,TRole
类型)。
Java Bean 结构:
Java
// TUser.java
public class TUser {
private Integer id;
private String loginAct;
private String name; // 用户姓名
// ... 其他基本属性 ...
// 关联属性:创建人 (一对一)
private TUser createByDO;
// 关联属性:修改人 (一对一)
private TUser editByDO;
// 关联属性:角色列表 (一对多)
private List<TRole> tRoleList;
// Getter/Setter ...
}
// TRole.java
public class TRole {
private Integer id;
private String role; // 角色英文标识
private String name; // 角色名称
// Getter/Setter ...
}
数据库表结构(简化):
-
t_user
:id
,login_act
,name
,create_by
,edit_by
-
t_role
:id
,role
,name
-
t_user_role
:user_id
,role_id
(用户-角色中间表)
4.1 嵌套结果(即时加载)实现
目标: 通过一次 SQL 查询,获取用户所有信息,包括创建人、修改人的姓名/账号,以及所有关联的角色信息。
步骤 1:定义基本 resultMap
(可选,但推荐)
为 TUser
和 TRole
定义最基础的 resultMap
,用于映射它们自身的基本字段。这可以提高代码复用性。
XML
<mapper namespace="com.bjpowernode.mapper.UserMapper">
<resultMap id="baseUserResultMap" type="com.bjpowernode.entity.TUser">
<id column="id" property="id"/>
<result column="login_act" property="loginAct"/>
<result column="name" property="name"/>
</resultMap>
<resultMap id="baseRoleResultMap" type="com.bjpowernode.entity.TRole">
<id column="role_id" property="id"/>
<result column="role_key" property="role"/>
<result column="role_name" property="name"/>
</resultMap>
</mapper>
步骤 2:定义包含关联关系的 resultMap
现在,我们定义一个主 resultMap
,它将使用 <association>
和 <collection>
来映射关联对象。
XML
<mapper namespace="com.bjpowernode.mapper.UserMapper">
<resultMap id="UserWithAssociationsResultMap" type="com.bjpowernode.entity.TUser">
<id column="id" property="id"/>
<result column="login_act" property="loginAct"/>
<result column="name" property="name"/>
<result column="phone" property="phone"/>
<result column="email" property="email"/>
<result column="create_by" property="createBy"/>
<result column="edit_by" property="editBy"/>
<association property="createByDO" javaType="com.bjpowernode.entity.TUser">
<id column="create_by_id" property="id"/>
<result column="create_by_login_act" property="loginAct"/>
<result column="create_by_name" property="name"/>
</association>
<association property="editByDO" javaType="com.bjpowernode.entity.TUser">
<id column="edit_by_id" property="id"/>
<result column="edit_by_login_act" property="loginAct"/>
<result column="edit_by_name" property="name"/>
</association>
<collection property="tRoleList" ofType="com.bjpowernode.entity.TRole">
<id column="role_id" property="id"/>
<result column="role_key" property="role"/>
<result column="role_name" property="name"/>
</collection>
</resultMap>
</mapper>
解释关键点:
-
id="UserWithAssociationsResultMap"
: 定义一个供 SQL 语句引用的resultMap
ID。 -
type="com.bjpowernode.entity.TUser"
: 指定此resultMap
映射到的主 Java 对象类型。 -
association
和collection
内部的column
属性:-
这是最核心的地方。它不是直接映射数据库的原始列名,而是映射你在 SQL
SELECT
语句中为关联表的列定义的别名。 -
例如,
create_by_id
、create_by_name
、role_id
等,这些别名是为了区分主对象和关联对象的同名属性。
-
-
collection
的ofType
: 明确指定集合tRoleList
中元素的具体 Java 类型com.bjpowernode.entity.TRole
。MyBatis 会根据这个类型实例化对象。
步骤 3:编写 SQL 查询(使用 JOIN
和列别名)
Mapper 接口中的方法:TUser selectUserDetailById(Integer userId);
XML
<mapper namespace="com.bjpowernode.mapper.UserMapper">
<select id="selectUserDetailById" resultMap="UserWithAssociationsResultMap">
SELECT
tu.id,
tu.login_act,
tu.name,
tu.phone,
tu.email,
tu.account_no_expired,
tu.credentials_no_expired,
tu.account_no_locked,
tu.account_enabled,
tu.create_time,
tu.create_by,
tu.edit_time,
tu.edit_by,
tu.last_login_time,
-- 创建人 (createByDO) 的字段,使用 `create_by_` 前缀别名
cbu.id AS create_by_id,
cbu.login_act AS create_by_login_act,
cbu.name AS create_by_name,
-- 修改人 (editByDO) 的字段,使用 `edit_by_` 前缀别名
ebu.id AS edit_by_id,
ebu.login_act AS edit_by_login_act,
ebu.name AS edit_by_name,
-- 角色 (tRoleList) 的字段,使用 `role_` 前缀别名
tr.id AS role_id,
tr.role AS role_key,
tr.name AS role_name
FROM
t_user tu
LEFT JOIN
t_user cbu ON tu.create_by = cbu.id -- 关联创建人
LEFT JOIN
t_user ebu ON tu.edit_by = ebu.id -- 关联修改人
LEFT JOIN
t_user_role tur ON tu.id = tur.user_id -- 关联用户-角色中间表
LEFT JOIN
t_role tr ON tur.role_id = tr.id -- 关联角色表
WHERE
tu.id = #{userId}
</select>
</mapper>
核心点:
-
LEFT JOIN
: 用于连接所有相关的表,确保所有数据都能在一次查询中返回。 -
AS
别名: 这是实现resultMap
嵌套结果映射的关键。你必须为所有关联表的列定义唯一的别名,这些别名将与resultMap
中<association>
和<collection>
内部的column
属性精确匹配。MyBatis 根据这些别名将数据正确地填充到createByDO
、editByDO
和tRoleList
中。
五、总结与最佳实践
-
优先使用嵌套结果(Eager Loading): 大多数场景下,一次
JOIN
查询配合resultMap
的嵌套结果映射是最高效且推荐的方式,因为它避免了潜在的 N+1 查询问题。 -
明确的列别名: 在 SQL 查询中为关联表的列使用清晰、不重复的别名至关重要,这些别名将作为
resultMap
映射的依据。 -
<id>
的重要性: 在resultMap
中(特别是<collection>
内部),正确标记主键<id>
是 MyBatis 正确聚合数据到父对象的关键。它帮助 MyBatis 识别并避免重复创建父对象,同时将所有子数据添加到正确的集合中。 -
javaType
和ofType
: 务必为<association>
指定javaType
,为<collection>
指定ofType
,这样 MyBatis 才能正确地实例化和填充对象。 -
合理规划 SQL 复杂度: 复杂的
JOIN
语句虽然能一次性获取所有数据,但过度复杂的 SQL 可能会降低可读性和维护性。在必要时,可以考虑拆分查询或使用 MyBatis 的二级缓存来优化。