问题背景
在开发文献筛选系统时,遇到了一个令人困惑的问题:通过 MyBatis 查询详情时,某些字段始终返回 null,但直接在数据库中查询却能看到数据。通过日志发现,部分字段显示为 <<BLOB>>,这揭示了问题的根源——MyBatis 的结果映射方式选择不当。
核心区别
resultType:自动映射
resultType 是 MyBatis 提供的简化映射方式,它会自动将查询结果映射到指定的 Java 类型。
映射规则:
按列名匹配:自动将数据库列名(或别名)与 Java 对象的属性名进行匹配
驼峰命名转换:如果开启了
mapUnderscoreToCamelCase,会自动将下划线命名转为驼峰命名顺序敏感:在某些情况下,列的顺序可能影响映射结果
类型推断:依赖 JDBC 驱动自动推断字段类型
优点:
代码简洁,无需额外配置
适合简单的一对一映射场景
缺点:
对复杂类型(BLOB、CLOB、JSON)支持较弱
无法显式指定 TypeHandler
列顺序与实体字段不匹配时可能出错
调试困难,映射问题不易发现
resultMap:显式映射
resultMap 是 MyBatis 提供的精确映射方式,需要显式定义每个字段的映射关系。
映射规则:
显式声明:明确指定每个数据库列与 Java 属性的对应关系
类型控制:可以指定
jdbcType和javaTypeTypeHandler:可以为特定字段指定类型处理器
顺序无关:不受 SQL 查询列顺序影响
优点:
映射关系清晰明确
完全控制字段映射细节
支持复杂类型(BLOB、JSON、自定义类型)
易于调试和维护
支持嵌套映射和关联查询
缺点:
代码量较大
需要手动维护映射配置
实际案例分析
问题场景
我的项目中有一个全文筛选的详情查询,实体类如下:
@Data
public class ProjectFullTextScreeningStepDetailResult {
private String id;
private String projectId;
private String stepId;
private String dataSource;
private String studyId;
private String studyUrl;
private String studyTitle;
private String author;
private String pubdate;
// 关键字段
private String fullTextContent; // longtext 类型
private String fullTextPdfUrl; // varchar(255)
private Integer fullTextStatus; // int
private Integer fullTextAiAgentScreeningStatus; // int
private Integer fullTextFinalDecisionStatus; // int
private Integer fullTextVeto; // int
@TableField(typeHandler = JacksonTypeHandler.class)
private PicoAnalysis fullTextExclusionReasons; // longtext (JSON)
private String fullTextAiExplanation; // longtext
}
数据库表结构:
CREATE TABLE slr_project_search_step_result (
ID varchar(64) PRIMARY KEY,
PROJECT_ID varchar(64),
STEP_ID varchar(64),
-- ... 其他基本字段
FULL_TEXT_CONTENT longtext, -- 全文内容
FULL_TEXT_PDF_URL varchar(255), -- PDF地址
FULL_TEXT_STATUS int, -- 全文状态
FULL_TEXT_AI_AGENT_SCREENING_STATUS varchar(255),
FULL_TEXT_VETO int,
FULL_TEXT_EXCLUSION_REASONS longtext, -- JSON格式
FULL_TEXT_AI_EXPLANATION longtext, -- AI解释
FULL_TEXT_FINAL_DECISION_STATUS int
);
错误写法(使用 resultType)
<select id="findByIdOfFullText"
resultType="vip.xiaonuo.client.modular.projectManagement.step.fifthFullTextScreening.result.ProjectFullTextScreeningStepDetailResult">
SELECT
r.`ID` AS `id`,
r.`PROJECT_ID` AS `projectId`,
r.`STEP_ID` AS `stepId`,
r.`DATA_SOURCE` AS `dataSource`,
r.`STUDY_ID` AS `studyId`,
r.`STUDY_URL` AS `studyUrl`,
r.`STUDY_TITLE` AS `studyTitle`,
r.`AUTHOR` AS `author`,
r.`PUBDATE` AS `pubdate`,
r.`FULL_TEXT_CONTENT` AS `fullTextContent`,
r.`FULL_TEXT_PDF_URL` AS `fullTextPdfUrl`,
r.`FULL_TEXT_EXCLUSION_REASONS` AS `fullTextExclusionReasons`,
r.`FULL_TEXT_AI_EXPLANATION` AS `fullTextAiExplanation`,
r.`FULL_TEXT_FINAL_DECISION_STATUS` AS `fullTextFinalDecisionStatus`
FROM `slr_project_search_step_result` r
WHERE r.ID = #{query.id} AND r.TENANT_ID = '-1'
</select>
问题分析:
缺少字段映射:SQL 查询缺少了
FULL_TEXT_STATUS、FULL_TEXT_AI_AGENT_SCREENING_STATUS、FULL_TEXT_VETO等字段BLOB 类型识别错误:MyBatis 日志显示
<<BLOB>>,说明longtext字段被识别为二进制大对象,无法自动转换为 StringTypeHandler 未生效:
fullTextExclusionReasons字段虽然在实体类中配置了JacksonTypeHandler,但在 resultType 模式下无法生效
查询日志:
==> Preparing: SELECT ... FROM slr_project_search_step_result r WHERE r.ID = ? AND r.TENANT_ID = '-1'
==> Parameters: 1994686882242932737(String)
<== Columns: id, projectId, ..., fullTextContent, fullTextPdfUrl, fullTextExclusionReasons, ...
<== Row: 1994686882242932737, ..., <<BLOB>>, null, <<BLOB>>, <<BLOB>>, null
<== Total: 1
可以看到:
fullTextContent显示为<<BLOB>>(无法转换)fullTextPdfUrl显示为null(实际数据库有值)fullTextExclusionReasons显示为<<BLOB>>(JSON 无法解析)
正确写法(使用 resultMap)
<!-- 定义 ResultMap -->
<resultMap id="FullTextDetailResultMap"
type="vip.xiaonuo.client.modular.projectManagement.step.fifthFullTextScreening.result.ProjectFullTextScreeningStepDetailResult">
<!-- 主键映射 -->
<id column="id" property="id"/>
<!-- 基本字段映射 -->
<result column="projectId" property="projectId"/>
<result column="stepId" property="stepId"/>
<result column="dataSource" property="dataSource"/>
<result column="studyId" property="studyId"/>
<result column="studyUrl" property="studyUrl"/>
<result column="studyTitle" property="studyTitle"/>
<result column="author" property="author"/>
<result column="pubdate" property="pubdate"/>
<!-- 关键:为 longtext 字段指定 jdbcType -->
<result column="fullTextContent" property="fullTextContent" jdbcType="LONGVARCHAR"/>
<!-- 普通字符串字段 -->
<result column="fullTextPdfUrl" property="fullTextPdfUrl"/>
<!-- 整型字段 -->
<result column="fullTextStatus" property="fullTextStatus"/>
<result column="fullTextAiAgentScreeningStatus" property="fullTextAiAgentScreeningStatus"/>
<result column="fullTextFinalDecisionStatus" property="fullTextFinalDecisionStatus"/>
<result column="fullTextVeto" property="fullTextVeto"/>
<!-- 关键:JSON 字段需要指定 TypeHandler -->
<result column="fullTextExclusionReasons"
property="fullTextExclusionReasons"
jdbcType="LONGVARCHAR"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
<!-- 大文本字段 -->
<result column="fullTextAiExplanation" property="fullTextAiExplanation" jdbcType="LONGVARCHAR"/>
</resultMap>
<!-- 使用 ResultMap 的查询 -->
<select id="findByIdOfFullText" resultMap="FullTextDetailResultMap">
SELECT
r.`ID` AS `id`,
r.`PROJECT_ID` AS `projectId`,
r.`STEP_ID` AS `stepId`,
r.`DATA_SOURCE` AS `dataSource`,
r.`STUDY_ID` AS `studyId`,
r.`STUDY_URL` AS `studyUrl`,
r.`STUDY_TITLE` AS `studyTitle`,
r.`AUTHOR` AS `author`,
r.`PUBDATE` AS `pubdate`,
r.`FULL_TEXT_CONTENT` AS `fullTextContent`,
r.`FULL_TEXT_PDF_URL` AS `fullTextPdfUrl`,
r.`FULL_TEXT_STATUS` AS `fullTextStatus`,
r.`FULL_TEXT_AI_AGENT_SCREENING_STATUS` AS `fullTextAiAgentScreeningStatus`,
r.`FULL_TEXT_FINAL_DECISION_STATUS` AS `fullTextFinalDecisionStatus`,
r.`FULL_TEXT_VETO` AS `fullTextVeto`,
r.`FULL_TEXT_EXCLUSION_REASONS` AS `fullTextExclusionReasons`,
r.`FULL_TEXT_AI_EXPLANATION` AS `fullTextAiExplanation`
FROM `slr_project_search_step_result` r
WHERE r.ID = #{query.id} AND r.TENANT_ID = '-1'
</select>
修复效果:
使用 resultMap 后,查询日志变为:
==> Preparing: SELECT ... FROM slr_project_search_step_result r WHERE r.ID = ? AND r.TENANT_ID = '-1'
==> Parameters: 1994686882242932737(String)
<== Columns: id, projectId, ..., fullTextContent, fullTextPdfUrl, ...
<== Row: 1994686882242932737, ..., The Oncologist. 2025..., https://ftp.ncbi.nlm.nih.gov/pub/pmc/..., {...}, The study shows...
<== Total: 1
所有字段都正确返回了!
关键技术点解析
1. JDBC 类型映射
MyBatis 支持的 JDBC 类型包括:
为什么需要指定 jdbcType="LONGVARCHAR"?
当数据库字段是 text 或 longtext 类型时:
JDBC 驱动可能将其识别为
CLOB或BLOB类型MyBatis 默认会尝试以二进制方式读取,导致显示为
<<BLOB>>显式指定
jdbcType="LONGVARCHAR"告诉 MyBatis 将其作为字符串处理
<!-- 错误:没有指定 jdbcType -->
<result column="fullTextContent" property="fullTextContent"/>
<!-- 结果:<<BLOB>> -->
<!-- 正确:指定为 LONGVARCHAR -->
<result column="fullTextContent" property="fullTextContent" jdbcType="LONGVARCHAR"/>
<!-- 结果:正常的字符串内容 -->
2. TypeHandler 的使用
TypeHandler 用于在 Java 类型和 JDBC 类型之间进行转换。
场景1:JSON 字段
<!-- 实体类中的定义 -->
@TableField(typeHandler = JacksonTypeHandler.class)
private PicoAnalysis fullTextExclusionReasons;
<!-- ResultMap 中的配置 -->
<result column="fullTextExclusionReasons"
property="fullTextExclusionReasons"
jdbcType="LONGVARCHAR"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
JacksonTypeHandler 的作用:
查询时:将数据库中的 JSON 字符串自动转换为 Java 对象
插入/更新时:将 Java 对象自动序列化为 JSON 字符串
场景2:自定义 TypeHandler
// 假设需要加密存储某些字段
public class EncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
// 加密后存储
ps.setString(i, encrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 解密后返回
return decrypt(rs.getString(columnName));
}
// ... 其他方法实现
}
使用:
<result column="sensitiveData"
property="sensitiveData"
typeHandler="com.example.handler.EncryptTypeHandler"/>
3. 列顺序问题
这是一个容易被忽略但可能造成严重问题的点。
问题场景:
假设实体类定义:
public class User {
private String id; // 1
private String name; // 2
private Integer age; // 3
private String email; // 4
}
SQL 查询(注意列的顺序):
<!-- 错误示例:列顺序与实体字段不一致 -->
<select id="getUser" resultType="User">
SELECT
id, -- 映射到 id ✓
email, -- 映射到 name ✗ (类型不匹配,可能为null)
name, -- 映射到 age ✗ (类型转换失败)
age -- 映射到 email ✗
FROM user WHERE id = #{id}
</select>
虽然使用了别名,但某些情况下(特别是查询列数与实体字段数不匹配时),MyBatis 可能按顺序进行映射,导致数据错位。
解决方案:使用 resultMap
<resultMap id="userMap" type="User">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="email" property="email"/>
</resultMap>
<select id="getUser" resultMap="userMap">
SELECT id, email, name, age -- 顺序无所谓
FROM user WHERE id = #{id}
</select>
最佳实践建议
何时使用 resultType
✅ 适用场景:
简单的查询,字段类型都是基本类型(String、Integer、Date等)
列名与属性名完全匹配(或通过驼峰命名自动匹配)
没有复杂类型(BLOB、CLOB、JSON)
一次性查询,不需要复用映射配置
<!-- 示例:简单查询 -->
<select id="getUserById" resultType="User">
SELECT id, name, age, email
FROM user
WHERE id = #{id}
</select>
何时必须使用 resultMap
⚠️ 必须使用的场景:
包含大字段类型(text、longtext、blob)
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
需要使用 TypeHandler(JSON、加密、枚举等)
<result column="metadata" property="metadata"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
复杂的关联查询(一对一、一对多)
<resultMap id="orderMap" type="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<association property="user" javaType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
</association>
</resultMap>
列名与属性名差异较大
<result column="create_time" property="gmtCreate"/>
<result column="update_time" property="gmtModified"/>
需要部分字段映射(不是所有列都需要)
<resultMap id="userSimpleMap" type="User">
<result column="id" property="id"/>
<result column="name" property="name"/>
<!-- 其他字段不映射 -->
</resultMap>
混合使用策略
在实际项目中,可以灵活混合使用:
<!-- 简单列表查询:使用 resultType -->
<select id="listUsers" resultType="User">
SELECT id, name, age, email
FROM user
WHERE status = 1
</select>
<!-- 详情查询(包含大字段):使用 resultMap -->
<select id="getUserDetail" resultMap="userDetailMap">
SELECT
id, name, age, email,
profile, -- longtext
preferences -- JSON
FROM user
WHERE id = #{id}
</select>
<resultMap id="userDetailMap" type="User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="email" property="email"/>
<result column="profile" property="profile" jdbcType="LONGVARCHAR"/>
<result column="preferences" property="preferences"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
</resultMap>
性能对比
两种方式的性能差异微乎其微:
结论:性能差异可以忽略不计,应该以功能正确性和代码可维护性为优先考虑因素。
调试技巧
1. 开启 MyBatis 日志
# application.yml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台输出
# 或使用 slf4j
# log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
# 或在 logback.xml 中配置
<logger name="vip.xiaonuo.client.modular.projectManagement.mapper" level="DEBUG"/>
日志输出示例:
==> Preparing: SELECT * FROM user WHERE id = ?
==> Parameters: 123(String)
<== Columns: id, name, age, email, profile
<== Row: 123, John, 25, john@example.com, <<BLOB>> -- 发现 BLOB 问题
<== Total: 1
2. 使用 HashMap 接收结果
当不确定映射是否正确时,可以临时使用 HashMap:
<select id="debugQuery" resultType="java.util.HashMap">
SELECT * FROM user WHERE id = #{id}
</select>
然后在代码中打印:
Map<String, Object> result = mapper.debugQuery("123");
System.out.println(result);
// 输出:{id=123, name=John, age=25, email=john@example.com, profile=[B@5a07e868}
// 注意:BLOB 类型显示为 byte 数组
3. 单元测试验证
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testGetUserDetail() {
User user = userMapper.getUserDetail("123");
// 验证基本字段
assertNotNull(user.getId());
assertNotNull(user.getName());
// 验证大字段
assertNotNull(user.getProfile());
assertNotEquals("<<BLOB>>", user.getProfile());
// 验证 JSON 字段
assertNotNull(user.getPreferences());
assertTrue(user.getPreferences() instanceof Map);
}
}
常见错误及解决方案
错误1:BLOB 字段显示为 <<BLOB>>
原因:MyBatis 将 longtext 识别为 BLOB 类型
解决:
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
错误2:JSON 字段解析失败
原因:未指定 TypeHandler
解决:
<result column="jsonData" property="jsonData"
jdbcType="LONGVARCHAR"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
错误3:字段值为 null 但数据库有值
可能原因:
SQL 查询缺少该字段
列名与属性名不匹配
多租户插件添加了额外的 WHERE 条件
排查步骤:
// 1. 检查 SQL 是否包含该列
// 2. 检查别名是否正确
// 3. 开启日志查看实际执行的 SQL
// 4. 直接在数据库执行该 SQL 验证
错误4:TypeHandler 不生效
原因:在 resultType 模式下,实体类的 @TableField 注解中的 typeHandler 不会生效
解决:必须使用 resultMap 并显式指定 typeHandler
<!-- 错误:在 resultType 下 TypeHandler 不生效 -->
<select id="getUser" resultType="User">
SELECT id, preferences FROM user WHERE id = #{id}
</select>
<!-- 正确:使用 resultMap -->
<select id="getUser" resultMap="userMap">
SELECT id, preferences FROM user WHERE id = #{id}
</select>
<resultMap id="userMap" type="User">
<result column="preferences" property="preferences"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
</resultMap>
总结
核心要点
resultType 适合简单场景,依赖自动映射
resultMap 适合复杂场景,提供精确控制
大字段(longtext、text)必须指定
jdbcType="LONGVARCHAR"JSON 字段必须使用 resultMap 并指定 TypeHandler
当出现
<<BLOB>>时,说明类型映射有问题
决策树
是否包含大字段(text/longtext/blob)?
├─ 是 → 使用 resultMap
└─ 否 → 是否需要 TypeHandler(JSON/加密等)?
├─ 是 → 使用 resultMap
└─ 否 → 是否有复杂关联查询?
├─ 是 → 使用 resultMap
└─ 否 → 可以使用 resultType
我的建议
在实际项目中,我倾向于:
列表查询:使用 resultType(简洁高效)
详情查询:使用 resultMap(安全可靠)
关联查询:必须使用 resultMap
当遇到字段映射问题时,第一时间考虑改用 resultMap,这能解决 90% 的映射问题。
参考资料:
关键词: MyBatis, resultType, resultMap, TypeHandler, BLOB, LONGVARCHAR, JSON映射, 字段映射
📝 本文记录于 2024年12月,基于 MyBatis 3.5.x 和 MyBatis-Plus 3.5.x 版本
💡 如果本文对你有帮助,欢迎点赞收藏!如有问题,欢迎在评论区讨论。
默认评论
Halo系统提供的评论