news 2026/10/31 3:36:37

微服务下的MyBatis-Plus集成实践:从选型到避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微服务下的MyBatis-Plus集成实践:从选型到避坑

做微服务拆分的时候,我第一个考虑的往往不是注册中心选型,也不是网关怎么搭,而是数据库访问层用什么。项目里第一个被拆出来的订单服务,数据访问层是和 MyBatis-Plus 一起落地的,整趟跑下来最大的感受是:它把单表 CRUD 的体力活全部接走了,但把 SQL 的控制权原封不动留给你。对于微服务这种服务边界清晰、单表操作占比高的场景,这刚好是大部分团队想要的东西。

我见过不少团队在 MyBatis、JPA 和 MyBatis-Plus 之间反复横跳,最后又改回来的。MyBatis-Plus 的上手成本很低,它不改变 MyBatis 的用法,只是在你原本要写样板代码的地方替你代劳。这篇就把我们在微服务项目里集成 MyBatis-Plus 的完整过程、核心配置、踩过的坑和解决办法拆开讲一遍,尤其适合那些正在从单体服务往外拆、又要保持开发速度的团队参考。

1. 为什么微服务的数据库访问层选了 MyBatis-Plus

1.1 这个项目里到底缺什么

这是一个典型的业务服务拆分场景:原来一个单体系统,订单、用户、商品、支付全挤在一套代码里,单库单表,几十个 mapper 文件编译起来特别慢,改一个字段要全量回归。到后面你发现,绝大多数新增接口都是对某几张表的单表增删改查,真正的复杂 join 和报表统计只占一小部分。

拆微服务的时候,数据库也被跟着拆开,每个服务持有自己的库,服务之间禁止跨库 join。这意味着你日常写的 SQL 天然会变成单表操作:查订单列表、按用户查购物车、改商品库存、插一条支付流水。这类操作用原生 MyBatis 写,每个实体类都要配一套 insert、deleteById、selectById、updateById 的 XML,极其无聊,而且一多就容易复制粘贴出错。

我最早考虑过用 JPA。它的抽象能力强,实体关系映射写起来省事,但在微服务场景里有个实际问题:团队里总有那么几位同事对 JPA 的懒加载、N+1 查询和复杂条件拼接不太拿手,写着写着性能就出问题。还有一点是 JPA 的命名规范和 SQL 控制力都不如 MyBatis 系来得直接,DBA 审查 SQL 的时候也头疼。

1.2 对比一圈之后的选型理由

当时我们项目组把三种方案拉到白板上做了个对比,除了上述问题,还从迁移成本和可控性两个角度评估。

对比维度原生 MyBatisSpring Data JPAMyBatis-Plus
单表 CRUD 编码量高,所有 SQL 手写低,方法名推导低,BaseMapper 直接提供
复杂 SQL 控制力强弱,需要 @Query 或 Specification强,XML 完全保留
团队学习成本低中高很低,会 MyBatis 就会这个
内置通用能力无有部分分页、逻辑删除、乐观锁、自动填充
从 MyBatis 迁移成本-高极低

这个表里最关键的一行是最后一行。我们不是新起一个项目从零做,而是把老系统的数据访问层迁出来,如果选 JPA 等于把 mapper 全部重写一遍,风险太大。选 MyBatis-Plus 则几乎是无痛的,实体类加几个注解,继承 BaseMapper,原有的 XML 文件继续留在工程里,复杂查询不受影响。

另外还有一个没写在表里的原因:微服务数量多起来以后,每个服务里都有大量简单重复的数据访问代码。MyBatis-Plus 把这一层抹平,能让团队把精力放在接口设计、缓存策略和性能优化上,而不是反复写一样的 SQL。对我们这种十几个服务并行开发的节奏来说,省下的人力非常可观。

注意:如果你的项目里存在大量多表 join 的复杂报表需求,MyBatis-Plus 并不会拖你后腿,XML 原生 SQL 照样写。真正需要确认的是,你的团队是否愿意遵守“单表操作用 MP,复杂查询写 XML”这条约定。

2. 微服务集成 MyBatis-Plus 的准备工作

2.1 依赖引入:版本问题比想象中多

集成第一件事是引入依赖,但这里的坑比想象中多。很多人在网上搜到旧教程,直接复制mybatis-plus-boot-starter,结果 Spring Boot 版本一升级就启动报错。

我们用的是 Spring Boot 2.7.x 时,依赖是这样加的:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency>

这里有个非常关键的分界线:如果你的服务基于 Spring Boot 3.x,就不能再用上面这个 starter,而是要引入mybatis-plus-spring-boot3-starter。3.x 环境下用普通 starter 会出现容器启动失败、SqlSessionFactory 创建异常之类的诡异问题,而且报错信息藏得比较深,不仔细看很容易以为是数据库连接问题。

版本选择上我的建议是:不要追最新,用稳定版。3.5.3.2 或者 3.5.4 在 2.7.x 体系下都很稳。确定版本后最好统一放到父 POM 的 dependencyManagement 里管理,后面每个服务引入时直接继承版本号,免得十几个服务里版本漂移,排查起来想骂人。

另外别忘了数据库驱动。微服务里每个服务独立连自己的库,MySQL 驱动版本要和数据库实例匹配。我们就有一次因为驱动版本太旧,连 8.0 的实例时 SSL 握手一直失败,折腾了半天才发现是驱动问题。

2.2 配置项与基础 Bean 的推荐模板

依赖引入之后,配置文件看起来简单,但有很多值得提前设置好的选项。我给出一个我们在服务里实际使用的模板,你可以直接抄:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_service username: your_username password: your_password mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.order.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

mapper-locations指定 XML 位置,这个路径建议显式配出来,不要用默认值。type-aliases-package配合 XML 里使用短类名,写 SQL 时能省不少字符。map-underscore-to-camel-case必须打开,否则数据库下划线字段映射不到实体的驼峰属性上,这种问题报错不明显,查起来还烦。

banner: false纯粹是让启动日志干净一点,在微服务多实例启动的时候,满屏的 MP Logo 看着头疼。DbConfig里的全局逻辑删除和主键策略我建议在配置层先统一,不要只依赖实体注解,这样即使新同事漏写注解,也不会尴尬地插入一条物理数据。

提示:log-impl: StdOutImpl只在开发环境开,生产环境务必关掉或者换成以参数形式输出的日志实现,否则 SQL 日志会刷爆你的日志系统,稍微高一点并发就能把磁盘写满。

分页插件的 Bean 也是在这个阶段就要准备好的:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }

这里有个常见误区:网上很多老文章会让你配一个叫做PaginationInterceptor的 Bean,那是 3.4 之前的老 API,在 3.5.x 版本里已经废弃甚至不存在了。新的写法是统一使用MybatisPlusInterceptor,然后往里面 add 各种 InnerInterceptor。不注意这一点,你在升级版本之后会发现分页莫名其妙不生效,实际原因是插件根本没被加载。

3. 核心功能逐项拆解与实操展示

3.1 条件构造器:单表查询基本不用写 SQL

条件构造器是 MyBatis-Plus 最有存在感的功能,也是我们日常开发中写得最多的东西。订单列表筛选、按状态统计、按时间范围查询,几乎全部用它来处理。

最推荐用的是 Lambda 形式:

LambdaQueryWrapper<Order> wrapper = Wrappers.<Order>lambdaQuery() .eq(Order::getUserId, userId) .ge(Order::getCreateTime, startTime) .lt(Order::getCreateTime, endTime) .in(Order::getStatus, Arrays.asList(1, 2, 3)) .orderByDesc(Order::getCreateTime) .last("limit 500"); List<Order> orders = orderMapper.selectList(wrapper);

用 Lambda 而不是直接传字段名的好处是编译期就能发现字段名拼写问题,重构实体时也不会留下断链的 SQL。你可能会觉得这只比手写 SQL 少了几行,但在订单服务里,类似的条件组合有几十种,全部用这种方式拼,代码量大幅下降,而且读起来像在描述业务规则,而不是在写字符串拼接。

需要注意的是,last("limit 500")这个方法会把字符串直接拼接到 SQL 末尾,如果你拼的是用户传入的参数,就存在 SQL 注入风险。我们内部约定:last方法只允许拼接常量或经过校验的固定值,不允许直接拼请求参数。

复杂查询也别硬套条件构造器。订单服务里有一个统计报表接口,要 join 三张表,我们直接在 OrderMapper 里手写 XML,在 XML 里用include复用公共条件片段。条件构造器负责单表场景,XML 负责复杂场景,边界划清楚,代码读起来就清爽。

3.2 分页插件:配置不对会静默失效

分页可能是 MyBatis-Plus 用户遇到问题最多的地方,而且出问题时往往不是报错,而是查询结果莫名多出所有数据。这通常意味着分页插件没有生效。

插件生效的前提是MybatisPlusInterceptor这个 Bean 被注册到 Spring 容器里,并且PaginationInnerInterceptor被 add 进拦截器列表中。确认这一点之后,再确认数据库类型:

PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL);

如果你的服务连的是 PostgreSQL 或者 Oracle,这里写错数据库类型,分页 SQL 方言会对不上。比如 MySQL 生成的是LIMIT ? OFFSET ?,PG 生成的还带LIMIT ? OFFSET ?但写法上略有差异,Oracle 则要用 ROWNUM 子查询,一旦类型不匹配,轻则分页结果不对,重则直接执行报错。

分页的正确用法是这样:

Page<Order> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Order> wrapper = Wrappers.<Order>lambdaQuery() .eq(Order::getUserId, userId); Page<Order> result = orderMapper.selectPage(page, wrapper); List<Order> records = result.getRecords(); long total = result.getTotal();

这里有一个容易被忽略的细节:selectPage方法返回的 Page 对象和传入的 page 是同一个对象,但建议用返回值接收,原因是有一些场景下插件会重建新的 Page 对象。我们统一用返回值处理,代码上也更清晰。

我在分页插件里加了setMaxLimit(500L),这是给团队设的一个安全上限。如果前端传了一个每页 10000 条的参数,直接被拦截器改写成 500,避免有人把列表接口当导出接口用。

3.3 自动填充、逻辑删除与乐观锁

微服务里每张表基本都带 create_time、update_time、deleted、version 这些通用字段,如果不借助框架能力,每个 insert 和 update 都要手动 set 一遍。MyBatis-Plus 的自动填充功能可以统一处理这部分。

实体类里的注解写法:

@Data @TableName("order_info") public class Order { @TableId(type = IdType.ASSIGN_ID) private Long id; private Long userId; private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableLogic private Integer deleted; @Version private Integer version; }

然后实现 MetaObjectHandler:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

注意这里有个小坑:fill 字段名写的是实体属性名,不是数据库列名,createTime对应create_time,一旦写错填充不出来,而且不会有任何报错提示。用strictInsertFill而不是旧版setFieldValByName,是因为前者会判断实体里是否已经有值,有值的情况下不会强行覆盖。

逻辑删除开启后,查询、更新、删除都会自动带上 deleted 条件。这个设计在单表服务里非常省心,但在设置唯一索引时要格外小心。比如用户表用手机号做唯一索引,逻辑删除后再次插入相同手机号就会触发唯一键冲突。这个问题的常见解法是把删除标记改成删除时间戳,正常为 0,删除后写入当前时间戳,这样唯一索引改成“手机号+删除时间戳”就能规避。

乐观锁插件配合库存类操作很好用。在原生的库存扣减里,我们会手写update stock = stock - 1 where id = ? and stock > 0,这个场景直接用 SQL 更干脆。乐观锁更适合那种“先查询再更新”的流程,比如用户修改订单备注,先查出订单对象,页面编辑后提交,updateById时会自动带上where version = ?,更新成功后 version 自动加一。如果中间有其他人改过,update 返回影响行数为 0,业务层就能感知到冲突并走重试或提示逻辑。

3.4 表字段映射与主键策略

表字段和实体属性的映射,除了驼峰规则自动转换外,还有一些不规则的字段名需要手动指定。比如订单表里有个字段叫order_no,你实体属性叫 orderNo,驼峰转换能自动处理。但如果数据库字段是remark,实体属性习惯叫memo,就得用 @TableField 指定:

@TableField("remark") private String memo;

这种映射关系我建议在处理每张表时都过一遍,尤其是历史库迁出来的表,字段命名不规范的很多,与其在 XML 里反复写别名,不如在实体上一次性映射清楚。

主键策略上,微服务场景强烈不建议用数据库自增主键。一是因为分库分表后自增键会产生冲突或空洞,二是因为微服务之间通过服务调用插入数据,事务边界往往比较长,如果 insert 后马上要用主键做后续关联,应用层生成主键更可控。ASSIGN_ID默认使用雪花算法生成分布式 ID,这个 ID 是 long 类型,前端接收时要注意精度问题,超过 JS 安全整数范围的 ID 会丢失精度。我们订单服务的主键 ID 在返回给前端时统一转成 String,这个坑踩过一次之后全项目组就记住了。

4. 微服务场景下的高级玩法

4.1 多数据源与读写分离

服务一多,数据库自然也开始拆分。一开始订单服务只有一主一从,查询量上来后要做读写分离。MyBatis-Plus 官方配套的dynamic-datasource-spring-boot-starter在这个场景下很好用,它允许你在同一个服务里配置多个数据源,然后通过注解切换。

依赖引入后配置如下:

spring: datasource: dynamic: primary: master strict: true datasource: master: url: jdbc:mysql://localhost:3306/order_service username: your_username password: your_password slave_1: url: jdbc:mysql://localhost:3307/order_service username: your_username password: your_password

使用的时候直接在 service 方法上标注:

@DS("slave_1") public List<Order> listOrders(OrderQuery query) { // 查询走从库 }

这个功能听着很香,但使用时有一个要注意的边界:@DS的切换是方法级别的,而且和事务注解放在一起时,数据源切换要在事务开启之前发生,否则事务已经拿到了连接,切库就不生效了。我们的约定是:写操作不加 @DS,走主库;读操作里如果有缓存更新之类的连带写操作,就把读和写拆到不同方法或不同 service 里。

另外,读写分离的最大隐患是主从延迟。订单刚创建成功,用户立刻刷新列表,查从库可能查不到。这种场景我们一般不强行切到走到从库的查询路径,而是用缓存或者直接标记查询主库来兜底。高一致性读不走读写分离,这是架构层面的原则,不是框架能解决的。

4.2 多租户插件与数据隔离

如果是做 SaaS 系统,每个服务后面都有一张带 tenant_id 的表,多租户隔离是一个绕不开的需求。MyBatis-Plus 提供了TenantLineInnerInterceptor,可以在 SQL 执行前自动追加租户条件,避免你手工在每一条查询里拼 tenant_id,更重要的是防止漏拼导致的越权数据访问。

配置方式就是往MybatisPlusInterceptor里再加一个拦截器:

TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor(new TenantLineHandler() { @Override public Expression getTenantId() { return new LongValue(TenantContextHolder.get()); } @Override public String getTenantIdColumn() { return "tenant_id"; } });

这个功能用起来省事,但接入时要特别注意两点。第一,不是所有表都有 tenant_id,比如一些全局配置表就没有租户概念,这时要实现ignoreTable方法把这些表排除,否则插件会在查询这些表时自动添加一个不存在的 tenant_id 条件,导致查询报错。第二,多租户插件对 SQL 里的子查询和 join 处理比较激进,如果某条 SQL 结构复杂,插件自动解析时可能会出错,这类 SQL 建议直接从插件忽略列表里排除,自己手工在 XML 里控制条件。

4.3 代码生成器与团队开发节奏

微服务开发中,新增一张表就要建一个实体、一个 mapper、一个 service,这套流程如果纯手工做,每张表大约要花十分钟,而且容易漏掉注解。MyBatis-Plus 的代码生成器可以把这个时间压缩到一分钟以内。

最简单的用法是这样的:

FastAutoGenerator.create("jdbc:mysql://localhost:3306/order_service", "username", "password") .globalConfig(builder -> builder.author("team").outputDir(System.getProperty("user.dir") + "/src/main/java")) .packageConfig(builder -> builder.parent("com.example.order") .entity("entity") .mapper("mapper") .service("service") .serviceImpl("service.impl")) .strategyConfig(builder -> builder .addInclude("order_item", "order_log") .entityBuilder().enableLombok() .logicDeleteColumnName("deleted") .versionColumnName("version")) .execute();

代码生成器生成的质量取决于你的表设计。表里的注释写得清楚,生成到实体上的注释就清楚;表里没有注释,生成的代码就是一片空的 Javadoc,后面补起来更累。所以我们内部要求建表必须写字段注释,这算是一个让工具真正好用的前置条件。

生成的代码只是骨架,我从来不直接把它推到远端。生成后会先过一遍 review,把不需要的接口方法删掉,补上业务相关的自定义方法。生成器解决的是重复劳动的 90%,剩下 10% 的业务判断始终是人的工作。

5. 常见问题排查与避坑实录

5.1 条件构造器字段名和 SQL 关键字的坑

用条件构造器写查询时,字段名如果和 SQL 关键字撞车,比如order、status、describe,生成的 SQL 执行时就会报语法错误。订单服务里有一张表的字段就叫order,每次查询都要小心。

解决办法有两个方向:一是建表时尽量避免用关键字做字段名;二是如果表已经存在,就在实体字段上用 @TableField 指定反引号包裹的列名:

@TableField("`order`") private String order;

这个问题在 XML 手写 SQL 时也有,但更容易发现。条件构造器隐藏了 SQL 细节,反而更容易写出带着关键字字段的查询而不自知。我们后来加了一个习惯:新表设计 review 时,专门检查字段名是不是关键字。

5.2 微服务下的事务边界问题

MyBatis-Plus 本身不处理分布式事务,它只负责和本地数据库的交互。微服务里经常出现这样的流程:订单服务调库存服务扣库存,库存服务调支付服务发起支付,每个服务内部有各自的事务。如果中间某一步失败,本地事务各自回滚,但全局状态已经不一致了。

我们在这个项目里没有急着上分布式事务中间件,而是先用本地消息表和状态机来做最终一致。具体来说,订单服务里有一个事件表,订单状态变更时先写事件表,再发消息到消息队列,库存服务消费成功后回执确认,订单服务更新事件状态为已确认。这样即使某一个服务宕机,事件表里的记录还在,恢复后重新推送就能补齐。

如果确实要强一致性的场景,比如库存预占和订单创建必须同时成功,才需要引入分布式事务中间件。MyBatis-Plus 和 Seata 的 AT 模式配合得还可以,Seata AT 模式会自动解析 SQL 生成 undo 日志,但会有一层额外的性能开销。我的建议是能不引入就不引入,微服务事务首先要靠业务设计规避,靠中间件兜底通常是最后的手段。

5.3 深分页性能问题

列表接口数据量一大,深分页就暴露问题了。LIMIT 500000, 20这种写法,MySQL 需要先扫描前面 50 万条数据再丢弃,耗时随偏移量线性增长,接口响应会越来越慢。

我们遇到过一次线上问题:订单查询列表页翻到第 500 页,接口耗时从 50ms 飙到 2 秒,很多用户投诉页面转圈。排查后发现就是深分页导致的。当时最直接的优化是用子查询改写成基于 ID 的分页:

SELECT * FROM order_info WHERE id IN ( SELECT id FROM order_info WHERE user_id = ? ORDER BY create_time DESC LIMIT 500000, 20 )

或者更优的方案是改成游标分页,用上次查询返回的最后一条记录的 ID 作为下次查询的起点,也就是 “where id > ? order by id asc limit 20”。但这需要前端配合修改交互方式,从页码翻页改成“加载更多”的模式。这个改动牵扯面不小,我们最后是改成了基于 ID 的深分页方案,性能问题解决了,前端交互没动。

MyBatis-Plus 的 Page 对象本身不支持游标分页,需要自己写 mapper XML。这种场景下框架提供的便利不再是关键,SQL 本身的优化才是正路。

5.4 版本升级的兼容坑

任何一个项目运行一年后都会面临依赖升级的问题,MyBatis-Plus 也一样。我们有一次从 3.3.x 升到 3.5.x,全量回归时发现很多 SQL 里自动填充的字段不生效了。查了半天发现是新版本中MetaObjectHandler的填充策略有了变化,要求填充字段必须有 @TableField(fill = ...) 注解,而老版本里只要配置了全局的填充逻辑,实体字段没有注解也能填充。

这种兼容性调整不会写在升级日志的显眼位置,只能通过回归测试发现。所以升级 MyBatis-Plus 前,我强烈建议把分页、逻辑删除、自动填充、乐观锁这四个核心功能都列入回归用例,每一个都有自己的坑,升级影响面往往就藏在这些看似不起眼的功能里。

另外升级时注意新老 starter 的切换,Spring Boot 3.x 搭配的必须是mybatis-plus-spring-boot3-starter,这点前面提过,但值得再次强调。我有一次帮其他团队看问题,他们的服务升级 Spring Boot 3 之后一直启动失败,报错信息完全指向 MyBatis,最后排查发现就是引错了 starter,这类问题在升级季特别容易集中爆发。

6. 个人经验与后续扩展方向

6.1 我踩过几次坑之后养成的习惯

第一,每个服务里的 MyBatis-Plus 配置都抽到一个公共的 starter 模块里,所有服务引同一个版本,避免依赖漂移。你有多少服务就会遇到多少版本不一致问题,这种问题排查起来极其痛苦,因为报错各不相同。

第二,实体类上的注解一定写全,包括字段对应关系、逻辑删除标记、乐观锁版本字段。不要依赖全局配置覆盖一切。代码生成器生成的时候就把这些注解带上,保证每张表的行为是一致的。

第三,和 DBA 约定慢 SQL 日志要保留。MyBatis-Plus 生成的 SQL 和手写的一样,也要经过索引优化。微服务环境下有的 SQL 看着短,实际执行计划不对劲,没有慢 SQL 日志就等于盲人摸象。

6.2 这个内容后续还可以怎么扩展

MyBatis-Plus 解决了数据访问层的便利性问题,但数据库访问只是微服务技术栈的一小块。我后续打算把几个方向补完:分库分表如何和 MyBatis-Plus 共存,比如中间件的选择与字段路由规则的实践;缓存层与 MyBatis-Plus 二级缓存的取舍问题,因为微服务场景下分布式缓存才是主流,MP 自带的一级缓存反而容易踩脏读的坑;还有是我们目前还没强烈需要的,就是慢查询自动分析与 SQL 审计,这块如果服务规模再翻一倍,就会变成刚需。

不过这些都要等当前的服务拆得再细一点再说。至少目前这个阶段,MyBatis-Plus 已经帮我们把最琐碎的那部分工作消化掉了,剩下的时间可以用来多关注业务本身。数据访问层的技术选型从来不是越多花样越好,只要团队用得顺手、监控看得清、出了问题能快速定位,这个选择就是合适的。

最后分享一个实在的建议:如果你的团队正准备把单体服务拆成微服务,数据访问层先不要急着上各种“重量级”框架,优先保证三个能力——代码生成效率、SQL 可控性、监控可见性。这三点在 MyBatis-Plus 里都能通过简单的配置和约定实现,等团队跑顺了再往高级功能方向演进,路会好走很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 2:14:14

数据库课程设计完整指南:从ER图到存储过程与答辩自查

简介&#xff1a;一套完整的数据库系统原理课程设计报告&#xff0c;以批发企业信息管理系统为实际案例&#xff0c;适合正在完成数据库课程设计的高校学生参考。报告完整梳理了订单、供应商、零售商、商品及商品类型等核心实体的需求分析&#xff0c;给出了概念模型中的E-R图与…

作者头像 李华
网站建设 2026/10/23 19:39:42

SpringBoot+Vue企业数据资产登记系统实战:从设计到部署

1. 从"数据管理"到"数据资产化"&#xff1a;这个系统到底在解决什么先说一个我自己做项目的感受&#xff1a;做了这么多年后台管理系统&#xff0c;大多数项目本质上是"换皮增删改查"&#xff0c;但这个基于SpringBootVue的企业数据资产登记系统…

作者头像 李华
网站建设 2026/10/24 0:10:26

5G SA问题优化实战:从架构差异到接入、移动性与速率排查指南

简介&#xff1a;面向5G网络优化工程师的《5G SA问题优化指导书》共1个docx文件&#xff0c;压缩包约8.99MB。文档聚焦SA规模放号后低接入率问题的定位与优化&#xff0c;先定义无线接通率KPI公式&#xff0c;明确RRC建立成功率、QosFlow建立成功率和NG信令连接成功率的组成&am…

作者头像 李华
网站建设 2026/10/23 16:40:19

Unity WebGL平台AssetBundle加载实战指南

1. 项目概述&#xff1a;为什么WebGL下的AssetBundle加载是个“烫手山芋”Unity部署到WebGL平台这件事&#xff0c;干过的人心里都有数——它不像打包成Windows或Android那样直来直去。你点下Build&#xff0c;等几分钟&#xff0c;出来一个build文件夹&#xff0c;里面一堆js、…

作者头像 李华
网站建设 2026/10/24 11:34:14

LACP配置失效根因解析:系统优先级与Oper Key匹配

1. 为什么LACP配置不能靠“背命令”——从实验室故障复盘说起去年在某高校网络实验室带训时&#xff0c;遇到一个典型场景&#xff1a;A同学按教材步骤敲完interface Eth-Trunk1、mode lacp、trunkport GigabitEthernet0/0/1 to 0/0/3三行命令&#xff0c;端口状态却始终显示Un…

作者头像 李华
网站建设 2026/10/25 6:28:23

drm常见知识整理

在drm框架内&#xff0c;显示的一般流程如下&#xff1a; 内存 (Framebuffer) → Plane (图层配置与合成) → CRTC (时序生成与扫描输出) → Encoder → Connector → 显示器 plane 代表一个图像源。在扫描输出过程中可与crtc混合或叠加在其上方。 其从drm_framebuffer对象获取…

作者头像 李华