十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MyBatis映射全攻略:从参数映射到缓存联动的完整指南

MyBatis映射全攻略:从参数映射到缓存联动的完整指南 最近在帮团队做一次代码审查发现不少同学在使用 MyBatis 时对“映射”这件事的理解还停在“能跑就行”的层面。等出了问题比如字段查出来是 null、关联查询报错、批量插入没回填主键现场就一步步 debug 查半天。项目里积累的这些痛点我觉得很有必要单独拿出来聊一聊。这篇就从实际踩坑出发把 MyBatis 映射里那些必须注意的点系统梳理一遍覆盖参数映射、结果映射、关系映射、缓存联动和常见排查套路基本是目前唯一一篇能把映射讲完整的总结。1. 映射配置的几个基础但致命的细节1.1 接口绑定与 XML 命名空间的对应关系MyBatis 的映射体系里mapper 接口和 XML 映射文件是通过“命名空间 操作 id”来绑定的很多人起步时没意识到这套绑定关系有多严格。接口的全限定名必须和 XML 中mapper namespace...的值完全一致比如接口是com.xxx.mapper.UserMappernamespace 就必须写成com.xxx.mapper.UserMapper少一个包名、拼错大小写都会在启动时或首次调用时报Invalid bound statement (not found)。这个报错信息可以说是 MyBatis 入门第一坑排查思路是先检查 namespace 是否和接口全限定名一致再检查 XML 中的 id 是否和接口方法名一致最后看 target/classes 下有没有把 XML 编译进去。光这一条我见过有人排查了一下午最后发现是 XML 文件名和接口名不同导致扫描不到。需要特别提醒的是在 Spring Boot 项目中XML 放在src/main/resources/mapper/目录下必须要在application.yml里配置mybatis.mapper-locations: classpath:mapper/*.xml否则 XML 不会被加载。接口上的Mapper注解解决的只是让 MyBatis 去扫描这个接口生成代理对象它不负责加载 XML。换句话说接口是“门面”XML 才是真正干活的“员工”两者没有正确绑定门面就是空壳。接口方法和 XML 中的 id 对应关系也有讲究。方法没有参数时id 就是方法名方法带参数时建议显式使用Param注解来标注参数名否则在编译时如果开启了-parameters参数还好没开启的话MyBatis 只能按param1、param2或者arg0、arg1来引用参数代码一旦写得复杂了容易引用错位置而且可读性也很差。实际开发中哪怕只有一个参数只要这个参数是简单类型且 XML 中要拿它做条件判断我都习惯加上Param避免后续加参数时影响 SQL 语义。1.2 resultType 和 resultMap 的选择逻辑这是很多人一开始就纠结的问题查询结果到底用resultType还是resultMapresultType的核心要求是查询出来的列名能直接映射到对象属性上它走的是 MyBatis 的自动映射机制。数据库字段是user_name实体属性是userName只要开启了map-underscore-to-camel-case: true默认在 Spring Boot 中实际上是 true但单独使用 MyBatis 时默认是 false就能自动完成下划线到驼峰的转换。如果查询结果列名和属性名对不上要么给列起合适的别名要么就不用resultType。resultMap则更加强大和灵活。它可以处理多表关联查询的结果映射、嵌套对象映射一对一、一对多、类型转换器TypeHandler的指定以及当列名和属性名无法通过自动映射规则匹配时的显式映射。比如枚举类型、JSON 字段、时间精度处理这些场景resultMap配合typeHandler才能完整兜住。我的建议是单表简单查询、字段能自动对应上的用resultType省事省力涉及多表关联、嵌套对象、特殊类型处理的一律用resultMap避免在歧义和性能上绕弯子。你可能会遇到resultType是 Map、resultMap是拥有明确列映射的情况这两种并不是非此即彼的关系很多时候一个查询里可以同时使用列别名加resultType来简化 XML。有一段真实的踩坑经历分享我曾经用resultTypejava.util.Map去接收一个多表关联查询结果结果用户信息确实出来了但时间字段变成了一长串时间戳前端拿到的数据根本无法直接展示。后来才发现是 MyBatis 对日期类型默认映射的行为在不配置 typeHandler 的情况下直接返回了底层 JDBC 的java.sql.Timestamp包装成 Map 后踩了类型映射的坑。这个问题用resultMap配置对应的typeHandler或者查询时用DATE_FORMAT函数格式化就好解决。1.3 自动映射级别到底怎么配置MyBatis 的自动映射有三个级别NONE、PARTIAL、FULL可以通过auto-mapping-behavior配置。默认是PARTIAL意思是结果集中没有嵌套结果映射时自动映射会生效NONE就是完全关闭自动映射所有字段必须手动指定FULL是即使有嵌套结果映射也会自动映射。实际开发中我几乎不会去改这个全局配置但在使用resultMap时有个隐藏的知识点值得注意在resultMap中如果手动指定了部分列的result未被显式指定的列仍然会根据自动映射规则尝试匹配在PARTIAL级别下。所以当查询列特别多、你又只想映射其中几列时最好在 SQL 中只 select 需要的那几列避免无关列参与自动映射也避免查询出无用数据浪费性能。另外一个容易被忽略的配置是map-underscore-to-camel-case这个配置在resultMap中同样起作用。如果你在resultMap里只手动映射了id而查询结果中有user_name列、实体中有userName属性只要开启了下划线转驼峰这个字段也能自动映射上。这其实是一个很典型的小技巧能帮你减少 resultMap 里的重复定义。2. 参数映射的坑比你想象的要多2.1 单参数与多参数传递时的行为差异参数映射看起来简单实际是最容易出诡异问题的地方。先用单参数说。如果接口方法的参数是实体对象比如UserXML 中直接通过属性名引用即可比如#{userName}、#{age}。这里面的坑在于如果你传的是一个 Map引用方式是#{key}如果你传的是简单类型比如 String、Integer那么直接写#{任意名字}甚至#{value}都可以拿到因为 MyBatis 对单简单类型参数做了特殊处理。但这种写法留给后续扩展的隐患很大因为你不知道 SQL 里引用的名字和图方便时写下的名字是否一致一旦重构就可能踩到。多参数情况就要靠Param注解或者依赖默认的param1、param2了。有一个我反复提醒团队的问题在不使用Param注解的情况下如果 IDE 的编译参数没有开启-parameters那么 XML 中只能通过arg0、arg1或者param1、param2来引用而且这两套下标都是从 0 和 1 各自开始的搞混了就会各种离谱错误。我在很多项目里建议过参数多于一个时必须显式标注Param不要留任何模糊空间这不是风格问题是可靠性问题。2.2 #{} 和 ${} 的映射差异与注入风险#{}会被 MyBatis 解析成 JDBC 的预编译参数占位符?可以防 SQL 注入${}是字符串替换直接把值拼到 SQL 里有注入风险。这是基础但依然有人搞混特别是在动态排序、动态表名、动态列名的场景里。有个典型例子用户需要对列表按指定列排序前端传了个sortField后端直接${sortField}拼到ORDER BY后面。表面上功能正常了但这是相当危险的写法。排序字段和表名这类结构体是无法用预编译参数替代的#{}在这里会直接报错因为 JDBC 不允许把 bind 变量用在表名或列名位置。这种场景下的安全做法是对所有允许传入的字段做白名单校验比如用 Map 维护合法列名到真实列名的映射关系而不是直接把用户输入拼进 SQL。另外提一下参数映射中的类型问题。#{createTime}在默认情况下会自动把 Java 的LocalDateTime映射到 JDBC 的TIMESTAMP一般没问题但如果数据库驱动版本较老可能需要加jdbcTypeTIMESTAMP来显式指定。还有一种情况是某些数据库对 NULL 参数的类型无法自动推断会报JDBCException所以必要的时候在传 null 可能性较高的字段上加jdbcTypeOTHER或者对应类型。这个建议来自实际生产环境的折腾经验不加在个别 Oracle 驱动下真的会从骨子里折磨人。2.3 动态 SQL 里判空判值的注意点动态 SQL 的映射问题本质上是参数值的逻辑判断与拼接问题。经典场景一if teststatus Y.toString()很多人直接写teststatus Y在 OGNL 表达式中单字符会被当作 char 类型处理和 String 类型的Y比较会收到意外结果所以必须调用toString()方法或者反过来写Y.equals(status.toString())或者直接teststatus Y.toString()。这种问题不报错但 SQL 就是不进这个条件调试时查半天才发现是类型比较的问题。经典场景二if testname ! null and name ! 这个写法本身没问题但需要注意传入的 name 可能存在空格。如果业务上认为空字符串和纯空格等价需要自己 trim否则条件永远成立。这个细节在开发联调阶段最容易暴露因为前端表单常常会传空格。经典场景三foreach遍历时 collection 属性值的写法。如果参数是数组写collectionarray如果是 List写collectionlist如果用了Param(ids)那么写collectionids。很多人会忘记这种对应关系特别是参数中既有实体对象又有 List 的时候稍不留神就写错 collection。建议对集合参数一律加上Param命名见名知意同时 XML 里用对应的名字既清晰又避免踩坑。3. 结果映射里的关系映射与缓存联动3.1 一对多、多对一映射的实现方式和性能代价association用于一对一、多对一比如一个用户对应一个部门collection用于一对多比如一个部门对应多个用户。两者都可以通过嵌套结果映射和嵌套查询两种方式实现。嵌套查询的方式是在 resultMap 的 association/collection 上指定select另一个查询的 statement idMyBatis 会为每一行结果额外发起一次查询也就是传说中的 N1 问题。列表查出来 100 条用户每条用户还要单独查一次部门信息一共 101 条 SQL。在这种方式下如果设置了fetchTypelazy能够延迟到真正访问关联属性时才发起查询但前提是 MyBatis 全局配置中允许懒加载且当前操作在 SqlSession 生命周期内。这个“生命周期”的限制很隐蔽如果你在 Service 层通过事务获取到实体后事务已经提交、SqlSession 已经关闭再在 Controller 层尝试访问懒加载属性就会触发LazyInitializationException。嵌套结果的方式是把多表查询的 resultMap 配置成 association/collection 嵌套由一个 SQL join 出所有字段MyBatis 内部按 resultMap 进行映射。这种方式避免了 N1但要注意结果集字段不能重名尤其是多个表都有相同的id列必须在 SQL 中给它们起不同的别名否则映射会错乱。这种错乱的排查成本很高因为它不报错只是数据莫名其妙。3.2 懒加载配置的实际生效场景MyBatis 的懒加载配置主要涉及三个点lazyLoadingEnabled是否启用懒加载、aggressiveLazyLoading是否积极触发懒加载、lazyLoadTriggerMethods哪些方法调用会触发懒加载。aggressiveLazyLoading默认是 false在较新版本中但如果它是 true那么当你调用主对象的任何 getter 方法时所有未加载的关联对象都会被立即加载这等于把懒加载关了。而lazyLoadTriggerMethods默认包含equals、clone、hashCode、toString也就是说你只要调用toString()方法打印实体对象就会触发全部懒加载属性的加载。这个坑真的很“坑”很多人在 debug 时看一眼实体对象关联数据就全被加载了导致后续接口性能突然变差排查半天找不到原因。实际代码中我习惯这样处理全局开启lazyLoadingEnabled和aggressiveLazyLoadingfalse然后在不需要懒加载的关联查询上使用fetchTypeeager显式指定。这个方法能保持默认的懒加载能力同时为特定场景开了口子。3.3 二级缓存与映射数据一致性问题MyBatis 的一级缓存是 SqlSession 级别的同一个 SqlSession 内多次查询同一 SQL 会命中缓存这个默认开启。二级缓存是 namespace 级别的默认关闭需要在 XML 中加入cache/来配置同时实体类要实现Serializable。二级缓存和多表映射之间有一个经典一致性问题如果一个 namespace 缓存了User查询但User关联的Department数据发生了更新更新发生在另一个 namespace 中此时第一个 namespace 的缓存并不会被清空导致查出来的用户关联部门信息还是旧数据。很多人天真地认为 MyBatis 会自动处理缓存联动实际上它只能感知自己 namespace 内的写操作。遇到这种多表关联且二级缓存开启的项目最稳妥的方案是直接关闭二级缓存或者将相关表的所有写操作统一在同一个 mapper namespace 中执行并注册缓存清除。生产环境中如果主要依赖数据库自身的查询缓存或外层 Redis 缓存我一般建议 MyBatis 的二级缓存关掉减少这种跨 namespace 的缓存一致性噩梦。这套判断对于数据一致性要求高的金融、订单类项目尤其重要宁可多查一次数据库也不能让关联数据变成“薛定谔的新鲜”。3.4 批量插入与主键回填批量插入在使用了映射的多表操作中也要格外小心。MySQL 下useGeneratedKeystrue keyPropertyid可以实现主键回填也就是插入后 MyBatis 会把自增主键值写回传入对象或集合中对象的id属性。这个特性在批量插入时同样有效前提是你使用同一个批量 tagforeach生成一条大的 insert 语句且数据库驱动支持批量返回自增主键。如果使用的是foreach这样拼接的批量 insert对应多条记录时MyBatis 能正确回写每条记录的 id例如 MySQL 5.1.13 驱动做了支持但如果你使用了ExecutorType.BATCH结合逐条插入的方式此时自增主键的回填有可能会失效。具体的区分在于ExecutorType.BATCH模式会延迟执行 SQL此时 JDBC 返回的主键是通过Statement.getGeneratedKeys()获取的而驱动和 MyBatis 的版本兼容性也会影响回填。这里踩坑较多我建议在项目初期确定技术选型时就针对批量插入主键回填做一次明确的集成测试选一条稳定路线固定下来。4. 映射不生效时的排查思路与工具链4.1 常见报错和对应排查路径说到映射问题最经典的报错就那几个整理成速查表方便大家对着查报错信息触发原因排查重点Invalid bound statement (not found)接口/XML 绑定不上namespace、id、mapper-locations 配置ResultMap collection 映射出 null列别名冲突或 collection resultMap 字段没对上SQL 别名、resultMap column 属性LazyInitializationExceptionSqlSession 已关闭后访问懒加载属性事务边界、fetchType 配置TypeException: Could not set parameters for mapping参数类型与 JDBC 类型不匹配Param、jdbcType、TypeHandlerSQLSyntaxErrorException但 SQL 看起来没问题${} 替换导致 SQL 结构异常动态排序、表名场景改用白名单多表结果字段互相覆盖多个表有同名字段且未起别名SQL 中显式给重名字段起别名4.2 实战排查技巧如何快速定位映射错误排查映射问题时我有一套固定流程分享给大家。第一步确认 XML 是否被正确加载。在 Spring Boot 项目中启动时开启mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl或者配置mybatis-plus的日志输出能看到 MyBatis 初始化时加载了哪些 XML如果发现自己的 XML 不在列表里基本就是 mapper-locations 配错了。第二步确认参数和结果映射是否被正确解析。开启 MyBatis 的 SQL 日志后观察预编译 SQL 中的?数量和实际传入参数的类型。如果传入参数类型不对日志中也能看出来如果 resultMap 映射出错一个比较隐蔽的表现是日志中打印的查询结果里某列始终为 null而数据库中不是 null那么十有八九是结果列的 column 名与实体的 property 名对不上。第三步找一个最小复现单元。遇到复杂的关联映射问题不要在大业务里断点调试应该抽出一个独立的 Mapper 方法、最小 SQL 来测试。这种“降维排查”在 MyBatis 映射问题上特别高效因为 SQL 的输出和映射规则的逻辑变得透明问题自然暴露。第四步用${}临时拼一个完整 SQL 来排查映射关系。这只用于测试阶段确认字段是否能够映射上调试完之后必须改回#{}并且不要在代码中留下${}。我在团队中见过太多排查到最后发现是这种临时改动被误提交到了生产仓库的情况所以这个操作需要特别谨慎。4.3 日志工具和插件加速映射问题定位开发阶段有条件的话可以引入 MyBatis 的 SQL 日志插件比如 IDEA 的MyBatis Log Free插件它能将 MyBatis 的预编译 SQL 和参数列表拼成一条可以直接执行的 SQL这样你就能直接在数据库客户端执行排查极大地提升了联调和排错效率。生产环境排查映射问题时不建议直接把 SQL 全部打到日志里因为关联查询打印出来的 SQL 可能很长、很占日志空间。我通常的做法是在配置中心里加一个动态开关按需开启某个 service 的 SQL 日志排查完立刻关闭。这个思路比一上来就开启全局 SQL 日志更可控也不会因为日志刷盘影响接口性能。5. 结合 Spring Boot 的映射配置实践5.1 Spring Boot 下的推荐配置模板Spring Boot 整合 MyBatis 时最常用的配置模板我整理了一份直接贴进来mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true lazy-loading-enabled: false aggressive-lazy-loading: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有几个关键点说明一下。type-aliases-package配置了实体类包路径后XML 中就不用写全限定类名了可以直接写resultTypeUser。这个省事的背后有一个坑如果包扫描范围太大可能会导致不同类型的同名类产生冲突MyBatis 在启动时就会报错。所以 aliases 包路径的划分要清晰不要图省事把整个项目的包都扫进去。map-underscore-to-camel-case是必须开启的除非你的所有数据库字段和实体属性都保持一致命名但那样往往意味着数据库设计不规范反正我见过的大多数正规项目都采用了下划线风格。开启这个配置能省掉很多无效的 resultMap 定义。lazy-loading-enabled我默认设置为 false原因在前面已经分析过了——懒加载的门槛比较高SqlSession 生命周期和事务边界不控制好就容易出问题。如果你确实需要懒加载再开启并且做好事务边界的设计。5.2 当表不存在自动建表的场景热词列表里有一条“springboot mybatis 当表不存在自动建表”这个场景在实际开发中也很常见尤其是在测试环境、演示环境或者一些轻量级项目里。MyBatis 本身不提供建表能力它只负责 SQL 执行。要实现“表不存在自动建表”通常的做法是在应用启动时执行一段建表 SQL可以是 Spring Boot 的schema.sql配合spring.sql.init.modealways也可以在应用启动监听器里通过JdbcTemplate或专门的初始化 Mapper 来执行CREATE TABLE IF NOT EXISTS语句。这个做法有一个适配映射的注意点如果建表逻辑和 MyBatis 的实体映射不同步很容易出现字段对不上的问题。比如实体里新增了一个字段但建表 SQL 没有同步查询的时候 MyBatis 按映射规则找字段发现数据库没有这一列运行时报Unknown column。我的经验是自动建表只适合演示、测试场景生产环境必须走规范的数据库版本管理工具比如 Flyway、Liquibase这是底线。5.3 IP 白名单和字段加密场景下的映射处理还有一个容易被忽略的场景当数据库字段存储的是加密数据时映射处理也要特别留意。比如用户的手机号在数据库中是密文存储而查询时需要将手机号传给下游系统如果用 resultType 直接映射映射得到的就是密文字段值。如果要在查询结果返回给前端前完成解密常规做法有三种第一种在实体类的 setter 方法里解密。这样做侵入性很强而且会在所有涉及该属性的场景生效不推荐。第二种自定义TypeHandler在查询结果从 JDBC 类型转换为 Java 属性类型时解密在插入或更新时加密。这是比较推荐的方案因为 TypeHandler 能够在 SQL 参数绑定和结果集取出两个阶段左右数据流转对业务代码零侵入且易于复用。第三种在 Service 层统一做字段解密转换简单直接但效率上会多一层对象拷贝对于分页列表场景不太友好。我自己的很多项目中是自定义 TypeHandler 来做字段级别的加解密的这个方案可以和resultMap配合得非常自然只需要在result columnphone propertyphone typeHandlercom.example.handler.EncryptTypeHandler/指定即可一处配置全局生效。6. 映射对象的类型设计与设计规范6.1 实体对象、查询对象、返回 VO 的职责分离“映射对象类型”是另一个高频关注点。很多初学者直接用数据库实体对象去接收前端传参又把实体对象直接返回给前端这种设计在简单 Demo 里没问题项目一复杂就问题百出。正确做法是角色分离实体对象Entity对应数据库表结构承载的是持久层的数据形态。查询对象Query承载查询条件可以包含分页参数、多条件组合、排序字段。返回对象VO/DTO对应接口返回的数据形态可以聚合多个表的数据也可以裁剪掉不必要的字段。这样做的好处是映射关系清晰Entity 映射表结构VO 映射接口结构Query 映射查询条件。如果三者混在一起结果就是接口字段、数据库字段、查询条件互相干扰Mapper 里的映射配置也会越写越复杂最终变成无人敢改的“屎山”。6.2 枚举映射的常见处理方式MyBatis 映射枚举时默认使用 EnumTypeHandler也就是按枚举的name()和数据库字段的字符串值进行匹配。如果你的枚举值顺序调整了数据库中存的是name()就会导致历史数据读取异常。更推荐的做法是存枚举的 code 值比如 int 类型在实体中使用枚举类型并且通过自定义 TypeHandler 或者EnumValueMyBatis-Plus 提供进行映射。在纯 MyBatis 中自定义 TypeHandler 处理枚举是标准做法。一个典型的枚举处理器需要实现BaseTypeHandlerT并覆盖setNonNullParameter、getNullableResult等方法。代码写起来不复杂但一定要做单元测试特别是 null 值场景和未知 code 值场景。如果代码里遇到无法识别的 code 值Handler 里要有明确的行为定义比如抛异常或者按照默认枚举值处理否则线上就是隐晦的 NPE。6.3 Map 接收结果集的取舍有些临时查询用resultTypejava.util.Map接收结果集非常方便多表 join 查询时不用专门定义 VO。但这种做法有两个明显的坑第一Map 中的 key 是列名列名的大小写行为取决于数据库驱动。Oracle 返回的列名会被转成大写MySQL 返回的列名保持查询书写的原始大小写这会导致你从 Map 里取 key 时要小心翼翼。团队协作中如果每个人都“照自己喜欢的方式”写列别名这个 Map 的 key 就完全失去约定模块间传值非常容易出问题。第二Map 接收结果集时会丢失 MyBatis 的自动类型映射能力Date 变时间戳的问题就是这么来的。在快速原型和统计报表中Map 接收是可以接受的但在核心业务链路中我应该尽量让返回结果落在一个结构化类型上这样编译器能帮你提前发现问题运行时也能减少类型转换异常。7. 缓存、打印与调试映射之外的联动注意点7.1 缓存配置对映射结果的影响前面已经详细分析过二级缓存的 namespace 级别一致性坑这里再补一个一级缓存的注意点。一级缓存默认开启范围是 SqlSession。在 Spring Boot 集成的 MyBatis 中每次请求默认都会开启新的 SqlSession由 SqlSessionTemplate 管理所以一级缓存的粒度通常不会跨请求生效。但如果在同一个事务中执行了多次相同查询第一次查询后修改了其中一个字段第二次查询若无特殊说明可能会命中缓存拿到的还是旧值这也是个隐蔽的一致性陷阱。处理办法是在 Mapper 的更新语句后调用sqlSession.clearCache()或者把事务划分得更细。还有一种情况使用了Transactional的方法内存在多次查询同一对象但如果你的方法中间调用了改库操作但并未提交一级缓存并不会因为改库而失效因为 MyBatis 清缓存是在更新语句执行时发生。这里有一个隐藏的坑updateUser方法若执行成功但事务在后续某一步回滚此时一级缓存中可能已经存储了由这个未提交事务的更新影响的旧查询结果导致用户看到数据异常。为了避免这种复杂问题我通常建议强制事务边界内避免依赖一级缓存做业务判断。7.2 SQL 打印与 MyBatis 参数映射热词里提到“mybatis配置打印”这是排查映射问题的第一利器。把setting namelogImpl valueSTDOUT_LOGGING/打开或者按 Spring Boot 的mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl配置后控制台会打印每条执行 SQL 的预编译形态和传入的参数值。实际工作中我更推荐使用 mapper 接口所在包的 logback 日志级别设置为 DEBUG这样既能查看打印出的 SQL也能控制全局日志量。如果使用了 MyBatis-Plus也可以借助其内置的 SQL 日志输出能力但要注意的是不同的日志实现打印出来的信息详细程度不同最好在排查阶段使用最详细的配置。7.3 MyBatis 和 MyBatis-Plus 在映射上的差异很多团队已经切换到 MyBatis-Plus它和 MyBatis 的关系不是替代而是在 MyBatis 之上做了增强。映射层面MyBatis-Plus 最大的变化是默认开启了实体类到表结构的自动映射下划线转驼峰默认开启并且提供TableName、TableId、TableField这套注解来补充映射规则。在使用 MyBatis-Plus 时映射的注意点主要在这些注解的语义上TableField(column_name)可以显式指定列名当实体属性和列名差异较大时非常有用。TableField(existfalse)表示该属性并非数据库字段这些属性用于承载关联数据或临时计算值如果不标记MyBatis-Plus 在自动生成 SQL 时会把不存在的字段拼进去凭空多出 SQL 语法错误。TableId(type IdType.AUTO)用于指定主键策略比如 AUTO 代表数据库自增INPUT 代表手工输入ASSIGN_ID 代表默认雪花算法。如果主键映射不对常见的表现是插入成功后主键没有回填或者在更新时候把主键当成普通字段处理。从 MyBatis 迁移到 MyBatis-Plus 时最容易出问题的就是 XML 中已有的 resultMap 和 MP 的注解机制混用。官方是支持两者共存的但你需要明确哪些 Mapper 是纯 MP 的走 BaseMapper 的 CRUD哪些是自定义 SQL走 XML 或注解。如果两者搅在一起映射规则重叠后可能覆盖掉另一方的行为查起来非常费劲。8. 架构层面的映射设计建议8.1 多数据源场景下的映射隔离热词提到了“saveorupdatebatch多数据源的问题”映射维度上多数据源的坑主要在于不同数据源的方言和映射规则可能不同。比如一个数据源是 MySQL另一个是 PostgreSQL时间类型、布尔类型、JSON 类型的映射行为都有差异。在 Spring Boot 中如果配置了多个 SqlSessionFactory每个数据源需要独立的mybatis.mapper-locations配置而且 mapper 接口要放在不同的包下通过MapperScan(basePackages ...)分别指定 SqlSessionTemplate。这样做的核心目的是让每个数据源有自己独立的 mapper 命名空间和缓存池避免混用导致 SQL 执行到错误的数据源。多数据源下还有一个常见问题事务跨数据源。默认情况下每个 DataSource 的事务是独立的无法通过单事务管理器同时控制。这时需要在 Service 层使用分布式事务方案比如 Seata、本地消息表等但这是数据一致性问题不在映射的范畴。这里只是想提醒一下把 SQL 写到正确的数据源再把事务边界设计合理是保证映射结果正确的基石。8.2 从映射视角审视数据库设计映射层面的很多怪异问题根源其实是数据库表设计不够规范。比如一张表的主键是联合主键实体类中就很难映射单个 id 字段resultMap 中也要为此做特殊处理比如一张表混用了多种命名风格部分字段下划线部分字段驼峰自动映射的配置就没法让人省心再比如状态字段是 int对应业务含义应该用枚举但为了图省事直接存数字或字符串前端展示时还得在代码里写一堆 if/else。我自己的态度是数据库设计在项目初期务必定好规范字段一律下划线命名主键一律单列自增或雪花状态、类型等离散取值字段用明确的编码规则多一张关联表就少一堆冗余字段。从源头设计上规避映射问题比写一万行 resultMap 都管用。实际项目里如果发现某个表生成的 resultMap 异常复杂先不要急着在 MyBatis 层绕回到数据库设计层面去分析往往几个字段重命名就能解决一大半映射问题。8.3 映射与代码生成器的平衡既然映射这么繁琐直接用代码生成器生成实体类和 XML能否一劳永逸我的答案是代码生成器是利器但要小心使用。它可以生成基础的实体类、Mapper 接口、XML 文件能保证命名规范和基础映射的准确性。但生成的代码有一个特点不适合手改因为它不是只读的重新生成时会被覆盖。如果你在生成的 resultMap 里手工添加了很多字段映射下次代码生成可能就全部覆盖掉了。所以我的实践是代码生成器只负责单表的基础 CRUD 和标准映射涉及复杂关联查询、乐观锁、逻辑删除、特殊 TypeHandler 的这些逻辑放在独立的手写 Mapper 或 XML 中不和生成的代码混在一个文件里。这样能避免“重新生成即丢失”的尴尬也能让代码生成的效率优势和人工定制的灵活性兼得。9. 最后交代一些个人的心得搞 MyBatis 映射这么多年踩过的坑真的不少。最开始我也喜欢用各种高级映射方式一对多嵌套、懒加载、二级缓存全都往项目里堆结果线上出了问题排查得非常痛苦。后来逐渐明白了一个道理MyBatis 的映射机制本质上是一层“数据库字段到 Java 对象”的桥桥越薄、越透明系统就越稳定。现在我的团队写 Mapper 时有几条铁律能自动映射就不写 resultMap必须写 resultMap 时列别名显式起好绝不依赖同名字段关联查询能拆就拆避免在数据库中一次性 join 几百行数据的复杂映射二级缓存默认全关缓存放 Redis所有简单 Mapper 用 MyBatis-Plus复杂 SQL 全部手写 XML每个自定义 resultMap 都必须有对应查询的列别名表确保换人维护时不会懵。最后再分享一个小技巧如果你发现一个 Mapper 方法怎么调都返回不了期望的结果先不要慌着改 XML在数据库客户端直接执行 MyBatis 打印出来的那条完整 SQL。如果 SQL 结果是对的问题一定在映射规则如果 SQL 结果本身就是错的那就从 SQL 和参数出发排查。用这个方法90% 的映射问题都能在半小时内定位。MyBatis 是个老牌的持久层框架资料多但分散希望这篇能把映射这块的系统认知补全到位。
返回列表