MyBatis 查询原理与字段映射
# MyBatis 查询原理与实体类映射
# 一、MyBatis 查询原理
MyBatis 作为一个半自动化的 ORM 框架,其查询过程涉及多个层面的转换和处理,下面详细说明其工作原理:
# 1. 配置文件解析阶段
- 初始化加载:应用启动时,MyBatis 读取并解析全局配置文件(
mybatis-config.xml
)和映射文件(Mapper XML) - 构建配置对象:解析配置后,创建
Configuration
对象,包含数据源、类型别名、映射器等信息 - 注册映射器:将所有的 Mapper 接口和 XML 配置文件中的 SQL 语句注册到
Configuration
对象中,建立namespace+id
到MappedStatement
的映射关系
# 2. 会话创建与准备阶段
- 创建 SqlSession:通过
SqlSessionFactory.openSession()
创建会话对象 - 获取 Mapper 代理:通过
SqlSession.getMapper(Class)
获取对应的 Mapper 接口的动态代理对象 - 方法调用拦截:当调用 Mapper 接口方法时,代理对象拦截请求,执行以下后续流程
# 3. SQL 解析与动态处理
- 获取映射语句:根据调用的方法和接口,找到对应的
MappedStatement
对象 - 动态 SQL 解析:
- 解析 XML 中的
<if>
、<where>
、<foreach>
等动态标签 - 根据传入的参数值判断条件是否成立
- 移除不成立的条件,保留满足条件的 SQL 片段
- 处理特殊字符,如自动处理
<where>
条件中的AND/OR
前缀
- 解析 XML 中的
- 最终 SQL 生成:将处理后的 SQL 片段组合成完整的 SQL 语句
# 4. 参数处理与绑定
- 参数映射:
- 获取方法传入的参数值
- 解析
@Param
注解,建立参数名到参数值的映射关系 - 对于无
@Param
注解的多参数方法,使用param1, param2
或arg0, arg1
作为参数名
- 参数解析:
- 解析 SQL 中的
#{}
占位符,提取参数名称和配置(如 jdbcType、typeHandler 等) - 将
#{}
替换为 JDBC 预编译占位符?
- 记录每个
?
对应的参数名和位置信息
- 解析 SQL 中的
- 类型处理:根据参数类型选择适当的
TypeHandler
来处理 Java 类型与 JDBC 类型之间的转换
# 5. JDBC 层处理
- 获取数据库连接:从数据源获取数据库连接
- 预编译 SQL:创建
PreparedStatement
对象,预编译 SQL 语句 - 设置参数:
- 遍历之前记录的参数信息
- 使用对应的
TypeHandler
将 Java 参数值设置到PreparedStatement
的对应位置
- 执行查询:调用
PreparedStatement.executeQuery()
执行查询,获取ResultSet
# 6. 结果集处理与对象映射
- 结果集元数据分析:
- 读取
ResultSet
的元数据信息(列名、类型等) - 根据结果类型决定映射策略(简单映射、嵌套映射等)
- 读取
- 结果映射过程:
- 遍历
ResultSet
中的每一行数据 - 创建目标对象实例(如 User 对象)
- 对于每个列:
- 找到列名对应的对象属性(考虑配置的命名策略,如驼峰转换)
- 使用适当的
TypeHandler
将 JDBC 类型转换为 Java 类型 - 通过反射或其他机制设置属性值
- 处理关联查询(一对一、一对多)关系,可能涉及额外的查询
- 返回填充后的对象集合
- 遍历
# 7. 缓存机制
- 一级缓存(SqlSession 级别):
- 默认开启,作用范围为同一 SqlSession 内
- 执行查询前先检查缓存,命中则直接返回缓存结果
- 同一 SqlSession 内的增删改操作会清空一级缓存
- 二级缓存(Mapper 级别):
- 需要手动配置开启,作用范围为同一 Mapper 的所有 SqlSession
- 当 SqlSession 关闭时,一级缓存结果会被推送到二级缓存
- 可配置各种缓存策略(LRU、FIFO 等)和刷新间隔
# 二、实体类与数据库字段的映射关系
# 1. 实体类字段与数据库字段数量关系
MyBatis 对接收实体类的字段数量没有严格要求,具有很高的灵活性:
# (1) 实体类字段少于数据库字段
// 数据库表:user(id, username, password, email, phone, address, create_time)
// 实体类只关心部分字段
public class UserBasicInfo {
private Integer id;
private String username;
private String email;
// getter 和 setter 方法
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
处理方式:
- MyBatis 只会尝试填充实体类中存在的属性,对于数据库中多出的字段会自动忽略
- 这种情况非常常见于只需要部分字段数据的查询场景
- 适用于视图对象(VO)、数据传输对象(DTO)等只关心部分字段的情况
# (2) 实体类字段多于数据库字段
// 数据库表:user(id, username, email)
// 实体类包含额外计算字段或非数据库字段
public class UserInfo {
private Integer id;
private String username;
private String email;
private Boolean isAdmin; // 非数据库字段,可能通过业务逻辑计算得出
private String fullAddress; // 非数据库字段,可能通过其他表数据组合生成
// getter 和 setter 方法
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
处理方式:
- 数据库不存在的字段会保持默认值(对象类型为 null,基本类型为默认值)
- 可以通过以下方式设置这些额外字段的值:
- 在 Java 代码中查询后手动设置
- 使用 MyBatis 的
<select>
中的表达式或函数计算 - 通过
ResultMap
的<constructor>
元素使用构造方法注入 - 通过关联查询填充(
<association>
或<collection>
)
# 2. 实体类字段命名与数据库字段不一致的处理
# (1) 使用列别名
最直接的方法是在 SQL 查询中使用列别名:
<select id="getUser" resultType="User">
SELECT
id,
username AS userName,
birth_date AS birthDate,
is_active AS active
FROM user
</select>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# (2) 开启驼峰命名自动转换
处理下划线命名到驼峰命名的转换:
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
1
2
3
2
3
开启后自动实现以下映射:
user_name
→userName
create_time
→createTime
# (3) 使用 ResultMap 进行手动映射
对于复杂或不规则的命名:
<resultMap id="userMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="createdAt" column="create_time"/>
<result property="status" column="user_status"/>
</resultMap>
<select id="getUser" resultMap="userMap">
SELECT user_id, user_name, create_time, user_status FROM user
</select>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# (4) 使用 @Results 注解(注解方式)
@Select("SELECT user_id, user_name, create_time, user_status FROM user")
@Results({
@Result(property = "id", column = "user_id"),
@Result(property = "name", column = "user_name"),
@Result(property = "createdAt", column = "create_time"),
@Result(property = "status", column = "user_status")
})
User getUser();
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 3. 开发中常见的实体类映射最佳实践
# (1) 分层设计模式
在实际项目中,通常会采用分层实体类设计:
- DO (Data Object):与数据库表结构完全对应的实体类
- DTO (Data Transfer Object):用于服务层之间传输数据的对象
- VO (View Object):用于展示层的视图对象
- BO (Business Object):业务对象,包含业务逻辑的实体类
// 数据库对象 - 与表结构完全对应
public class UserDO {
private Long id;
private String username;
private String password; // 敏感信息,仅数据层使用
private String email;
private Date createTime;
// getter 和 setter
}
// 数据传输对象 - 服务间传输,不包含敏感信息
public class UserDTO {
private Long id;
private String username;
private String email;
// getter 和 setter
}
// 视图对象 - 返回给前端,包含额外的展示信息
public class UserVO {
private Long id;
private String username;
private String email;
private Boolean isAdmin;
private String lastLoginTime; // 格式化后的时间
// getter 和 setter
}
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
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
# (2) 查询场景的映射策略
简单查询场景:
- 使用驼峰命名转换配置,保持实体类和数据库命名规范一致
- 对于临时需求,可以直接使用 Map 接收查询结果
// 直接使用 Map 接收查询结果
@Select("SELECT id, username, email FROM user WHERE id = #{id}")
Map<String, Object> getUserById(@Param("id") Long id);
1
2
3
2
3
复杂查询场景:
- 定义专用的结果实体类,只包含需要的字段
- 使用 ResultMap 处理复杂映射关系
// 定义专用的结果类
public class UserStatistics {
private String username;
private Integer articleCount; // 文章数量
private Date lastPostTime; // 最后发帖时间
// getter 和 setter
}
// 映射配置
<resultMap id="userStatsMap" type="UserStatistics">
<result property="username" column="username"/>
<result property="articleCount" column="article_count"/>
<result property="lastPostTime" column="last_post_time"/>
</resultMap>
<select id="getUserStatistics" resultMap="userStatsMap">
SELECT u.username, COUNT(a.id) as article_count, MAX(a.create_time) as last_post_time
FROM user u LEFT JOIN article a ON u.id = a.user_id
GROUP BY u.id
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# (3) 使用 BeanUtils 或对象映射框架
对于不同实体类之间的转换,可以使用:
- Apache BeanUtils
- Spring BeanUtils
- MapStruct
- ModelMapper
// 使用 Spring BeanUtils 进行对象转换
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
// 或使用 MapStruct 定义映射接口
@Mapper
public interface UserMapper {
UserVO doToVo(UserDO userDO);
UserDTO doToDto(UserDO userDO);
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# (4) 处理特殊类型的映射
枚举类型:
// 自定义 TypeHandler 处理枚举
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, UserStatus parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode());
}
@Override
public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return UserStatus.fromCode(code);
}
// 其他必要方法实现...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
JSON 数据:
// 处理 JSON 字段
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
private Class<T> clazz;
// 构造函数和方法实现...
}
// 在 XML 中配置
<result property="attributes" column="attributes" typeHandler="com.example.JsonTypeHandler"/>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
编辑此页 (opens new window)
上次更新: 2025/03/16, 13:51:03