清一色
2025-12-11
点 赞
0
热 度
4
评 论
0

MyBatis 中 resultType 和 resultMap 的区别及最佳实践

  1. 首页
  2. 随手记
  3. MyBatis 中 resultType 和 resultMap 的区别及最佳实践

问题背景

在开发文献筛选系统时,遇到了一个令人困惑的问题:通过 MyBatis 查询详情时,某些字段始终返回 null,但直接在数据库中查询却能看到数据。通过日志发现,部分字段显示为 <<BLOB>>,这揭示了问题的根源——MyBatis 的结果映射方式选择不当。

核心区别

resultType:自动映射

resultType 是 MyBatis 提供的简化映射方式,它会自动将查询结果映射到指定的 Java 类型。

映射规则:

  1. 按列名匹配:自动将数据库列名(或别名)与 Java 对象的属性名进行匹配

  2. 驼峰命名转换:如果开启了 mapUnderscoreToCamelCase,会自动将下划线命名转为驼峰命名

  3. 顺序敏感:在某些情况下,列的顺序可能影响映射结果

  4. 类型推断:依赖 JDBC 驱动自动推断字段类型

优点:

  • 代码简洁,无需额外配置

  • 适合简单的一对一映射场景

缺点:

  • 对复杂类型(BLOB、CLOB、JSON)支持较弱

  • 无法显式指定 TypeHandler

  • 列顺序与实体字段不匹配时可能出错

  • 调试困难,映射问题不易发现

resultMap:显式映射

resultMap 是 MyBatis 提供的精确映射方式,需要显式定义每个字段的映射关系。

映射规则:

  1. 显式声明:明确指定每个数据库列与 Java 属性的对应关系

  2. 类型控制:可以指定 jdbcTypejavaType

  3. TypeHandler:可以为特定字段指定类型处理器

  4. 顺序无关:不受 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>

问题分析:

  1. 缺少字段映射:SQL 查询缺少了 FULL_TEXT_STATUSFULL_TEXT_AI_AGENT_SCREENING_STATUSFULL_TEXT_VETO 等字段

  2. BLOB 类型识别错误:MyBatis 日志显示 <<BLOB>>,说明 longtext 字段被识别为二进制大对象,无法自动转换为 String

  3. TypeHandler 未生效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 类型包括:

Java 类型

JDBC 类型

适用场景

String

VARCHAR

普通字符串

String

LONGVARCHAR

text、longtext 大文本

String

CLOB

CLOB 类型

byte[]

BLOB

二进制数据

Integer

INTEGER

整数

Date

TIMESTAMP

时间戳

为什么需要指定 jdbcType="LONGVARCHAR"

当数据库字段是 textlongtext 类型时:

  • JDBC 驱动可能将其识别为 CLOBBLOB 类型

  • 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

适用场景:

  1. 简单的查询,字段类型都是基本类型(String、Integer、Date等)

  2. 列名与属性名完全匹配(或通过驼峰命名自动匹配)

  3. 没有复杂类型(BLOB、CLOB、JSON)

  4. 一次性查询,不需要复用映射配置

<!-- 示例:简单查询 -->
<select id="getUserById" resultType="User">
    SELECT id, name, age, email
    FROM user
    WHERE id = #{id}
</select>

何时必须使用 resultMap

⚠️ 必须使用的场景:

  1. 包含大字段类型(text、longtext、blob)

<result column="content" property="content" jdbcType="LONGVARCHAR"/>
  1. 需要使用 TypeHandler(JSON、加密、枚举等)

<result column="metadata" property="metadata" 
        typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
  1. 复杂的关联查询(一对一、一对多)

<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>
  1. 列名与属性名差异较大

<result column="create_time" property="gmtCreate"/>
<result column="update_time" property="gmtModified"/>
  1. 需要部分字段映射(不是所有列都需要)

<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>

性能对比

两种方式的性能差异微乎其微:

维度

resultType

resultMap

映射速度

略快(反射直接设值)

略慢(需要查找映射配置)

内存占用

相同

相同

SQL 执行

相同

相同

结论:性能差异可以忽略不计,应该以功能正确性代码可维护性为优先考虑因素。

调试技巧

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 但数据库有值

可能原因:

  1. SQL 查询缺少该字段

  2. 列名与属性名不匹配

  3. 多租户插件添加了额外的 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>

总结

核心要点

  1. resultType 适合简单场景,依赖自动映射

  2. resultMap 适合复杂场景,提供精确控制

  3. 大字段(longtext、text)必须指定 jdbcType="LONGVARCHAR"

  4. JSON 字段必须使用 resultMap 并指定 TypeHandler

  5. 当出现 <<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 版本

💡 如果本文对你有帮助,欢迎点赞收藏!如有问题,欢迎在评论区讨论。


大道至简,知易行难

清一色

isfp 探险家

站长

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性
切换评论

目录

八月寻英,扬帆起航,追风逐梦!!!

44 文章数
7 分类数
2 评论数
15标签数