首页 / 资讯中心 / 文章详情

若依3.8.7持久层改造:MyBatis-Plus与代码生成器迁移

若依3.8.7持久层改造:MyBatis-Plus与代码生成器迁移 ★ FEATURED ARTICLE
若依 3.8.7 前后端分离版这套底子我前后接手过好几个基于它二次开发的项目前端 Vue 那套基本没人抱怨真正让人难受的是持久层单表 CRUD 全靠手写 XML一个十几张表的业务模块光 Mapper 文件就能堆出半屏字段一改就得同步改 XML、改实体、改 Service漏一处就 500。这次我把整套持久层换成 MyBatis-Plus顺手把自带的 Velocity 代码生成器一起改成 Plus 版本让新生成的代码天生带 Wrapper、分页和自动填充。这篇就按我实际动手的顺序讲依赖怎么换、配置怎么迁、分页和审计字段怎么接管、生成器模板怎么一段一段改以及我踩过的那些坑。适合手上正压着一个若依二开项目、想上 MP 又怕改崩的同行也适合刚接触这套脚手架、想知道它持久层长什么样的朋友。1. 这套改造到底在改什么先看清骨架再动手动框架之前我习惯先把要改的东西画清楚不然很容易改着改着发现牵一发动全身。若依 3.8.7 的持久层不是简单地用了 MyBatis而是围绕 MyBatis 做了一整套封装包括 SqlSessionFactory 的手工装配、PageHelper 分页、基于 Velocity 的代码生成模板。这三块是咬合在一起的只换其中一块另外两块大概率会给你脸色看。1.1 若依 3.8.7 持久层的四层结构拆解我把它拆成四层来看。最底层是mybatis-spring-boot-starter提供 SqlSessionFactory 和 Mapper 扫描往上一层是MyBatisConfig配置类手工构建SqlSessionFactoryBean从application.yml里读typeAliasesPackage、mapperLocations、configLocation三个参数塞进去再往上是com.ruoyi.common.core.mapper.BaseMapper这个占位接口所有业务 Mapper 都继承它它本身几乎是个空壳只挂了Mapper语义最上面是代码生成器用 Velocity 模板按数据库表结构生成 domain、mapper、service、serviceImpl、controller 五件套加一份菜单 SQL。理解这个结构之后改造路径就清楚了把 starter 换掉、把MyBatisConfig里的工厂类型换掉、把占位 BaseMapper 升级成 MP 的 BaseMapper、把模板文件里的注解和继承关系换掉。四步对应四个层次一层一层来中间任何一层没对齐下一层就会报错。这里有个容易被忽略的点若依在common模块里放的那个BaseMapper是个绝佳的改造锚点。因为所有业务 Mapper 都继承了它你只要让它extends com.baomidou.mybatisplus.core.mapper.BaseMapperT全项目所有 Mapper 瞬间获得 MP 的selectPage、selectList(Wrapper)能力业务代码一行不用改。这个技巧比逐个改业务 Mapper 的继承关系要省事得多也不容易漏。1.2 三条改造路线的成本对比我评估过三种走法最后选了第三种这里把账算给你看。路线具体做法改造量遗留风险我的评价路线一并存保留原生 MyBatis只在新模块引入 MP极小两套 SqlSessionFactory 争抢数据源配置事务边界模糊不推荐后期排查成本极高路线二渐进替换老模块保留 XML新模块用 MP中等pagehelper 与 MP 分页插件同时存在startPage()后紧跟的 MP 查询会被二次分页可接受但必须严格约定一个方法里只用一种分页路线三整体替换移除 pagehelper分页统一走 MP较大集中在 controller 层老代码的分页调用要逐处改推荐改完一次痛苦之后一直舒服选路线三的核心理由是分页冲突。PageHelper 的原理是在Executor层挂 InterceptorstartPage()把分页参数丢进 ThreadLocal紧接着执行的第一个 SQL 被改写并自动取总数。MP 的PaginationInnerInterceptor也是干这个的而且它还有一个分页参数上下文机制。两个插件同时挂着你调startPage()之后再执行baseMapper.selectPage(...)前者会拦截后者生成的 SQL 再加一次LIMIT结果就是LIMIT ?,? LIMIT ?,?这种明显不对的语句或者总数对不上。这个坑我在过渡期踩过一次排查了半天最后发现是插件双注册。所以我的建议很明确如果你决定上 MP就一次性把 pagehelper 依赖摘掉分页全部迁到 MP。改造量看起来大但若依的分页调用其实高度集中——每个list接口开头一行startPage()结尾一行getDataTable(list)全项目扫一遍大概几十处半天能改完。1.3 版本边界与不能动的地方版本要对齐不然依赖冲突会让你怀疑人生。我这边实测可用的组合是JDK 8、Spring Boot 2.5.x若依 3.8.7 自带、mybatis-plus-boot-starter3.5.3.2。选 3.5.3.x 而不是最新的 3.5.5是因为 3.5.4 之后对mybatis-spring的版本要求抬到了 2.1.x和 Spring Boot 2.5 默认带的 2.0.x 会对不上强行升级可能波及 Spring 的事务代理。3.5.3.2 依赖mybatis-spring 2.0.7和 Spring Boot 2.5 的组合是稳的。有几处我建议你不要动application.yml里的mapperLocations路径结构保持原样业务 XML 文件不用挪位置MapperScan的扫描范围保持原样mybatis-config.xml文件保留MP 用的也是 MyBatis 的原生配置文件格式里面的useGeneratedKeys、callSettersOnNulls这些 setting 依然有效。改的东西越少出问题时回溯越容易。2. 依赖替换与全局配置迁移这一层是地基改完必须能正常启动、正常连库否则后面全是空谈。我的习惯是改完立刻写一个最简单的selectById单测跑通确认工厂装配没问题再往下走。2.1 pom 里到底要删什么、加什么先看父 pom 的dependencyManagement找到 mybatis 相关的那一段把 starter 换掉!-- 原来的 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version${mybatis.version}/version /dependency !-- 换成 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency在ruoyi-common或者ruoyi-framework模块里把pagehelper-spring-boot-starter的依赖删掉或者注释掉。这一步别心软留着它就会在启动时自动注册PageInterceptor和 MP 的分页插件打架。删除之后一定会编译报错因为BaseController.getDataTable()里用到了com.github.pagehelper.PageInfoPageUtils.startPage()也引用了 PageHelper 的类。这两处我先不急着改留着让编译器帮我定位所有依赖点——编译器报错的位置就是你要改造的全部位置比人工搜索靠谱。有一个冲突要特别注意如果你的项目里还有其他 starter 间接引入了mybatis-spring-boot-starter比如某些数据源组件、某些分页组件启动时会出现两个 SqlSessionFactory或者Mapper 重复注册的报错。用mvn dependency:tree -Dincludesorg.mybatis*查一下把间接依赖排除掉exclusions exclusion groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId /exclusion /exclusions2.2 application.yml 配置项逐条对照若依原来的配置在mybatis:前缀下MP 换成了mybatis-plus:。注意不是所有配置项都能直接平移有些键名变了。我把对照关系整理成表格你照着改就行。原配置项新配置项说明mybatis.typeAliasesPackagemybatis-plus.typeAliasesPackage通配符**依然支持不用改值mybatis.mapperLocationsmybatis-plus.mapperLocations路径保持classpath*:mapper/**/*Mapper.xmlmybatis.configLocationmybatis-plus.configLocation指向mybatis/mybatis-config.xmlmybatis.configuration.*mybatis-plus.configuration.*驼峰映射、缓存开关等无mybatis-plus.global-config.db-config.id-type新增主键策略无mybatis-plus.global-config.banner新增关掉启动横幅改完大概是这个样子mybatis-plus: typeAliasesPackage: com.ruoyi.**.domain mapperLocations: classpath*:mapper/**/*Mapper.xml configLocation: classpath:mybatis/mybatis-config.xml global-config: banner: false db-config: id-type: auto table-underline: true configuration: map-underscore-to-camel-case: true cache-enabled: false call-setters-on-nulls: trueid-type: auto这个必须设。MP 默认主键策略是ASSIGN_ID雪花算法而若依的建表脚本绝大多数主键是bigint auto_increment。不设这一项你插入数据时会发现主键变成了一串十九位的长数字数据库自增序列完全没用上而且前端 JS 处理这么大整数还会丢精度。这个问题极其隐蔽——插入不报错查询不报错直到某天发现在前端搜不到某条记录才炸出来。日志级别也顺手加一下方便看 SQLlogging: level: com.ruoyi: debug mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl2.3 MyBatisConfig 的工厂类型替换这是整个改造里最关键的一处代码。原来的MyBatisConfig用的是SqlSessionFactoryBean必须换成MybatisSqlSessionFactoryBean。这两者名字只差几个字但后者是 MP 的核心——MP 的GlobalConfig、SqlInjector、各种 InnerInterceptor 都挂在它上面用错类型插件全都不生效。Configuration public class MyBatisConfig { Autowired private Environment env; Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 分页插件必须指定数据库类型否则分页方言判断不出来 PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(1000L); // 单页最大条数防止前端传个超大值拖垮库 pagination.setOverflow(false); // 页码越界时不回到首页直接返回空 interceptor.addInnerInterceptor(pagination); // 防全表更新与删除放在最后 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); String typeAliasesPackage env.getProperty(mybatis-plus.typeAliasesPackage); String mapperLocations env.getProperty(mybatis-plus.mapperLocations); String configLocation env.getProperty(mybatis-plus.configLocation); if (StringUtils.isNotEmpty(typeAliasesPackage)) { factory.setTypeAliasesPackage(typeAliasesPackage); } if (StringUtils.isNotEmpty(mapperLocations)) { factory.setMapperLocations( new PathMatchingResourcePatternResolver().getResources(mapperLocations)); } if (StringUtils.isNotEmpty(configLocation)) { factory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); } GlobalConfig globalConfig new GlobalConfig(); globalConfig.setBanner(false); factory.setGlobalConfig(globalConfig); factory.setPlugins(mybatisPlusInterceptor()); return factory.getObject(); } }三个细节值得单独说。第一setPlugins()和setGlobalConfig()必须在getObject()之前调用getObject()一旦执行工厂就构建完了之后再 set 什么都是无效的。我见过有人把 set 写在 return 之后的位置编译没问题运行不报错就是插件不生效非常难受。第二PaginationInnerInterceptor的构造参数DbType.MYSQL不能省。省掉之后 MP 会尝试从 JDBC 连接里猜数据库类型多数情况能猜对但在多数据源场景下会猜错表现为分页 SQL 用了 PostgreSQL 的LIMIT x OFFSET y语法去 MySQL 上跑。第三插件顺序有讲究。MP 官方建议的顺序是多租户/动态表名 → 分页/乐观锁 → 防全表操作。如果你后面还要加多租户插件记得插在分页前面否则分页插件先改写了 SQL多租户插件再去找表名就找不到了。2.4 启动自检怎么确认插件真的生效了改完别急着往下走先验证。我一般做三个检查。第一个启动日志里不应该再出现 PageHelper 的 banner如果还有说明依赖没删干净。第二个随便找个已有接口调用一次打开 debug 日志看输出的 SQL如果带LIMIT就说明分页插件在跑。第三个写个临时接口测试BlockAttackInnerInterceptor执行一次不带 where 条件的update正常应该抛出Prohibition of table update operation异常。这个异常出现说明防全表插件生效了可以放心。这里补一句个人经验BlockAttackInnerInterceptor上线前一定要确认业务代码里没有全表更新的合法场景比如定时任务里批量刷某个状态字段、数据修复脚本。我遇到过一个数据迁移脚本用了update xxx set status 1 where 1 1改造后被直接拦掉脚本静默失败跑了三天才发现数据没变。这类脚本记得加真实条件或者临时关掉插件。3. 框架能力对齐分页、填充、主键、逻辑删除地基打好之后要把若依原来依赖 MyBatis 实现的那几项通用能力一一对接到 MP 上。这一步做不干净后面代码生成器生成的代码就跑不通。3.1 分页PageHelper 与 MP 分页的迁移策略前面说了要统一到 MP 分页具体怎么改若依的典型写法是这样GetMapping(/list) public TableDataInfo list(SysUser user) { startPage(); ListSysUser list userService.selectUserList(user); return getDataTable(list); }startPage()从请求里解析pageNum、pageSize、orderByColumn、isAsc塞给 PageHelpergetDataTable()把List包装成PageInfo再取总数。改到 MP 之后这两端都要动。服务层这边让 Service 接口继承IServiceT实现类继承ServiceImplMapper, T分页方法直接调page()PageSysUser page userService.page( new Page(pageNum, pageSize), new LambdaQueryWrapperSysUser() .like(StringUtils.isNotEmpty(user.getUserName()), SysUser::getUserName, user.getUserName()) .eq(user.getStatus() ! null, SysUser::getStatus, user.getStatus()) .orderByDesc(SysUser::getCreateTime) );控制层这边getDataTable()需要改造成能识别IPageprotected TableDataInfo getDataTable(List? list) { TableDataInfo rspData new TableDataInfo(); rspData.setCode(HttpStatus.SUCCESS); rspData.setMsg(查询成功); rspData.setRows(list); rspData.setTotal(list instanceof IPage ? ((IPage?) list).getTotal() : list.size()); return rspData; }这里有个小坑MP 的page()返回的是IPageTgetRecords()才是真正的数据列表。如果你直接把IPage塞给getDataTable前端表格会渲染成空白——因为rows里放的是一个分页对象而不是数组。我建议要么在 controller 里传page.getRecords()并把 total 单独传要么就像上面这样让getDataTable内部判断类型。我倾向后者接口签名统一调用方无感。关于排序参数若依前端传的是orderByColumnisAsc值是数据库列名的驼峰形式MP 的 Wrapper 需要的是实体方法引用两者对不上。我的处理是写一个小工具把前端传来的字段名转成LambdaQueryWrapper的orderBy字符串形式同时做一层白名单校验——只允许排序列在预设集合里防止前端传个不存在的列导致 SQL 报错或者更糟的情况被拼接进去。这层白名单别省虽然若依自己有SqlUtil.escapeOrderBySql做转义但换成 MP 的字符串排序后这层防护就丢了。3.2 审计字段自动填充MetaObjectHandler 接管若依的BaseEntity里有一组公共字段createBy、createTime、updateBy、updateTime、remark、params、searchValue。原来这些是在ServiceImpl里手工 set 的MP 有自动填充机制可以彻底省掉这步。先给BaseEntity的字段加注解public class BaseEntity implements Serializable { TableField(exist false) private String searchValue; TableField(fill FieldFill.INSERT) private String createBy; TableField(fill FieldFill.INSERT) private Date createTime; TableField(fill FieldFill.INSERT_UPDATE) private String updateBy; TableField(fill FieldFill.INSERT_UPDATE) private Date updateTime; private String remark; TableField(exist false) private MapString, Object params; }TableField(exist false)这两处是必须的不加会直接报错params是Map类型searchValue在表里根本没有对应列MP 在拼接 SQL 时会尝试把它们当列名拼进去报Unknown column params。这个错误我在第一次改造时遇到过报错信息不太直观得看完整的 SQL 才能定位。除了这两个实体子类里那些children、parentName、ancestors之类的非表字段也要挨个加或者更省事的方式是继承一个专门用于 VO 的父类。然后是填充器Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, Date.class, new Date()); this.strictInsertFill(metaObject, updateTime, Date.class, new Date()); try { String username SecurityUtils.getUsername(); this.strictInsertFill(metaObject, createBy, String.class, username); this.strictInsertFill(metaObject, updateBy, String.class, username); } catch (Exception e) { // 无登录上下文的场景比如定时任务、数据初始化 } } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, Date.class, new Date()); try { this.strictUpdateFill(metaObject, updateBy, String.class, SecurityUtils.getUsername()); } catch (Exception e) { } } }SecurityUtils.getUsername()外面必须包 try-catch。若依用SecurityContextHolder拿当前登录用户在定时任务、MQ 消费、单元测试这些没有请求上下文的场景里会抛NullPointerException。我第一版没包结果定时任务一跑就报错日志里一堆堆栈排查了半天才发现是填充器的问题。还有一个细节strictInsertFill的严格体现在它只在字段有TableField(fill ...)注解时才填充而且如果字段已经有值就不覆盖。这个行为大部分时候是对的——手工指定了createTime就不该被覆盖。但如果你希望强制覆盖用setFieldValByName()代替。3.3 主键策略与 IdType 的选择全局配置里设了id-type: auto但代码生成器生成的实体最好还是显式标注原因有两个一是明确读代码的人一眼知道主键怎么来的二是某些表可能确实需要用雪花 ID 或者 UUID全局配置一刀切不合适。TableId(value user_id, type IdType.AUTO) private Long userId;IdType的选项里AUTO对应数据库自增ASSIGN_ID对应雪花算法19 位 LongASSIGN_UUID对应 32 位字符串。若依的建表脚本清一色bigint(20) auto_increment所以默认用AUTO。如果你的项目里有分库分表规划主键就不能靠数据库自增了得换成ASSIGN_ID同时注意前端 JS 的精度问题——Long 类型的雪花 ID 超过 2^53 之后在 JS 里会丢精度需要在序列化时把 Long 转成 String。这个精度问题很典型。表现是列表页看着正常点进详情却 404因为前端拿着丢了精度的 ID 去查自然查不到。解决办法是在Jackson的全局配置里把 Long 序列化成 String或者在实体字段上加JsonSerialize(using ToStringSerializer.class)。若依默认没有这个问题因为它用的是自增 ID位数少但一旦切雪花 ID 就会冒出来。3.4 逻辑删除与若依 status 字段的边界MP 的逻辑删除很方便加个TableLogic注解deleteById()会自动变成update set del_flag 1查询自动带上del_flag 0。但我建议你在若依项目里慎用。原因在于若依多数业务表压根没有del_flag字段删除就是物理删除。如果你在全局配置里写了logic-delete-field: delFlagMP 会给所有实体的所有查询自动拼上del_flag 0条件而那些表里没有这个列直接报错。这个错误很隐蔽因为报错信息是Unknown column del_flag in where clause但你翻遍实体类也找不到 del_flag 在哪定义的——它在全局配置里。正确做法是全局配置不设逻辑删除字段需要逻辑删除的表单独在实体上加注解。TableLogic(value 0, delval 2) private String delFlag;value和delval两个值要和你表里的实际取值对上。若依的sys_user表里del_flag是0正常、2删除如果你照抄网上的示例写成0和1会发现已删除的用户还能查出来。这种字段语义必须去翻建表脚本确认不能想当然。还有一点逻辑删除生效后selectById查不到已删除的数据但数据库里还在。做数据统计、做关联查询的时候要特别注意TableLogic只对 MP 自动生成的 SQL 生效手写 XML 里的 SQL 不受影响该带的条件还得自己带。这个不一致性在多表关联查询里特别容易出事。4. 代码生成器改造为 Plus 版本这是整个项目里最有价值的一块。若依自带的生成器用的是原生 MyBatis 模板生成的 Mapper 只有空壳SQL 全靠 XML 里手写。改成 MP 版本之后单表 CRUD 一行 XML 都不用写Wrapper 直接拼条件新模块的开发效率能翻倍。4.1 先读懂原生成器的调用链在动手改模板之前得先知道生成器是怎么跑的。若依的生成器模块叫ruoyi-generator核心链路是这样GenController接收请求 →GenTableServiceImpl负责查询表结构、读取列信息 →GenUtils负责把数据库列类型映射成 Java 类型、构造GenTable和GenColumn对象 →VelocityInitializer初始化模板引擎 → 按GenUtils.getTemplatePath()返回的路径列表逐个渲染vm目录下的模板文件 → 输出成 zip 包。模板文件放在src/main/resources/vm/下结构是vm/ ├── java/ │ ├── controller.java.vm │ ├── domain.java.vm │ ├── mapper.java.vm │ ├── service.java.vm │ └── serviceImpl.java.vm ├── sql/ │ └── sql.vm └── vue/ ├── index.vue.vm └── ...改造范围就是java/目录下的五个文件sql.vm和前端模板基本不用动。这一点很重要——意味着改造是可控的、可回滚的改坏了把文件换回去就行。在动手前我建议你把原来的五个模板先备份一份改的时候对照着看。Velocity 语法对格式敏感比如#if和#end必须配对$变量名拼错不会报错只会输出空白多一个空行都可能影响生成的代码格式。备份能让你在改崩时快速回退。4.2 模板文件逐个改造domain.java.vm的改造重点是加表名注解和主键注解。原模板生成的实体继承BaseEntity我们把TableName和TableId加上package ${packageName}.domain; #foreach ($import in $importList) import ${import}; #end import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; /** * ${functionName}对象 ${tableName} * * author ${author} * date ${datetime} */ TableName(${tableName}) public class ${ClassName} extends BaseEntity { private static final long serialVersionUID 1L; #foreach ($column in $columns) #if(!$column.superColumn $column.javaField ! createBy $column.javaField ! createTime $column.javaField ! updateBy $column.javaField ! updateTime) /** ${column.columnComment} */ #if($column.pk) TableId(value ${column.columnName}, type IdType.AUTO) #end private $column.javaType $column.javaField; #end #end这里#if(!$column.superColumn ...)那段判断是关键。若依的GenColumn里有个superColumn标记用来表示这个字段是否已经在BaseEntity里声明过了。原模板靠它跳过createBy这些字段避免重复声明。改造时保留这个判断另外把createBy、createTime这几个名字也显式排除一遍双保险。mapper.java.vm的改造最简单因为前面已经把com.ruoyi.common.core.mapper.BaseMapper升级成了 MP 版本模板里的继承关系一行都不用改。只需要在头部加个 importpackage ${packageName}.mapper; import ${packageName}.domain.${ClassName}; #if($table.sub) import ${packageName}.domain.${subClassName}; #end /** * ${functionName}Mapper接口 * * author ${author} * date ${datetime} */ public interface ${ClassName}Mapper extends BaseMapper${ClassName} { #if($table.sub) /** * 批量删除${subTable.functionName} */ public int delete${subClassName}By${subTableFkClassName}s(#{${subTableFkClassName}s}); #end }如果你不想动 common 模块里的 BaseMapper比如怕影响别的项目那就改成显式 import MP 的接口模板里写import com.baomidou.mybatisplus.core.mapper.BaseMapper;。两种方式都行前者影响面广但一劳永逸后者隔离性好但每个模板都要写。service.java.vm让接口继承IServicepackage ${packageName}.service; import java.util.List; import com.baomidou.mybatisplus.extension.service.IService; import ${packageName}.domain.${ClassName}; /** * ${functionName}Service接口 */ public interface I${ClassName}Service extends IService${ClassName} { /** * 查询${functionName}列表 */ public List${ClassName} select${ClassName}List(${ClassName} ${className}); #if($table.sub) public List${subClassName} select${subClassName}List(${subTableFkClassName}); #end }继承IService之后save、updateById、removeById、page、list、getById这些方法全部自带不用自己声明。serviceImpl.java.vm的改造有个取舍。原模板生成的实现类是Service public class XxxServiceImpl implements IXxxService内部注入 Mapper手写每个方法。改成继承ServiceImplMapper, Entity之后baseMapper属性可以直接用原来的方法体基本不用改只是把注入的xxxMapper换成baseMapperpackage ${packageName}.service.impl; import java.util.List; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import ${packageName}.mapper.${ClassName}Mapper; import ${packageName}.domain.${ClassName}; import ${packageName}.service.I${ClassName}Service; /** * ${functionName}Service业务层处理 */ Service public class ${ClassName}ServiceImpl extends ServiceImpl${ClassName}Mapper, ${ClassName} implements I${ClassName}Service { Override public List${ClassName} select${ClassName}List(${ClassName} ${className}) { LambdaQueryWrapper${ClassName} wrapper new LambdaQueryWrapper(); #foreach($column in $columns) #if($column.queryType EQ !$column.superColumn) wrapper.eq(${className}.get${column.capJavaField}() ! null, ${ClassName}::get${column.capJavaField}, ${className}.get${column.capJavaField}()); #end #end return baseMapper.selectList(wrapper); } }这里用$column.queryType把若依原来的查询方式EQ、LIKE、BETWEEN、GT、LT映射成 Wrapper 的条件构造这是个很实用的自动化点。原来这些条件靠 XML 的if标签拼现在直接生成到代码里。queryType为LIKE的字段用.like()BETWEEN的用.between()生成出来的方法体可读性比 XML 好太多。一个注意点LambdaQueryWrapper的方法引用写法依赖 Lombok 或者手写的 getter。若依生成的实体默认带 getter/setter模板里有#foreach生成所以没问题。但如果你的项目用了 Lombok 的Data要确认模板里生成的 getter 和 Lombok 没有冲突重复定义会编译报错。controller.java.vm改分页调用GetMapping(/list) public TableDataInfo list(${ClassName} ${className}) { PageDomain pageDomain TableSupport.buildPageRequest(); Page${ClassName} page ${className}Service.page( new Page(pageDomain.getPageNum(), pageDomain.getPageSize()), ${className}Service.buildQueryWrapper(${className}) ); return getDataTable(page); }这里我把 Wrapper 的构建挪到 Service 层buildQueryWrapper方法在serviceImpl里生成。这么做的好处是 controller 保持轻量而且同一个 Wrapper 可以被多个接口复用比如导出接口和列表接口共用同一套查询条件。若依原来的写法是每个接口自己调startPage()条件靠 XML 复用现在改成 Wrapper 复用思路是一样的只是位置变了。导出接口要注意TableDataInfo和AjaxResult的返回类型不同。导出走的是ExcelUtil.exportExcel()它接收List而不是IPage所以得先page.getRecords()再传进去。忘了这一步会报类型转换异常。4.3 GenUtils 与列信息映射的适配模板改完之后还有几个 Java 层面的小调整。第一个是GenUtils.getTemplatePath()它返回一个 MapString, Stringkey 是模板名value 是输出路径。这部分基本不用改但如果你想让生成器同时输出一份mapper.xml哪怕是空的可以在这里加一条。我的做法是保留 XML 输出但内容为空因为若依的前端页面模板依赖mapper.xml存在才生成某些结构删掉会引起前端模板报错。第二个是主键字段的识别。原生成器通过column.isPk()判断主键这个逻辑在GenTableServiceImpl里不用动。但如果你的表用了复合主键MP 不支持复合主键TableId只能标一个字段这种情况得改用手写 SQL 绕过。这种情况在若依项目里很少见但外键关联表偶尔会遇到。第三个是字段类型的映射。若依的GenUtils.getJavaType()把tinyint、smallint、int映射成Integerdatetime映射成Date。MP 对类型没有额外要求这块保持原样即可。但有一类字段要注意json类型。MySQL 5.7 的json列在若依加 MP 的环境里需要额外处理实体字段标TableField(typeHandler JacksonTypeHandler.class)同时类上要加TableName(autoResultMap true)否则查出来永远是 null。这个坑我踩过排查了很久。还有一个隐性的适配点若依的GenTable里有个treeCode、treeParentCode的树形结构配置生成的selectXxxList会按parent_id排序。改成 Wrapper 之后这部分排序要挪到 Wrapper 的orderBy里别忘了加否则树形菜单的顺序会乱。4.4 生成结果回归验证清单模板改完不代表完事得用一张真实的表跑一遍看生成结果对不对。我固定用这几条检查检查项期望结果不通过时的典型原因实体类注解有TableName、主键有TableId模板#if($column.pk)判断写错import 齐全TableName、TableId、IdType都在忘了在模板头部加 importService 继承extends IServiceT模板里漏改了 interface 声明分页调用生成的是service.page(...)而非startPage()controller 模板没替换编译通过mvn compile 无错误import 冲突比如同时 import 两个 BaseMapper启动无报错无 Mapper 重复注册、无 Bean 冲突旧依赖未排除表结构改完之后记得让生成器重新同步一次。若依的生成器有个编辑功能可以在界面上改字段的 Java 类型、查询方式、是否在列表显示这些配置存在gen_table_column表里。改了表结构之后同步会把新列补进来但已有的配置不会丢——这个机制很贴心改模板期间表结构频繁变动也不怕。最后一步是把生成出来的代码放进项目跑一遍完整 CRUD。我的验收标准是新增一条、列表能查到、修改能生效、删除能成功、导出的 Excel 能打开。五步全过模板改造就算收工。5. 踩坑实录与排查速查表改造过程中遇到的问题我基本都记了笔记挑五个最高频的写出来附上定位思路。5.1 五个高频报错的定位路径报错一Invalid bound statement (not found)这是最经典的。原因是 Mapper 接口的方法在 XML 里找不到对应 SQL或者 XML 根本没被加载。在若依 MP 的场景下常见原因有三个mapperLocations配置的路径通配符写错比如写成classpath:mapper/**/*.xml少了*XML 文件的namespace和 Mapper 接口全限定名不一致打包时 XML 没被复制到target/classes需要在 pom 里配resources包含**/*.xml。排查方法启动时加--debug看日志里有没有Parsed mapper file字样没有就是没加载。**报错二Unknown
阅读完成 · 觉得有帮助?
咨询建站