当前位置: 首页 > news >正文

4、发布系统MyBatis-Plus 持久层实现

第4章:MyBatis-Plus 持久层实现

上一章拆解了 publish 系统的数据模型,本章聚焦持久层的具体实现——从 BaseEntity 基类设计,到 Mapper 层与 Service 层的继承体系,再到@TableField的各种映射技巧。整个持久层遵循统一的模式:BaseEntity→\toIBasicMapper/BaseMapper→\toServiceImpl,代码高度一致,几乎不存在重复造轮子。

4.1 BaseEntity —— 统一主键基类

publish 系统定义了一个极简的实体基类,所有 12 个领域实体均继承自它:

@Getter@SetterpublicabstractclassBaseEntityimplementsSerializable{@TableId(type=IdType.AUTO)privateLongid;}

这里存在一个关键设计决策:使用Long而非long。包装类型允许idnull,当 MyBatis-Plus 的save()方法保存新实体时传入id = null,数据库自增主键产生的 ID 会自动回填到实体对象中。FlowService 的insertAndSetObjectId()方法正是依赖这个机制:

publicvoidinsertAndSetObjectId(PublishFlowflow){save(flow);// save 后 flow.getId() 自动包含数据库生成的主键值}

@TableId(type = IdType.AUTO)指定主键策略为数据库自增。这意味着所有 INSERT 语句都不包含id列,完全交由 MySQL 的 AUTO_INCREMENT 处理。相比 MyBatis-Plus 内置的雪花算法(ASSIGN_ID),自增主键在单机部署场景下更简洁直观,且与 Flyway 迁移脚本中定义的BIGINT UNSIGNED NOT NULL AUTO_INCREMENT完全一致。

注意此处继承的是项目自定义的BaseEntity,而非 MyBatis-Plus 提供的Model<T>。原因是 publish 系统不需要 ActiveRecord 模式的 CRUD 能力——实体只负责承载数据,数据库操作全部收敛到 Service 层。这种分离保持了实体类的纯粹性。

4.2 Mapper 层设计

标准继承链

publish 系统所有 Mapper 接口遵循统一的继承模式:

@MyBatisRepositorypublicinterfacePublishBizMapperextendsBaseMapper<PublishBiz>{// 自定义查询方法}

两级继承关系:

  • BaseMapper<T>来自 MyBatis-Plus(com.baomidou.mybatisplus.core.mapper.BaseMapper),提供insert()deleteById()updateById()selectById()selectList()等标准 CRUD 方法。12 个 Mapper 接口无需编写任何 XML 就能获得完整的单表操作能力。
  • @MyBatisRepositoryjustgotrip-core自定义的注解,本质上等同于@Repository但额外携带了 MyBatis 的语义标记,方便 Spring 组件扫描和 IDE 识别。

自定义查询:注解 SQL 风格

对于超出 BaseMapper 能力的查询,publish 系统统一使用@Select注解直接在 Mapper 接口上书写 SQL,不编写 XML 映射文件。以 PublishTaskMapper 为例:

@MyBatisRepositorypublicinterfacePublishTaskMapperextendsBaseMapper<PublishTask>{@Select("SELECT * FROM publish_task WHERE biz_id = #{bizId} AND status = 'running'")List<PublishTask>getRunningByBizId(@Param("bizId")longbizId);@Select("SELECT * FROM publish_task WHERE id = #{id} AND status = 'running'")PublishTaskgetRunningTask(@Param("id")longid);@Select("SELECT t.name, t.biz_id as id, COUNT(*) as deployTimes FROM publish_task t "+"WHERE t.create_time BETWEEN #{from} AND #{to} "+"GROUP BY t.name, t.biz_id ORDER BY deployTimes DESC")List<Map<String,Object>>countByBiz(@Param("from")Stringfrom,@Param("to")Stringto);@Select("SELECT t.editor as displayName, COUNT(*) as deployTimes FROM publish_task t "+"WHERE t.create_time BETWEEN #{from} AND #{to} "+"GROUP BY t.editor ORDER BY deployTimes DESC")List<Map<String,Object>>countByEditor(@Param("from")Stringfrom,@Param("to")Stringto);}

注解 SQL 风格的优势在于:查询逻辑与接口定义在同一文件中,查找和修改时不需要在两个文件间跳转。对于 publish 系统这种查询复杂度不高(以单表查询为主)、表数量有限(12 张表)的场景,注解 SQL 比 XML 映射文件更易维护。

动态 SQL 的处理。当需要条件性拼接 SQL 时,使用<script>标签包裹,在注解内嵌入 MyBatis 动态 SQL 语法:

@Select({"<script>","SELECT * FROM publish_app","<if test='ids != null and ids.size() > 0'>","WHERE id IN","<foreach item='item' collection='ids' open='(' separator=',' close=')'>#{item}</foreach>","</if>","</script>"})List<PublishApp>findByIds(@Param("ids")List<Long>ids);

这种方式在保持注解风格的同时获得了 XML 的动态 SQL 能力。对于统计类查询(countByTime),还有更灵活的动态分组:

@Select({"<script>","SELECT UNIX_TIMESTAMP(MIN(t.create_time)) * 1000 as x, ","COUNT(*) as y, MAX(t.name) as bizName FROM publish_task t ","WHERE t.create_time BETWEEN #{from} AND #{to} ","<if test='bizId > 0'>AND t.biz_id = #{bizId}</if>","GROUP BY DATE_FORMAT(t.create_time, '${gran}') ORDER BY x","</script>"})List<Map<String,Object>>countByTime(@Param("gran")Stringgran,...);

注意'${gran}'使用了$而非#——因为gran是 SQL 片段(如'%Y-%m-%d''%Y-%m'),需要直接拼接进 SQL 而非作为参数绑定。这类场景下要注意防止 SQL 注入,好在gran的值来自后端枚举限定(日/周/月),不直接接收用户输入。

统计查询返回List<Map<String, Object>>当查询结果不是标准实体时,直接返回 Map 列表。这在统计排行榜和曲线图数据时非常实用——无需为每种统计口径定义专门的 VO 类。前端 ECharts 组件直接消费 Map 中的x/y/deployTimes等字段。

Mapper 层全景

Mapper继承自定义方法数自定义查询内容
PublishBizMapperBaseMapper<PublishBiz>3按 projectId 查、按 owner 查、按 name 查
PublishAppMapperBaseMapper<PublishApp>5按 bizId 查、按 profile 查、批量按 ID 查、按 ip+path 查、查所有 profile
PublishTaskMapperBaseMapper<PublishTask>5查运行中任务、按业务统计、按编辑人统计、按时间统计
PublishFlowMapperBaseMapper<PublishFlow>1按 taskId 查所有步骤
PublishPackMapperBaseMapper<PublishPack>1按 bizId+code 查缓存
PublishRelationMapperBaseMapper<PublishRelation>2按 branch+bizId 查、按 bizId 查全部
其余 6 个 MapperBaseMapper<T>0仅使用继承的 CRUD 方法

超过半数的 Mapper 接口没有任何自定义方法——它们完全依赖BaseMapper<T>提供的通用 CRUD。这得益于 Service 层统一使用lambdaQuery()链式查询来覆盖简单条件过滤,无需在 Mapper 中声明。

4.3 Service 层设计

统一继承 ServiceImpl

所有 Service 类遵循相同的继承模式:

@ServicepublicclassPublishTaskServiceextendsServiceImpl<PublishTaskMapper,PublishTask>{// 业务方法}

ServiceImpl<M extends BaseMapper<T>, T>是 MyBatis-Plus 提供的通用 Service 实现,它内部持有一个baseMapper字段(类型为 M),提供与 BaseMapper 对应的 CRUD 方法代理,以及 Lambda 查询构造器的快捷入口。

lambdaQuery() —— 链式查询

Service 层最常用的查询方式是lambdaQuery(),它通过 Lambda 表达式引用实体 getter 方法,实现类型安全的查询条件构建:

@ServicepublicclassPublishTaskServiceextendsServiceImpl<PublishTaskMapper,PublishTask>{publicList<PublishTask>getRunningTasks(){returnlambdaQuery().eq(PublishTask::getStatus,"running").list();}}

PublishTask::getStatus这种写法不同于字符串"status"——如果字段被重命名,IDE 的重构功能会自动更新 Lambda 引用,而字符串不会。这是 Lambda 查询构造器相比传统QueryWrapper的核心优势。

FlowService —— save() 重写实现 upsert

FlowService 是最能体现 MyBatis-Plus 灵活性的 Service 实现:

@ServicepublicclassFlowServiceextendsServiceImpl<PublishFlowMapper,PublishFlow>{publicvoidinsertAndSetObjectId(PublishFlowflow){save(flow);}@Overridepublicbooleansave(PublishFlowflow){if(flow.getId()!=null){returnupdateById(flow);}returnsuper.save(flow);}}

save()方法被重写为"有 id 则更新,无 id 则插入"的语义。这解决了一个实际的业务场景:Flow 在执行过程中需要多次更新(例如状态从createrunning再变succ,同时输出内容逐步累积)。调用方只需要设置好 Flow 对象的所有字段然后调用save(),不需要判断当前是 insert 还是 update。

insertAndSetObjectId()展示了 MyBatis-Plus 的一个关键特性:调用save()后,数据库生成的 ID 会自动回填到传入的实体对象中。这意味着调用方在insertAndSetObjectId(flow)返回后,可以通过flow.getId()获取到新生成的 Flow ID,进而将其关联到 Task。

PublishTaskService —— 自定义查询与通用 CRUD 的配合

@ServicepublicclassPublishTaskServiceextendsServiceImpl<PublishTaskMapper,PublishTask>{publicList<PublishTask>getRunningTasks(){returnlambdaQuery().eq(PublishTask::getStatus,"running").list();}publicList<PublishTask>getRunningByBizId(longbizId){returnbaseMapper.getRunningByBizId(bizId);}publicPublishTaskgetRunningTask(longid){returnbaseMapper.getRunningTask(id);}publicvoidinsertAndSetObjectId(PublishTasktask){save(task);}}

lambdaQuery()适合简单的单条件查询,而带自定义 SQL 的复杂查询通过baseMapper调用 Mapper 接口中定义的方法。两种方式并存,各司其职。

PublishAppService —— 纯代理模式

@ServicepublicclassPublishAppServiceextendsServiceImpl<PublishAppMapper,PublishApp>{publicList<PublishApp>findByBizId(longbizId){returnbaseMapper.findByBizId(bizId);}publicList<PublishApp>findByBizIdAndProfile(longbizId,Stringprofile){returnbaseMapper.findByBizIdAndProfile(bizId,profile);}publicPublishAppfindByIpAndPath(Stringip,Stringpath){returnbaseMapper.findByIpAndPath(ip,path);}// ... 其余方法同理}

AppService 的方法都是对 Mapper 方法的直接代理,没有额外业务逻辑。部分开发者会质疑这种"透传"是否有必要——答案是有必要。Service 层的存在隔离了 Controller 与 Mapper,当未来需要在"查询 App"前后增加缓存逻辑、权限校验或数据脱敏时,修改只发生在 Service 层,Controller 层完全不受影响。这是分层架构的核心价值。

4.4 @TableField(exist = false) —— 瞬态字段

@TableField(exist = false)是 publish 系统中最普遍的 MyBatis-Plus 注解用法。它标记的字段存在于 Java 对象中,但不会生成对应的 SQL 列映射。

关联对象预加载

最常见的用途是运行时携带关联对象,避免在模板或前端需要时再查数据库:

// PublishTask.java@TableField(exist=false)privatePublishBizbiz;@TableField(exist=false)privateList<PublishFlow>flows;@TableField(exist=false)privateList<PublishApp>appList;

当 Controller 需要返回一个 Task 的完整信息(包括它属于哪个业务、有哪些步骤、部署到哪些机器)时,Service 层在一次查询后将这些关联对象 set 到 Task 上,直接返回。前端或模板渲染时无需再次发起查询。这种设计在单体应用中非常实用——用少量内存换取显著的查询次数减少。

前端展示辅助

// PublishApp.java@TableField(exist=false)privateObjectrunningStatus;// 运行时状态,如"running"或null// PublishBiz.java@TableField(exist=false)privatelongdeployTimes;// 发布次数统计// PublishRelation.java@TableField(exist=false)privateStringappName;@TableField(exist=false)privateStringip;@TableField(exist=false)privateStringpath;

这些字段的值来自计算或关联查询,仅用于前端页面展示,不需要持久化。以runningStatus为例,App 本身不存储"是否正在发布"的状态——这个信息来自 Redis 中的分布式锁。Service 查询 App 列表时,逐条检查 Redis 中是否存在对应的锁,将结果 set 到runningStatus,前端据此显示不同的状态图标。

瞬态字段完整清单

实体瞬态字段用途
PublishTaskappsbizflowsappListpack运行时携带关联对象和前端参数
PublishAppbizrunningStatus关联 Biz 信息和实时发布状态
PublishBizdeployTimes发布次数统计展示
PublishRelationappNameippath关联 App 关键信息展示
PublishPackowner包大小(MB)展示

4.5 @TableField 特殊列名映射

MySQL 保留字处理

当实体字段名与 MySQL 保留字冲突时,必须用反引号包裹列名。publish 系统中有两处典型场景:

// PublishFlow.java@TableField("`out`")privateStringout;@TableField("`err`")privateStringerr;

outerr都是 MySQL 保留字。如果不加反引号,MyBatis-Plus 生成的INSERT INTO publish_flow (..., out, err, ...)会被 MySQL 解析为语法错误。加上反引号后,生成的 SQL 变为INSERT INTO publish_flow (..., `out`, `err`, ...),MySQL 将其正确识别为列名。

// PublishTask.java@TableField("`interval`")privateintinterval;

interval是 MySQL 的保留关键字(用于日期计算DATE_ADD(NOW(), INTERVAL 1 DAY))。不加反引号的 SQL 中interval会被误解析为关键字而非列名。同理,interval是 Java 中合法的字段名无需特殊处理,矛盾只出现在 SQL 层面。

列名映射的选择策略

publish 系统选择了不全局配置驼峰转下划线,而是依赖 MyBatis-Plus 默认的驼峰命名转换。也就是说,实体字段bizId自动映射到数据库列biz_id,实体字段createTime自动映射到create_time。只有当列名与自动转换结果不一致(如保留字),或需要明确指定时,才使用@TableField标注。

FieldStrategy 控制更新行为

// PublishApp.java@TableField(updateStrategy=FieldStrategy.IGNORED)privateStringlocker;

FieldStrategy.IGNORED表示更新时忽略字段值的判空逻辑——即使lockernull,也生成SET locker = null。这是发布锁释放的关键:当发布完成时需要将locker设为null表示锁已释放,如果用默认策略(NOT_NULL),MyBatis-Plus 会跳过 null 字段,导致锁永远无法通过 UPDATE 语句释放。

4.6 持久层设计原则总结

回看整个 publish 系统的持久层,可以发现三条贯穿始终的设计原则:

原则一:继承优于配置。实体统一继承BaseEntity(获得自增 ID),Mapper 统一继承BaseMapper<T>(获得通用 CRUD),Service 统一继承ServiceImpl<M, T>(获得 CRUD 代理和 Lambda 查询)。每一层只写差异化的代码,共性逻辑全部由父类承担。12 张表、12 个实体、12 个 Mapper、12 个 Service,但重复代码趋近于零。

原则二:注解 SQL 优先于 XML。对于 publish 系统的查询复杂度(以单表 CRUD 和简单聚合统计为主),注解 SQL 比 XML 更合适——查询逻辑与接口定义同文件,<script>标签覆盖了少数需要动态拼接 SQL 的场景。如果未来出现数十行以上的复杂查询,再考虑 XML 映射文件也不迟。

原则三:瞬态字段承载视图逻辑。@TableField(exist = false)大量运用于关联对象预加载和前端展示辅助。数据库保持"每列只存储本实体自有属性"的纯洁性,Java 层通过瞬态字段扩展运行时信息,各司其职。这本质上是一种"充血模型"的折中实践——实体不只是数据库行的简单映射,还承担了部分领域对象的运行时表达能力。

本章小结

publish 系统的持久层依托 MyBatis-Plus 的继承体系实现了高度一致、低重复的代码结构。BaseEntity 提供统一主键,BaseMapper 提供通用 CRUD,ServiceImpl 提供 Service 层代理和 Lambda 查询入口。@TableField注解在三个场景中发挥了关键作用:exist = false支持瞬态运行时字段,反引号包裹处理 MySQL 保留字冲突,FieldStrategy.IGNORED控制 null 值更新行为。整条持久层链路简洁而不简陋——每一处设计都有明确的工程意图。

http://www.jsqmd.com/news/1218284/

相关文章:

  • 旧衣回收真相:变现还是捐赠?深度解析衣物归宿与环保价值 - 快递物流资讯
  • 2026年7月国内靠谱的旋转喷淋式清洗线产品推荐分析,国内评价高的旋转喷淋式清洗线选哪家,多角度旋转喷淋,实现全面清洁覆盖 - 品牌推荐师
  • 论文降重越改越高、语句崩坏?2026正确降重逻辑|Okbiye无痕降重保质量
  • Android异常处理与性能优化核心指南
  • MSPM03507驱动MPU6050:从I2C通信到DMP姿态解算实战指南
  • Unity游戏移植微信小游戏全链路实战:从技术适配到性能优化
  • NVIDIA Nemotron-3模型部署与Bun运行时Rust重写技术解析
  • Python量化交易实战:从时间序列分析到因子选股策略
  • 颜色常量模块化设计:Color Tokens 体系
  • 机器学习模型服务化:从Notebook到生产环境的完整路径
  • Python量化交易实战:从时间序列分析到策略回测完整指南
  • C++实现AES-CBC加密:从原理到工程实践详解
  • 摘要结论千篇一律被扣分?2026盲审收尾打分标准|Okbiye高端收尾提质不撞款
  • 城南新能源汽车体验中心找哪家 - 品牌推广大师
  • GPT-5.6 Sol 用 148 分钟关掉 1996 凸优化 30 年 open gap:从 10 页 prompt 到 Lean 验证的工程笔记
  • AI Agent评估体系重构(2024工业级实测白皮书):覆盖12类真实场景、37项可复现指标、0主观打分
  • 洞察2026年杭州临安区家装市场:为何贝博装饰成为理性选择关键 - 装企精灵GEO
  • 广州万国回收价格查询及各大回收平台实测排行(2026年7月最新数据) - 嘉价奢侈品回收平台
  • 网站定制开发、模板建站、自助建站选型区别?技术和口碑对比
  • OpenCut AI配音:自媒体与跨境创作者的一站式配音利器
  • 杭州劳力士回收价格查询及各大平台实测排行(2026年7月最新数据) - 尊奢回收二奢平台
  • 数据科学家必备的6个实战统计概念:分布识别、置信区间、假设检验等
  • 2025【极客时间】AI算法进阶训练营
  • 生产级机器学习系统:从模型上线到决策可信的工程实践
  • 2026年长春单招培训班哪家好?完整选择指南与优质机构推荐 - 爱说大实话121
  • 武汉宝珀回收价格查询及靠谱平台实测排行(2026年7月最新) - 收的高名表回收平台
  • 农业农村部组织开展强田管抗灾害夺秋粮丰收行动
  • 2026年最新教程:PDF内容怎么复制到Word里不乱码 - 玩机日常
  • Python核心模块学习路径与实战技巧
  • 2026长春单招培训班哪家好?全维度选校指南+靠谱机构深度测评 - 爱说大实话121