ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

动态SQL与MyBatis Generator:从标签解析到代码生成实战

动态SQL与MyBatis Generator:从标签解析到代码生成实战 1. 为什么我建议你把动态SQL当一门“小语言”来学很长一段时间里我对 MyBatis 的印象停留在“写 SQL 比 JDBC 舒服一点”这个层面。直到我接手一个老合同管理系统里面每个列表查询都手写了一堆重复的条件判断改一个字段要连着改三四个 XML我才意识到动态 SQL 不是花活而是 MyBatis 能不能用好的一道分水岭。今天想聊的就是两个被讨论最多也最容易用歪的东西——动态 SQL 和 MyBatis Generator。前者决定你写 Mapper 的能力上限后者决定你入职新项目后第一周是花在复制 CRUD 上还是花在真正理解业务上。先说动态 SQL。很多教程喜欢把if、where、foreach单独拎出来做个 demo你照着抄一遍感觉会了到了真实项目里又不知道该怎么组合。原因很简单动态 SQL 其实是一套小型模板语言它有变量、有判断、有循环、有复用片段。你要是只背标签不建立整套组合思维写出来的 SQL 依然会在第一个复杂查询面前崩掉。1.1 一个200行SQL的真实教训那个合同系统里的列表查询我印象很深。页面上一共有七个筛选条件合同编号、客户名称、签订日期起止、合同状态、经办人、金额上下限、是否包含作废。对应到 Mapper XML 里早期同事的做法是where 11打底后面跟十多个if。你能想象吗一个查询方法里同时堆了订单子查询、客户子查询、金额区间判断、日期格式化函数还有三套不同的排序逻辑整个 SQL 加起来超过 200 行。当时最痛苦的不是 SQL 长而是没人敢动它。你想加一个“仅查看自己负责的合同”条件必须先读懂里面哪一段是干什么的你想把一个相等判断改成IN判断可能牵扯到两处if共用变量你甚至不小心动了一个空格的顺序拼接出来的 SQL 就直接语法错误。后来我花了整整一个下午把这段 SQL 按业务语义拆成了四个sql片段用include引用再配合where和trim把条件按模块分组整个方法直接从 200 行降到 90 行而且后续加需求再也没出过拼接问题。这个经历给我最大的触动是动态 SQL 的核心不是“能拼”而是“拼得可维护”。条件多了以后谁能让 XML 像积木一样一块块拼装谁才是真正入了门。1.2 动态SQL能解决什么不能解决什么动态 SQL 能解决的是“同一套查询骨架在不同入参下产生不同 SQL”的问题。比如列表查询客户可能什么都不填直接点搜索也可能只填一个名称模糊搜索还可能带时间范围搜索。如果不用动态 SQL你就得写三个方法、三个 SQL或者用 Java 代码拼 SQL 字符串——前者冗余后者不仅容易注入还很难统一优化。MyBatis 的动态 SQL 让你在 XML 里做条件分支参数缺了就少拼一段参数全了就拼全逻辑清清楚楚。但它不能解决所有事情。第一它不能替代数据库设计关联表过多、索引缺失动态 SQL 写得再漂亮也是慢查询第二它不擅长处理极其复杂的动态表名、动态字段名这类需求硬要用${}去拼往往会把安全性拖垮第三它替代不了代码评审因为动态 SQL 写多了以后肉眼很难看出最终生成的 SQL 长什么样所以“能少拼就少拼、能拆就拆”反而成了最重要的原则。一句话总结动态 SQL 是让你在“条件不确定”的场景下保持 SQL 可读性的工具不是让你把所有逻辑都塞进 XML 的理由。2. 动态SQL核心标签拆解从if到foreach的完整实操我按实际项目里出现的频率把高频标签逐个过一遍。下面的例子尽量用真实的业务场景而不是教科书式的select * from user where id #{id}。2.1 if where别再用 where 11 硬扛if是最基础的判断标签语法很直白test里写判断表达式成立就拼接内部 SQL。但光有if不够因为多个条件组合时第一个成立的条件前面要加WHERE后面的加AND。老写法是where 11 and xxx能用但很丑而且让数据库的谓词分析变得不那么直观。更好的写法是where标签它会在内部有条件成立时自动插入WHERE并且会自动去掉第一个条件前缀的AND或OR。来看一个典型的条件查询select idlistContracts resultTypecom.example.entity.Contract SELECT * FROM contract where if testcontractNo ! null and contractNo ! AND contract_no #{contractNo} /if if testcustomerName ! null and customerName ! AND customer_name LIKE CONCAT(%, #{customerName}, %) /if if teststatusList ! null and statusList.size() 0 AND status IN foreach collectionstatusList itemst open( separator, close) #{st} /foreach /if if teststartDate ! null AND sign_date gt; #{startDate} /if if testendDate ! null AND sign_date lt; #{endDate} /if /where /select这里有几个细节要提醒你。where虽然能去掉开头多余的AND但它是靠“匹配”实现的如果你的第一个if里面写了括号嵌套或者其他奇怪的前缀偶尔会判断不准确。所以我的习惯是即使有whereif内部也尽量用AND开头不要交叉混用OR。另外判断空字符串要记得把“空格”也考虑进去if testcustomerName ! null and customerName ! 里的空串检查对表单默认值很有用。如果你用了 MyBatis 3.5 以上的版本还可以直接用testcustomerName ! null and customerName.trim() ! 但会影响一点性能量大的话尽量在 Java 层就处理掉。2.2 choose/when/otherwise处理多选一的业务分支if是“每个条件独立判断”谁成立谁拼。但很多业务是“排他”的比如列表页的排序规则可能是按时间、按金额、按状态三选一而不是同时生效。这时候如果你用多个if就会拼出多个ORDER BY直接语法错误。正确的做法是choose它类似 Java 里的switchchoose when testsortType DATE ORDER BY sign_date DESC /when when testsortType AMOUNT ORDER BY amount DESC /when otherwise ORDER BY id DESC /otherwise /choosechoose从上到下匹配匹配到第一个成立的when后就不再看后面的了所以天然是互斥的。项目里我经常把它用在“筛选模式”上比如“全部 / 本月 / 自定义区间”这样的三个 tab前端只传一个queryType后端用一个choose就处理干净比在 Java 层 if-else 里拼 SQL 优雅得多。有个容易踩的坑when的test表达式里字符串比较一定要加引号注意外层单引号、内层双引号的写法。testsortType DATE是常见的正确形式写反了 MyBatis 会把DATE当成字符解析失败报错还比较隐晦。2.3 set trim让更新语句不再胆战心惊做UPDATE的时候动态更新是最刚需的场景。前端表单可能只改了备注也可能改了金额和状态你不能用一个写死全部字段的UPDATE把所有列都覆盖一遍——那样会把没传的字段更新成null。最稳妥的方案是用set标签update idupdateContract UPDATE contract set if testamount ! null amount #{amount}, /if if teststatus ! null status #{status}, /if if testremark ! null and remark ! remark #{remark}, /if if testupdateTime ! null update_time #{updateTime}, /if /set WHERE id #{id} /updateset会自动在内部有条件成立时插入SET并且会去掉最后一个多余的逗号。这是它最友好的地方因为新手手写时经常会留下一个尾逗号数据库直接报错。但你要注意两个问题。第一如果所有if都不成立最终生成的 SQL 是UPDATE contract WHERE id ?这在 MySQL 里会直接报语法错误。所以务必在 Java 层或 SQL 层兜底要么保证至少一个字段有值要么先查一次再决定是否更新。第二set不会管字段本身的业务约束比如某个列在数据库里是NOT NULL但你表单里没传这个字段就不会出现在 SQL 里数据库不会报错但业务上可能不符合预期。所以动态更新适合“允许部分字段更新”的场景如果业务要求“必须更新完整”那应该用固定的UPDATE。trim是比set更底层的通用方案。它的四个属性prefix、suffix、prefixOverrides、suffixOverrides可以干很多事。比如手动实现一个settrim prefixSET suffixOverrides, if testamount ! null amount #{amount}, /if if teststatus ! null status #{status}, /if /trimtrim的语义是如果内部有内容就在整体前面加prefix整体后面加suffixprefixOverrides会去掉内容开头匹配的字符串suffixOverrides会去掉内容结尾匹配的字符串。理解了这四个属性你就能写出各种自定义拼接规则比如动态GROUP BY、动态ORDER BY组合。2.4 foreach批量操作与 IN 列表的正确姿势foreach用得非常频繁最常见的两个场景是IN查询和批量插入。先看IN查询select idlistByIds resultTypecom.example.entity.Contract SELECT * FROM contract WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /select这里的collection取值要注意如果 Mapper 方法只有一个参数且参数是List默认名字是list或collection如果参数是数组默认是array。如果不想记这些默认名最稳妥的是在 Mapper 接口上用Param(ids) ListInteger ids明确指定名字。我几乎每个集合参数都会加Param因为默认命名规则在不同版本、不同场景下容易记混一旦写错就直接报There is no getter for property named xxx排查起来很烦。批量插入是foreach的另一个高频场景insert idbatchInsert INSERT INTO contract (contract_no, customer_name, amount, status) VALUES foreach collectionlist itemitem separator, (#{item.contractNo}, #{item.customerName}, #{item.amount}, #{item.status}) /foreach /insert这里有几个性能和安全细节。MySQL 默认单条 SQL 有max_allowed_packet限制一般默认 4MB 或 64MB。批量插入的数据量太大时整个 SQL 字符串可能超过限制数据库直接拒绝。我实践下来的分片经验是每批 500 到 1000 条比较稳妥。如果你用的连接池是 Druid还要注意maxStatementLength之类的参数必要时在 JDBC URL 上把rewriteBatchedStatementstrue打开虽然它影响的是addBatch模式但配合批量插入能显著减少网络往返。网上很多人问“java mybatis mybatis-plus 批量怎么做”其实 MyBatis 底层就靠这个foreachMyBatis-Plus 的saveBatch也只是帮你把这个循环生成了而已。另外注意foreach也可以配合动态拼接做“批量更新”比如UPDATE ... SET field CASE id WHEN ...但那样的 SQL 可读性很差我一般建议拆成多条更新放在事务里执行不要非把复杂逻辑压成一条 SQL。2.5 bind sql/include复用是动态SQL的隐藏价值bind是个容易被忽略但很实用的小标签。典型场景是模糊查询MySQL 可以CONCAT(%, #{name}, %)但如果你要兼容 Oracle或者你在 PostgreSQL 里用||拼接不同数据库语法不一样。用bind可以把参数处理统一放到 XML 里select idlistByName resultTypecom.example.entity.Contract bind namepattern value% customerName %/ SELECT * FROM contract WHERE customer_name LIKE #{pattern} /selectbind的value里写的是一个 OGNL 表达式它会在执行前把计算结果绑定到pattern变量上。这样做的好处是Mapper 接口入参不需要额外处理所有拼接逻辑留在 XML后续切换数据库方言时只需要改这一处。sql和include是复用片段的重要手段。前面说的那个 200 行 SQL拆解后可以抽象出公共片段sql idcontractColumns id, contract_no, customer_name, amount, status, sign_date /sql sql idbaseWhere where if testcustomerName ! null and customerName ! AND customer_name LIKE CONCAT(%, #{customerName}, %) /if if teststatus ! null AND status #{status} /if /where /sql然后不同查询方法里这样引用select idlistContracts resultTypecom.example.entity.Contract SELECT include refidcontractColumns/ FROM contract include refidbaseWhere/ /selectinclude还可以配合property给片段传变量。比如你想让同一个baseWhere在不同方法里作用到不同表可以写成include refidbaseWhereproperty nametableAlias valuec//include然后在片段里用${tableAlias}.status #{status}。但这里要用${}注意别把这部分暴露给外部输入否则会有注入风险。我的建议是sql片段适合复用字段列表、公共条件、排序规则但不适合过度抽象因为片段一旦嵌套多层XML 可读性会急剧下降到时候维护的成本比复制粘贴还高。2.6 每个标签背后的OGNL表达式你说if testcustomerName ! null and customerName ! 里的and、!、null这些是哪来的答案是 OGNL一个 Java 里的表达式语言。MyBatis 动态 SQL 的test、bind的value都会交给 OGNL 求值所以它的语法基本和 Java 表达式类似、!、、||、!、方法调用list.size()、三目运算等都能用。明白这一点后很多坑就能解释了。比如你写if teststatus 1当参数status是Integer时没问题但如果参数是String你得写1。比如你用list.size() 0判断集合非空这是 OGNL 在调用 List 的方法如果你直接写list ! null and list ! 对集合来说的比较可能不生效。再比如某些版本下testids.size() 1里的在 XML 里最好写成gt;否则 XML 解析器会报错。我自己写动态 SQL 时test表达式遵守三个原则第一能用简单的! null判断就不用复杂表达式降低出错面第二文本值统一加引号数字值不加保持和 Java 字面量一致第三集合判空写成xxx ! null and xxx.size() 0不写成xxx ! null and xxx ! 。这些规则你在面试时随口说出来通常会让面试官觉得你是踩过坑的。3. 动态SQL的底层逻辑从参数绑定到BoundSql不少人写完动态 SQL 能跑但一旦报错就慌了因为不清楚 MyBatis 到底是怎么把 XML 变成数据库能执行的 SQL 的。这一节我从参数和执行链路两个角度拆一下理解了这两块很多诡异的报错都能迎刃而解。3.1 #{} 和 ${} 的区别与 param index 的关系#{}最终会被 MyBatis 替换成?预处理参数占位符然后通过 JDBC 的PreparedStatement设置参数。这样做的好处有两个一是防止 SQL 注入二是让数据库可以复用执行计划。而${}是直接做字符串替换把变量内容原样拼进 SQL 字符串所以它天生有注入风险也容易因为引号问题出语法错误。我在网上看热词里一直有人搜“mybatis param index”其实就是#{}里的参数引用机制。默认情况下如果你的 Mapper 方法只有一个参数MyBatis 会把这个参数包装成map可以用_parameter来引用它如果有多个参数又不加ParamMyBatis 会生成param1、param2这样的名字。用Param(xxx)后XML 里就可以直接用xxx了。所以你会发现很多老代码里写的是#{param1}、#{param2}这就是没加注解时的默认行为。在动态 SQL 里test表达式引用参数时遵循同样的规则。假设方法签名是ListContract list(Param(query) ContractQuery query)那么 XML 里写if testquery.customerName ! null#{}里写#{query.customerName}。如果你用了_parameter这种默认名可读性会很差所以再次建议多参数的 Mapper 方法一定显式加Param。至于什么时候能用${}我的底线是只用于非用户控制的静态片段比如sql片段里的表别名、固定的排序列名或者在生成器里用来拼接数据库分页方言。凡是可能被外部输入影响的值一律走#{}。别为了省事把用户输入的排序字段直接${sortField}拼进去那是典型的注入入口。3.2 一条动态SQL的执行链路在 MyBatis 里XML 中的select节点会被解析成一个MappedStatement它内部维护的 SQL 信息是一个SqlSource。如果这个 SQL 包含了动态标签MyBatis 会把它包成DynamicSqlSource如果不含动态标签直接用RawSqlSource。执行的时候DynamicSqlSource.getBoundSql(parameterObject)会把参数对象传入通过 XMLScriptBuilder 递归解析各种动态节点。这个过程有点像模板引擎渲染遇到if就调用 OGNL 判断test表达式成立就把子节点内容拼进去遇到foreach就遍历集合按open、separator、close生成列表遇到sql就去查找对应的SqlNode解析。最后拼出一个BoundSql里面包含了完整的 SQL 字符串和参数映射关系。理解这条链路以后有几个排查问题的思路就很清楚了。比如你改了 XML 但没生效多半是没重新编译或 MyBatis 缓存了 metaObject比如你发现生成的 SQL 里AND多了或者少了问题一定出在动态节点的prefixOverrides或where的处理逻辑上。我之前排查过一个莫名其妙多出一个WHERE的问题最后发现是sql片段里已经写了where外层又套了一个where两段逻辑叠加把 SQL 结构搞乱了。这种问题如果不理解拼接顺序光盯着报错看是找不出答案的。3.3 动态SQL的性能与缓存边界动态 SQL 每次拼接时都要走一遍 OGNL 求值和 SQL 字符串拼接所以相比固定 SQL 会有一定的 CPU 开销但这个开销通常很小不至于成为瓶颈。真正要关注的是“生成的 SQL 是否还走得上索引”。比如你对一个可空字段写if teststatus ! null AND status #{status}/if条件是可选时没问题但如果很多条件都是可选用户却一个都不选SQL 就变成SELECT * FROM contract全表扫描是必然的。所以动态 SQL 写不写是一回事查询计划能不能用好是另一回事配合EXPLAIN检查每个分支的索引命中情况是我每次上线前都会做的事。至于缓存很多人在搜“mybatis缓存”“mybatis二级缓存实现”。我的经验是动态 SQL 和缓存的交集很容易被误解。一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 内执行相同 SQL 会命中缓存二级缓存是 Mapper 级别的需要手动开启。如果你给某个 Mapper 开了二级缓存而它内部又有很多动态 SQL缓存命中率会因为 SQL 字符串不同而大幅下降因为动态条件一变SQL 就不是同一个了。所以实务里我只建议对基础字典表、静态配置表开二级缓存复杂的列表查询宁可走数据库或加 Redis 缓存也别指望 MyBatis 二级缓存兜底。面试时如果你能把“动态 SQL 导致缓存 key 差异大”这个点说出来比单纯背一级、二级缓存的区别有说服力得多。4. MyBatis Generator从零配置到生成代码落地动态 SQL 本身能让你把 Mapper 写明白而 MyBatis GeneratorMBG解决的是另一件事项目里的基础 CRUD 代码谁来写。我见过很多团队还在手动复制粘贴selectByPrimaryKey、updateByPrimaryKeySelective写十张表就开始烦躁写五十张表就开始出 bug。MBG 是 MyBatis 官方提供的代码生成器连表结构都不用手敲直接生成实体类、Mapper 接口和 XML能省下大量机械劳动。4.1 为什么你值得花半天把 MBG 用起来先摆几个我真实的感受。第一手动写 CRUD 的出错率比想象中高字段漏了、类型映射错了、XML 里 resultMap 的column和实体属性对不上这些错误在编译期根本发现不了只能运行时报错。MBG 从数据库表反推代码字段名、类型、主键规则全部以元数据为准生成结果一致性极高。第二生成器让你把规范固化下来比如所有实体类继承同一个基类所有字段都加注释所有 Mapper 都遵循同样的命名风格这些一致性靠人肉保证很难但生成器天然就可以。第三也是最重要的MBG 生成的代码是“可丢弃”的。这句话怎么理解它不像某些代码生成工具那样生成一堆没法改的代码恰恰相反MBG 标准做法是生成的基础 CRUD 不手动修改后续要改生成的代码就重新生成覆盖你的业务查询另外写在扩展接口或扩展 XML 里。这样每次数据库变更你重新跑一遍生成器基础层自动跟着变手写部分不会受影响。这个工作流一旦跑起来维护成本会直线下降。4.2 generatorConfig.xml 关键节点逐个拆解MBG 的入口是一个generatorConfig.xml第一次配置会觉得节点很多其实核心就五个部分jdbcConnection、javaModelGenerator、sqlMapGenerator、javaClientGenerator、table。下面是完整的最小配置?xml version1.0 encodingUTF-8? !DOCTYPE generatorConfiguration PUBLIC -//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd generatorConfiguration context idmysqlContext targetRuntimeMyBatis3Simple defaultModelTypeflat jdbcConnection driverClasscom.mysql.cj.jdbc.Driver connectionURLjdbc:mysql://localhost:3306/contract_db?useUnicodetrueamp;characterEncodingutf8 userIdroot password123456 /jdbcConnection javaTypeResolver property nameforceBigDecimals valuefalse/ /javaTypeResolver javaModelGenerator targetPackagecom.example.entity targetProjectsrc/main/java property nameenableSubPackages valuetrue/ property nametrimStrings valuetrue/ property nameimmutable valuefalse/ /javaModelGenerator sqlMapGenerator targetPackagemappers targetProjectsrc/main/resources property nameenableSubPackages valuetrue/ /sqlMapGenerator javaClientGenerator typeXMLMAPPER targetPackagecom.example.mapper targetProjectsrc/main/java property nameenableSubPackages valuetrue/ /javaClientGenerator table tableNamecontract domainObjectNameContract enableCountByExamplefalse enableDeleteByExamplefalse enableUpdateByExamplefalse enableSelectByExamplefalse/ /context /generatorConfiguration逐个讲关键配置。第一targetRuntime我推荐根据团队情况二选一MyBatis3Simple生成的代码最简单只包含单表主键 CRUD适合你打算自己写复杂查询的情况MyBatis3会额外生成大量XXXExample类能把单表查询做成“动态条件模板”但代码量巨大类数量直接翻倍。我个人的倾向是MyBatis3Simple因为它生成的代码容易读懂Example 类那种设计对于多人协作来说学习成本偏高。第二defaultModelTypeflat非常重要。如果不设置或设置成默认的conditionalMBG 可能会根据表的主键结构生成主键类、BLOB 类等多个类导致实体类数量膨胀。flat模式下每张表只生成一个实体类简单直接绝大多数业务场景都够用。第三javaModelGenerator里的trimStrings属性建议设为true这样生成实体的 setter 会自动 trim 掉字段值两端的空格减少表单数据脏值入库的概率。第四table节点按需配置。如果只填tableName生成器会使用数据库表名反推实体类名如果你有表名前缀比如t_contract想生成Contract可以加property namedomainObjectName valueContract/这里也可以直接用上面示例里的domainObjectName属性。还有更细的列映射比如某列的generatedKeytrue表示数据库自增主键某个字段不想生成可以用ignoreColumn排除。表特别多的时候可以用table tableName%通配所有表再配合过滤规则来批量生成。4.3 三种运行方式命令行、Maven插件、Java代码MBG 的运行方式有几种我按实际使用频率从低到高说。命令行方式下载mybatis-generator-core的 jar 包执行java -jar mybatis-generator-core-1.4.2.jar -configfile generatorConfig.xml这种方式适合临时跑一次缺点是要手动下载 jar、手动维护版本。Maven 插件方式是我最推荐、也是项目里最常用的。在pom.xml里加插件plugin groupIdorg.mybatis.generator/groupId artifactIdmybatis-generator-maven-plugin/artifactId version1.4.2/version configuration configurationFilesrc/main/resources/generatorConfig.xml/configurationFile overwritetrue/overwrite verbosetrue/verbose /configuration dependencies dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies /plugin然后执行业务模块下的 Maven 命令mvn mybatis-generator:generate注意overwrite设为true时MBG 会覆盖同名文件。这个设计配合“生成代码不手工修改”的策略非常安全因为基础 CRUD 永远不会被业务改动污染。第三种是 Java 代码方式适合写进自动化构建脚本或者 IDE 工具里。核心代码只有几行ListString warnings new ArrayList(); Configuration config new ConfigurationParser(warnings).parseConfiguration( new File(src/main/resources/generatorConfig.xml)); ShellCallback callback new DefaultShellCallback(true); MyBatisGenerator generator new MyBatisGenerator(config, callback, warnings); generator.generate(null);实际项目里我见过有人把这段代码写成一个单元测试数据库 schema 变更后直接跑一下测试就重新生成了代码配合 CI 里的定时任务也算一种团队基建。不过大多数时候 Maven 插件已经够用。4.4 生成的代码长什么样以MyBatis3Simple为例表contract会生成三个文件Contract.java实体类字段对应表的列包含 getter/setter如果开启了toString等选项还会生成对应方法。ContractMapper.javaMapper 接口默认包含deleteByPrimaryKey、insert、selectByPrimaryKey、updateByPrimaryKey、updateByPrimaryKeySelective等几个方法。ContractMapper.xmlMapper XML包含对应的动态 SQL 和固定的resultMap。生成的 XML 会自动带一个BaseResultMap它会将表的每一列映射到实体字段resultMap idBaseResultMap typecom.example.entity.Contract id columnid propertyid jdbcTypeINTEGER/ result columncontract_no propertycontractNo jdbcTypeVARCHAR/ result columnamount propertyamount jdbcTypeDECIMAL/ ... /resultMap这个resultMap是基础层和手写层共用的核心资产。你在扩展查询 SQL 时直接select idlistByCustomer resultMapBaseResultMap就可以了不需要每个查询都重新定义一遍字段映射。另外MBG 会根据列的 JDBC 类型为#{}自动生成jdbcType。这块有个容易遇到的问题会在后面的常见问题里专门讲比如 Oracle 的DATE类型映射到 Java 的Date时时间精度是否会丢。5. Generator 进阶改造与生产经验基础用法学会后真正决定 MBG 好不好用的是后续的改造和工程管理。这一节我集中写生产里会遇到的硬核细节。5.1 自定义注释生成器别再让生成的代码带着丑注释MBG 默认生成的文件顶部会有“This class was generated by MyBatis Generator...”这种注释看起来很丑而且如果你重新生成文件注释还会说“do not modify”。想要生成干净、带上自己团队风格注释的代码最靠谱的做法是实现一个注释生成器插件。核心是利用 MBG 的插件扩展点重写addModelClassComment、addMapperComment、addComment等方法。下面这段代码是我项目里精简约好的版本public class CustomCommentGenerator extends DefaultCommentGenerator { Override public void addModelClassComment(XmlElement xmlElement, IntrospectedTable introspectedTable) { // 不生成默认注释 } Override public void addComment(XmlElement xmlElement) { // 跳过 XML 节点的默认注释 } Override public void addConfigurationProperties(Properties properties) { super.addConfigurationProperties(properties); } }然后在generatorConfig.xml的context里通过plugin注册plugin typecom.example.generator.CustomCommentGenerator /你还可以根据自己的需要给实体类字段加上业务说明。比如希望在实体类每个字段上生成“数据库字段的注释”可以在addFieldComment方法里从IntrospectedColumn.getRemarks()拿数据库注释拼到 Javadoc 上。这样生成的代码等于自带文档团队成员看实体类就能明白每个字段的含义。还有个小技巧你可以在插件里统一给实体类加上Data注解如果你项目用 Lombok加EqualsAndHashCode(callSuper true)等类注解这样生成出来的实体类更贴合团队规范。但注意生成代码被反复覆盖后Lombok 注解也会被重新生成所以插件逻辑必须稳定否则每次生成完都要手动改。5.2 生成代码与手写SQL如何分层管理这是 MBG 使用中最核心的工程问题。我的方案是生成层和业务层分离物理上就分开。具体做法是MBG 生成的ContractMapper.java只负责基础 CRUD你不允许在它上面加任何自定义方法。业务查询、复杂动态 SQL 全部写在另一个扩展接口里比如ContractExtMapper.java配套的 XML 是ContractExtMapper.xml。然后通过 Spring 装配或手动注入在 Service 层同时注入两个 MapperAutowired private ContractMapper contractMapper; Autowired private ContractExtMapper contractExtMapper;这样做的好处非常明显。第一数据库表结构变了你重新跑 MBGContractMapper和Contract.java被覆盖但ContractExtMapper完全不受影响手写 SQL 的稳定性有保障。第二基础 CRUD 和维护代码分开后代码评审和排查问题都能快速定位主键查询、根据主键更新这种通用操作走ContractMapper业务查询找ContractExtMapper。如果你不想多建接口也可以采用另一种做法MBG 生成所有基础方法然后你用sql片段给 XML 追加自定义查询。这时要注意mapper的 namespace 一定不能动XML 文件也尽量保持和ContractMapper.java同一个命名空间下。但我的经验是这样做有隐患因为overwritetrue时MBG 会直接覆盖整个 XML 文件你手动追加的查询会被冲掉。所以我强烈建议走“扩展接口”这条路线从物理上隔离生成代码和手写代码。5.3 常见问题速查表我整理了生产环境里遇到频率最高的几个问题直接给出解决方案问题现象原因解决办法生成的实体字段少了一个表结构未刷新或列被ignoreColumn排除了检查generatorConfig.xml的table配置确认没有把列排除重新生成前先刷新数据库连接生成时间类型是Date但数据库是datetime查询结果少了时分秒JDBC 驱动返回的类型映射问题检查实体字段类型是否用了java.time.LocalDateTime必要时在javaTypeResolver里配置自定义类型映射重新生成后手写 XML 被覆盖overwritetrue覆盖了整个文件把自定义 SQL 放到独立的扩展 Mapper 文件或者调整overwritefalse并手动合并不推荐Maven 插件生成时报“does not exist”MBG 插件找不到数据库驱动在插件dependencies里显式添加数据库驱动依赖生成的 XML 里#{}多了jdbcTypeOTHER某些数据库类型无法推断在table里用columnOverride columnxxx jdbcTypeVARCHAR/指定类型生成时提示主键为 null表没有主键或主键配置错误MBG 强依赖主键信息生成selectByPrimaryKey表无主键时建议加上逻辑主键Oracle 查询时间字段映射不对Oracle 的DATE同时包含日期和时间驱动映射逻辑不同使用rs.setFetchSize相关配置或在实体类型上做JsonFormat、类型处理器处理动态 SQL 执行时拼接多了AND第一个if前面有多余的空格或换行把AND写在if内部并尽量在where内保持同一行风格还有一个经常被问到的“mybatis配置打印”怎么搞。这个和动态 SQL、生成器都相关因为排查 SQL 问题最需要的就是看到实际执行的 SQL 长什么样。在 Spring Boot 项目里最简单的方式是在application.yml配置mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl或者针对某个 Mapper 单独开日志等级logging: level: com.example.mapper: debug这样你就能在控制台看到动态 SQL 拼接后的完整语句和参数绑定列表。我排查动态 SQL 问题时第一步永远是打开 SQL 日志第二步才是看 XML。生成的 SQL 和预想不一致时日志里的 Preparing:后面那段就是最直接的证据。5.4 面试关联动态SQL和Generator常被问到的点结合这几年看到的各种“mybatis面试题”整理我觉得和本文主题最相关的几个高频问题可以这样答。为什么#{}能防注入因为它是预处理占位符参数通过 JDBCsetXXX绑定和 SQL 语句结构完全分离用户输入再“坏”也只能当数据不会成为 SQL 结构的一部分。动态 SQL 常用标签有哪些底层怎么实现的最常用的是if、where、set、foreach、choose、bind、sql/include。底层就是 XML 解析成 SqlNode 树运行时根据参数递归拼接 SQL最终生成 BoundSql交给 Executor 执行。你把DynamicSqlSource、BoundSql这条链路说出来深度就够了。where 11有什么问题为什么推荐用wherewhere 11虽然能避免没有条件时语法报错但语义丑陋、数据库优化器对常量条件的处理也没必要而且第一个条件前面必须写 AND拼接风格非常容易出错。where自动管理 WHERE 和 AND 前缀。用过 MyBatis Generator 吗谈谈避免手写重复 CRUD 的思路用 MBG 生成基础 CRUD生成代码不手改业务查询写在扩展 Mapper 里数据库变更后重新生成基础层手写部分隔离。这里可以顺带讲一下自定义注释、Lombok 插件、批量生成策略都是加分项。还有一个我特别喜欢问自己的问题如果表字段很多更新逻辑要求“只更新非空字段”你怎么设计 SQL这个就是updateByPrimaryKeySelective的典型场景本质就是set加多个if。把生成器生成的方法名和动态 SQL 标签结合起来讲会让面试官觉得你是真在项目里干过而不是只会背概念。最后想补一句关于“mybatis xml高亮”的题外话很多前端项目里展示 Mapper XML 时没有语法高亮排查效率很低。如果你用的是 IntelliJ IDEA装好 MyBatis 相关插件后XML 里动态标签会有高亮和折叠Maven 构建时也能提示 XML 语法错误。这个属于开发体验的一部分但往往被低估了。写在最后我自己的经验是动态 SQL 用得好不好本质上是“你能不能预判最终生成的 SQL 长什么样”。写每段if的时候大脑里模拟一遍传入参数后拼出的完整 SQL能显著降低低级错误。MBG 则秉持一个“生成即基础基础即勿改”的纪律宁可多花半天做注释生成器和扩展 Mapper 的骨架也不要图省事直接在生成文件里手写业务逻辑否则下一次数据库变更时你会同时失去生成代码和手写代码。如果你正在搭一个新项目我建议你从第一张表开始就把generatorConfig.xml配好哪怕项目很小也值得这样做。等到表数量超过十张再回头补你会发现自己已经在重复 CRUD 上浪费了两个完整的下午。真的这个工具半天就能上手但它每年能帮你省下的时间远远不止半天。
返回列表