资讯详情

资讯详情

MyBatis resultMap实战:彻底解决数据库字段与Java属性映射难题

做MyBatis查数据的时候最让人头大的不是SQL写不出来而是SQL明明查出了字段Java对象里却对应不上结果一片null。尤其是“数据库字段下划线命名”和“Java属性驼峰命名”这种看似差一点点的问题调试起来特别容易让人原地怀疑人生字段名看着对属性名也认识但结果就是装不进去。这个问题的标准解法就是自定义映射resultMap。往浅了说它解决的是“user_id”和“userId”谁对应谁的问题往深了说它可以管理主键、构造器参数、嵌套对象、结果集自动映射规则甚至多表联查里的列名冲突。这篇是自定义映射专题的第四篇我聚焦在最基础也最容易翻车的一块字段名和属性名的映射关系到底是怎么一回事以及resultMap应当怎么配才算真正配明白。不管你是刚接触MyBatis不久还是已经写过不少resultMap我相信这篇里都有一些值得你停下来细看的内容。1. 为什么明明字段名一样查出来却是null1.1 一个最常见的“看起来没问题”的翻车现场先看一段很多人都会写的代码。数据库表里有user_id、user_name、email三个字段Java实体类长这样public class User { private Integer userId; private String userName; private String email; // getter/setter 省略 }Mapper XML里的查询也很常规select idselectById resultTypecom.example.entity.User select user_id, user_name, email from user where id #{id} /select查出来的结果是email有值userId和userName是null。为什么因为MyBatis默认没有开启下划线转驼峰它拿数据库返回的列名user_id去找Java属性时不会自动把下划线去掉再变成userId。换句话说MyBatis不是不知道user_id是什么而是它不知道user_id应该对应userId。如果你给SQL里的列名手动起别名例如user_id as userId那可以解决当前一条SQL的问题。但如果项目里有几十个表、几百个查询每一条SQL都要靠别名来纠正命名差异工作量上去不说还特别容易漏。这种场景天生就是resultMap的主场。1.2 从JDBC到MyBatis结果集是怎么变成对象的要真正理解resultMap得先知道MyBatis默认是怎么把结果集变成对象的。数据库执行完SQL后返回的是JDBC的ResultSet。MyBatis拿到结果集之后会调用ResultSetMetaData.getColumnLabel()拿到每一列的名字然后在配置的目标类型里找对应的属性名找到就去调用它的setter方法找不到就跳过这一列不报错也不提示。这个自动匹配过程就叫自动映射。自动映射的默认策略是autoMappingBehavior为PARTIAL。在这个策略下如果列名和属性名本身能对应上就会自动赋值如果列名是user_id而属性名是userId又没有开启下划线转驼峰那就只能得到一个null。这也就解释了为什么翻车现场往往静悄悄MyBatis不会跟你说“我找不到userId的列”它只会默默地忽略掉那一列。所以排查这类问题第一反应不该是怀疑SQL写错了而是先想清楚当前查询用的是resultType还是resultMap、列名和属性名之间到底差在哪。2. resultMap到底做了什么核心原理拆解2.1 它不是“代码生成器”而是一张映射说明书很多人对resultMap有个误解以为它只是把数据库字段和Java属性一对一列出来就行。其实resultMap更像是一张“人工翻译词典”它的核心元素是type指明了结果要装进哪个类内部再通过id、result、constructor这些子元素告诉MyBatis每一列应该怎么处理。一个最小的resultMap长这样resultMap iduserMap typecom.example.entity.User id propertyid columnid / result propertyuserName columnuser_name / result propertyemail columnemail / /resultMap这里property写的是Java属性名column写的是数据库列名或SQL别名。MyBatis拿到结果集后会按照这张说明书的描述去创建对象、调setter、赋值。type的作用是告诉MyBatis该创建哪个类的实例所以这个类必须有默认构造方法或者你通过constructor显式指定构造参数。2.2 id与result标签看似一样实际分工完全不同在resultMap里id和result都能把数据库列映射到Java属性但它们的语义完全不同。很多新手图省事把所有字段全部写成result或者反过来把所有字段都写成id短时间看不出问题等用到二级缓存或者嵌套结果映射时就会踩坑。标签作用典型使用场景id标记主键字段MyBatis会用它的值参与缓存key的计算和对象唯一性判断映射数据库主键、逻辑主键、唯一业务键result映射普通字段只负责赋值不参与唯一性逻辑普通列、业务字段、聚合字段主键字段在结果集里是“这一行是谁”的标识。如果一张结果集里没有唯一键或者你非要把一个姓名列写成id缓存和嵌套映射都可能错乱。开发规范比较严格的项目里一条resultMap通常只有一个id其余全用result。2.3 type属性与autoMapping的隐藏规则还有一个容易被忽略的点resultMap并不要求把所有列都列全。默认情况下MyBatis在应用resultMap之后仍然会尝试对没有显式映射的列做自动映射。也就是说下面这种resultMap只声明了两列resultMap iduserMap typecom.example.entity.User id propertyid columnid / result propertyuserName columnuser_name / /resultMap如果查询结果里还有email这一列email属性依然可能被自动映射上因为MyBatis发现它没在resultMap里会回过头按自动映射规则再跑一遍。这个机制是好是坏要分场景。好处是省事坏处是容易“偷偷映射”到你不希望被赋值的字段。当你希望结果集里的列完全由resultMap掌控可以在resultMap上显式加autoMappingfalseresultMap iduserMap typecom.example.entity.User autoMappingfalse ... /resultMap这样就不会再有“漏网之列”被自动塞进对象里了。3. 从实际案例看字段名和属性名的四种映射困境3.1 下划线转驼峰能解决但别乱开全局配置针对user_name到userName这种映射需求MyBatis提供了一个全局开关settings setting namemapUnderscoreToCamelCase valuetrue/ /settings开启之后所有查询的列名会自动去掉下划线并转成驼峰user_id会变成userId。这个开关确实好用很多新项目一上来就把它打开。但我建议你在老项目里谨慎动这个全局配置。因为它影响的是所有Mapper、所有查询不仅包括你写的SQL还包括公共模块里别人写的SQL甚至第三方组件内置的SQL。一旦某个SQL里的列名用了user_name但Java属性偏偏也叫user_name开启后可能反而对不上。所以我的习惯是新项目可以开老项目尽量用resultMap做局部映射不轻易动全局配置。3.2 数据库聚合字段与DTO属性的错位写统计报表时SQL经常会有这种聚合结果select count(*) as total_count, max(amount) as max_price, avg(amount) as avg_price from orderDTO里的属性大概率是totalCount、maxPrice、avgPrice。如果查询直接用resultType就还得靠别名把它们写成驼峰比如count(*) as totalCount。但这种写法的问题是SQL里的别名跟Java对象耦合在了一起换一个DTO复用这段SQL时又要重新改别名。用resultMap可以把这个耦合拆开resultMap idorderStatMap typecom.example.vo.OrderStatVO result propertytotalCount columntotal_count / result propertymaxPrice columnmax_price / result propertyavgPrice columnavg_price / /resultMap查询方法再写哪一列就无所谓了select idselectOrderStat resultMaporderStatMap select count(*) as total_count, max(amount) as max_price, avg(amount) as avg_price from order /select这样SQL里保留数据库习惯的命名Java对象里保留Java风格的命名中间只靠resultMap这一层翻译。3.3 查询结果里多出来的冗余字段有时候SQL是同事写好的里面顺手select *了一堆字段你的对象里却只想要其中一部分。如果使用自动映射多余字段只要能对上属性名就会被Java对象接收多出来的数据白白增加了内存占用更危险的是可能把不该暴露的字段也装进去。这种场景我建议在resultMap上把该映射的列显式声明出来同时关掉自动映射resultMap idsafeUserMap typecom.example.vo.SafeUserVO autoMappingfalse id propertyid columnid / result propertynickname columnnick_name / /resultMap这么写的效果是无论查询结果里多出多少列只要不在resultMap里MyBatis都不会把它们映射到对象上安全边界非常清晰。3.4 多表联查的字段前缀冲突多表联查是字段名映射的重灾区。两个表都叫id、都叫name时如果你直接select *后一个字段很可能把前一个覆盖掉。即便你用resultType加别名也很难保证后面新同事改SQL时还会记得保留别名。更稳妥的方式是用resultMap统一管理联查列resultMap idorderUserMap typecom.example.vo.OrderUserVO id propertyorderId columnorder_id / result propertyuserId columnuser_id / result propertyuserName columnuser_name / result propertytotalAmount columntotal_amount / /resultMapSQL这边只要保证列名或者别名跟resultMap里的column一致就行select idselectOrderUser resultMaporderUserMap select o.id as order_id, o.user_id as user_id, u.name as user_name, o.total_amount as total_amount from orders o left join user u on o.user_id u.id where o.id #{orderId} /select联查结果里真正要落到Java对象的字段全由resultMap说了算。多加一张表只需要在SQL里追加列名和别名Java对象的映射不会再被意外破坏。4. 动手配置resultMap的完整流程含完整XML示例4.1 配置前先想清楚的三个问题动手写resultMap之前我建议你先回答三个问题返回值是一个普通POJO还是需要嵌套另一个对象的复杂结果这决定了你是用result还是需要association、collection。列名与属性名之间是否存在无法靠自动映射解决的差异如果所有列名都和属性名一致resultMap可以写得非常精简。这条映射规则以后会不会复用如果会建议给它起一个语义清晰的id方便其他查询通过resultMap引用。想清楚之后写resultMap就是填空式的操作。4.2 XML里的书写顺序与常见错误一个完整但简单的resultMap配置可以是这样的resultMap idorderUserMap typecom.example.vo.OrderUserVO autoMappingfalse id propertyorderId columnorder_id / result propertyuserId columnuser_id / result propertyuserName columnuser_name / result propertytotalAmount columntotal_amount / /resultMap对应的select改造为select idselectOrderUser resultMaporderUserMap select o.id as order_id, o.user_id as user_id, u.name as user_name, o.total_amount as total_amount from orders o left join user u on o.user_id u.id where o.id #{orderId} /select这里有一个很容易被XML解析器教训的坑resultMap子元素的书写顺序是有要求的。MyBatis的XML DTD规定constructor要在最前面接着是id和result然后才是association和collection。我见过不少同事把association写在result前面结果启动项目时报了一堆“内容类型必须匹配”的错误其实就是顺序不对。4.3 与注解方式的对比什么时候用注解更省事除了XMLMyBatis也支持用注解声明映射。Select(select user_id, user_name, email from user where id #{id}) Results(id userMap, value { Result(id true, property userId, column user_id), Result(property userName, column user_name), Result(property email, column email) }) User selectById(Integer id);注解方式的优点是写起来集中查询方法、SQL、映射关系都在同一个地方适合简单查询。但如果映射关系复杂或者一个resultMap要被多个查询复用我个人更推荐XML。原因很简单XML里的resultMap是一个独立命名的配置块任何Mapper方法都能通过resultMapxxx引用而注解里写的Results(id userMap)虽然也能复用但在大量SQL方法里维护起来远不如XML直观。5. resultMap配置得好不好看两个细节5.1 constructor标签处理不可变对象如果实体类的属性是final的或者没有无参构造方法默认的对象创建方式就行不通。这时得用constructor告诉MyBatis怎么调用构造方法。举个例子一个统计VO只有构造器没有setterpublic class OrderStatVO { private final Long totalCount; private final BigDecimal maxPrice; public OrderStatVO(Long totalCount, BigDecimal maxPrice) { this.totalCount totalCount; this.maxPrice maxPrice; } }resultMap配置resultMap idorderStatMap typecom.example.vo.OrderStatVO constructor idArg columntotal_count javaTypelong / arg columnmax_price javaTypejava.math.BigDecimal / /constructor /resultMap注意idArg一般用于标记构造参数里的主键字段跟id的语义类似普通构造参数用arg。如果你的实体类有默认构造方法大部分情况下不需要写constructor但真遇到不可变对象时这个配置就是唯一解法。5.2 复用resultMapextends的坑和正确姿势当一个resultMap的基础映射可以复用时可以用extends减少重复配置resultMap idbaseUserMap typecom.example.entity.User id propertyid columnid / result propertyuserName columnuser_name / /resultMap resultMap idorderUserMap extendsbaseUserMap typecom.example.vo.OrderUserVO result propertyorderNo columnorder_no / /resultMap这里有个容易忽略的坑父resultMap的type和子resultMap的type可以不同但子type对应的类必须继承或至少包含父resultMap里映射的那些属性。也就是说子VO里必须有id、userName这些字段否则MyBatis在往子对象里赋值时会找不到setter而报错。另外如果父级和子级对同一个column做了不同的映射运行结果可能和你想的不一样。我不建议靠“覆盖”去处理这种问题最好在设计SQL时就给列起不同的别名从源头避免同一个column出现在两个层级里。5.3 调试技巧如何快速确认映射是不是对的排查字段名映射问题最快的方法不是盯着实体类看而是把日志打开。MyBatis的日志配置里可以看到SQL执行后返回的列名列表例如logging.level.com.example.mapperdebug日志里通常会输出 Columns: user_id, user_name, email和 Row: 1, 张三, emailexample.com。看到列名之后再去比对resultMap里的column值问题基本一眼就能定位。如果日志里没有详细列名还有一个土办法先把查询的resultType临时改成map让MyBatis返回MapString, Object。这样你能直接看到SQL实际返回的key是什么是user_id还是userId然后再去调整resultMap。等确认好后再改回正规的返回类型。这个办法虽然不够“优雅”但在排场上非常管用。最后说一个我自己的习惯不要把resultMap当成一个“出了问题才救火”的工具而是在设计Mapper接口时就把字段映射规则固定下来。宁可多写几行XML也不要依赖SQL别名和全局开关“凑巧”映射成功。项目里的SQL会越来越多但一份结构清晰的resultMap永远比临时补的一个别名更能经得起时间考验。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →