结果映射
结果映射(ResultMap)是 MyBatis 最强大的特性之一,用于处理数据库结果集与 Java 对象之间的映射。本章将详细介绍各种映射场景和技巧。
自动映射
MyBatis 支持自动映射,当数据库列名与 Java 属性名一致时,可以自动完成映射。
开启自动映射
<!-- 全局配置 -->
<settings>
<!-- NONE: 禁用自动映射 -->
<!-- PARTIAL: 只自动映射没有嵌套的结果(默认) -->
<!-- FULL: 自动映射所有结果(包括嵌套) -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
</settings>
驼峰命名映射
<settings>
<!-- 开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
开启后,数据库列名 create_time 会自动映射到 Java 属性 createTime。
<!-- 无需手动映射 -->
<select id="selectById" resultType="User">
SELECT id, username, create_time FROM user WHERE id = #{id}
</select>
resultMap 基础
当自动映射无法满足需求时,使用 resultMap 进行手动映射。
基本结构
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="email" property="email"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
属性说明
| 属性 | 说明 |
|---|---|
id | resultMap 的唯一标识 |
type | 映射的 Java 类型(全限定名或别名) |
autoMapping | 是否启用自动映射(覆盖全局设置) |
extends | 继承其他 resultMap |
id 和 result 元素
<resultMap id="BaseResultMap" type="User">
<!-- id 元素:标识主键字段,提高性能 -->
<id column="id" property="id"/>
<!-- result 元素:普通字段映射 -->
<result column="username" property="username"/>
<result column="create_time" property="createTime"/>
</resultMap>
| 属性 | 说明 |
|---|---|
property | Java Bean 属性名 |
column | 数据库列名或列别名 |
javaType | Java 类型(通常可自动推断) |
jdbcType | JDBC 类型 |
typeHandler | 类型处理器 |
使用 resultMap
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
构造器映射
当类没有无参构造器或需要通过构造器注入时,使用构造器映射。
实体类
public class User {
private final Long id;
private final String username;
private String email;
public User(Long id, String username) {
this.id = id;
this.username = username;
}
// setter/getter
}
映射配置
<resultMap id="ConstructorResultMap" type="User">
<constructor>
<idArg column="id" javaType="long"/>
<arg column="username" javaType="string"/>
</constructor>
<result column="email" property="email"/>
</resultMap>
constructor 元素属性
| 属性 | 说明 |
|---|---|
column | 数据库列名 |
javaType | Java 类型 |
jdbcType | JDBC 类型 |
typeHandler | 类型处理器 |
select | 嵌套查询 |
resultMap | 嵌套结果映射 |
idArg 和 arg
idArg:标识主键参数arg:普通参数
<resultMap id="ConstructorResultMap" type="User">
<constructor>
<idArg column="id" javaType="long" name="id"/>
<arg column="username" javaType="string" name="username"/>
<arg column="email" javaType="string" name="email"/>
</constructor>
</resultMap>
使用 name 属性可以按名称匹配构造器参数(需要 Java 8+ 且开启 -parameters 编译选项)。
关联映射(一对一)
使用 association 元素处理一对一关联关系。
数据模型
实体类
@Data
public class User {
private Long id;
private String username;
private Order latestOrder;
}
@Data
public class Order {
private Long id;
private String orderNo;
private BigDecimal amount;
private Long userId;
}
嵌套结果映射
<resultMap id="UserWithOrderMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<association property="latestOrder" javaType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</association>
</resultMap>
<select id="selectUserWithLatestOrder" resultMap="UserWithOrderMap">
SELECT u.*, o.id as order_id, o.order_no, o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
ORDER BY o.id DESC
LIMIT 1
</select>
嵌套查询
<resultMap id="UserWithOrderMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<!-- 嵌套查询:通过 select 指定另一个查询 -->
<association property="latestOrder"
column="id"
select="com.example.mybatis.mapper.OrderMapper.selectLatestByUserId"/>
</resultMap>
<select id="selectById" resultMap="UserWithOrderMap">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- OrderMapper.xml -->
<select id="selectLatestByUserId" resultType="Order">
SELECT * FROM orders
WHERE user_id = #{userId}
ORDER BY id DESC
LIMIT 1
</select>
association 属性
| 属性 | 说明 |
|---|---|
property | Java Bean 属性名 |
javaType | Java 类型 |
column | 传递给嵌套查询的列名 |
select | 嵌套查询语句 ID |
fetchType | 加载方式:lazy/eager |
resultMap | 引用外部 resultMap |
延迟加载
<settings>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
<resultMap id="UserWithOrderMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<!-- 延迟加载 -->
<association property="latestOrder"
column="id"
select="com.example.mybatis.mapper.OrderMapper.selectLatestByUserId"
fetchType="lazy"/>
</resultMap>
集合映射(一对多)
使用 collection 元素处理一对多关联关系。
实体类
@Data
public class User {
private Long id;
private String username;
private List<Order> orders;
}
@Data
public class Order {
private Long id;
private String orderNo;
private BigDecimal amount;
}
嵌套结果映射
<resultMap id="UserWithOrdersMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="UserWithOrdersMap">
SELECT u.*, o.id as order_id, o.order_no, o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
嵌套查询
<resultMap id="UserWithOrdersMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<collection property="orders"
column="id"
ofType="Order"
select="com.example.mybatis.mapper.OrderMapper.selectByUserId"/>
</resultMap>
<select id="selectById" resultMap="UserWithOrdersMap">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- OrderMapper.xml -->
<select id="selectByUserId" resultType="Order">
SELECT * FROM orders WHERE user_id = #{userId}
</select>
collection 属性
| 属性 | 说明 |
|---|---|
property | Java Bean 属性名 |
ofType | 集合元素类型 |
column | 传递给嵌套查询的列名 |
select | 嵌套查询语句 ID |
fetchType | 加载方式:lazy/eager |
resultMap | 引用外部 resultMap |
ofType vs javaType
ofType:集合元素的类型javaType:集合本身的类型(通常可自动推断)
<!-- ofType 指定 List 中元素的类型 -->
<collection property="orders" ofType="Order">
<!-- javaType 指定集合类型(可选) -->
<collection property="orders" javaType="ArrayList" ofType="Order">
鉴别器映射
使用 discriminator 元素根据列值选择不同的映射规则。
实体类
@Data
public abstract class Vehicle {
private Long id;
private String name;
private Integer type;
}
@Data
@EqualsAndHashCode(callSuper = true)
public class Car extends Vehicle {
private Integer doorCount;
}
@Data
@EqualsAndHashCode(callSuper = true)
public class Truck extends Vehicle {
private BigDecimal loadCapacity;
}
映射配置
<resultMap id="VehicleResultMap" type="Vehicle">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="type" property="type"/>
<discriminator javaType="int" column="type">
<case value="1" resultMap="CarResultMap"/>
<case value="2" resultMap="TruckResultMap"/>
</discriminator>
</resultMap>
<resultMap id="CarResultMap" type="Car" extends="VehicleResultMap">
<result column="door_count" property="doorCount"/>
</resultMap>
<resultMap id="TruckResultMap" type="Truck" extends="VehicleResultMap">
<result column="load_capacity" property="loadCapacity"/>
</resultMap>
<select id="selectVehicleById" resultMap="VehicleResultMap">
SELECT * FROM vehicle WHERE id = #{id}
</select>
内联 case
<resultMap id="VehicleResultMap" type="Vehicle">
<id column="id" property="id"/>
<result column="name" property="name"/>
<discriminator javaType="int" column="type">
<case value="1" resultType="Car">
<result column="door_count" property="doorCount"/>
</case>
<case value="2" resultType="Truck">
<result column="load_capacity" property="loadCapacity"/>
</case>
</discriminator>
</resultMap>
继承映射
使用 extends 属性继承其他 resultMap。
<!-- 基础映射 -->
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
</resultMap>
<!-- 扩展映射 -->
<resultMap id="DetailResultMap" type="User" extends="BaseResultMap">
<result column="phone" property="phone"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<!-- 带关联的扩展映射 -->
<resultMap id="UserWithOrdersMap" type="User" extends="BaseResultMap">
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
</collection>
</resultMap>
复杂映射示例
多层级关联
<resultMap id="UserWithOrdersAndItemsMap" type="User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<collection property="items" ofType="OrderItem">
<id column="item_id" property="id"/>
<result column="quantity" property="quantity"/>
<association property="product" javaType="Product">
<id column="product_id" property="id"/>
<result column="product_name" property="name"/>
</association>
</collection>
</collection>
</resultMap>
<select id="selectUserWithOrdersAndItems" resultMap="UserWithOrdersAndItemsMap">
SELECT
u.id as user_id, u.username,
o.id as order_id, o.order_no,
oi.id as item_id, oi.quantity,
p.id as product_id, p.name as product_name
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
LEFT JOIN order_item oi ON o.id = oi.order_id
LEFT JOIN product p ON oi.product_id = p.id
WHERE u.id = #{id}
</select>
实体类
@Data
public class User {
private Long id;
private String username;
private List<Order> orders;
}
@Data
public class Order {
private Long id;
private String orderNo;
private List<OrderItem> items;
}
@Data
public class OrderItem {
private Long id;
private Integer quantity;
private Product product;
}
@Data
public class Product {
private Long id;
private String name;
}
多对多关联
<resultMap id="UserWithRolesMap" type="User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<collection property="roles" ofType="Role">
<id column="role_id" property="id"/>
<result column="role_name" property="name"/>
<collection property="permissions" ofType="Permission">
<id column="permission_id" property="id"/>
<result column="permission_name" property="name"/>
</collection>
</collection>
</resultMap>
<select id="selectUserWithRoles" resultMap="UserWithRolesMap">
SELECT
u.id as user_id, u.username,
r.id as role_id, r.name as role_name,
p.id as permission_id, p.name as permission_name
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN role r ON ur.role_id = r.id
LEFT JOIN role_permission rp ON r.id = rp.role_id
LEFT JOIN permission p ON rp.permission_id = p.id
WHERE u.id = #{id}
</select>
性能优化
N+1 问题
使用嵌套查询时可能产生 N+1 问题:
<!-- 产生 N+1 问题 -->
<resultMap id="UserWithOrdersMap" type="User">
<id column="id" property="id"/>
<collection property="orders"
column="id"
select="selectOrdersByUserId"/>
</resultMap>
<!-- 查询 1 次 -->
<select id="selectUsers" resultMap="UserWithOrdersMap">
SELECT * FROM user
</select>
<!-- 每个用户查询 1 次,共 N 次 -->
<select id="selectOrdersByUserId" resultType="Order">
SELECT * FROM orders WHERE user_id = #{userId}
</select>
解决方案
方案一:使用嵌套结果映射(JOIN)
<resultMap id="UserWithOrdersMap" type="User">
<id column="id" property="id"/>
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
</collection>
</resultMap>
<select id="selectUsersWithOrders" resultMap="UserWithOrdersMap">
SELECT u.*, o.id as order_id, o.order_no
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
</select>
方案二:批量嵌套查询
<resultMap id="UserWithOrdersMap" type="User">
<id column="id" property="id"/>
<collection property="orders"
column="{userId=id}"
select="selectOrdersByUserIds"
fetchType="lazy"/>
</resultMap>
<select id="selectUsers" resultMap="UserWithOrdersMap">
SELECT * FROM user
</select>
<select id="selectOrdersByUserIds" resultType="Order">
SELECT * FROM orders WHERE user_id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</select>
延迟加载配置
<settings>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 按需加载(false)或全部加载(true) -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 触发延迟加载的方法 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
最佳实践
1. 使用 id 元素标识主键
<resultMap id="BaseResultMap" type="User">
<!-- 使用 id 而不是 result -->
<id column="id" property="id"/>
<result column="username" property="username"/>
</resultMap>
2. 合理使用自动映射
<!-- 简单映射使用自动映射 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 复杂映射使用 resultMap -->
<select id="selectWithOrders" resultMap="UserWithOrdersMap">
...
</select>
3. 避免过度嵌套
<!-- 不推荐:多层嵌套查询 -->
<association property="a" select="...">
<association property="b" select="...">
<association property="c" select="..."/>
</association>
</association>
<!-- 推荐:使用 JOIN 一次性查询
4. 复用 resultMap
<!-- 定义可复用的 resultMap -->
<resultMap id="OrderResultMap" type="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
</resultMap>
<!-- 引用复用 -->
<resultMap id="UserWithOrdersMap" type="User">
<id column="id" property="id"/>
<collection property="orders" resultMap="OrderResultMap"/>
</resultMap>
小结
本章详细介绍了 MyBatis 结果映射的各种场景:
- 自动映射:简单场景下的自动映射
- resultMap:手动映射配置
- constructor:构造器映射
- association:一对一关联
- collection:一对多关联
- discriminator:鉴别器映射
- extends:继承映射
- 性能优化:解决 N+1 问题
下一章将介绍 MyBatis 的缓存机制。