MyBatis-Plus 使用笔记
本项目持久层全面使用 MyBatis-Plus(3.5.5,Spring Boot 3 starter)。本文结合真实代码讲 7 个点:雪花主键、逻辑删除、分页插件、公共字段自动填充、条件构造器、ServiceImpl/BaseMapper、手写乐观锁扣库存,最后是代码生成器。
一、整体用法:Mapper + Service 双层基类
每个表三件套:
// 1.实体:@TableName 绑定表,@TableId 指定主键策略
@Data
@TableName("seckill_goods")
public class SeckillGoods implements Serializable {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
...
}
// 2.Mapper:继承 BaseMapper,白拿 insert/deleteById/updateById/selectById/selectList/selectPage...
public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> {}
// 3.Service:接口继承 IService,实现类继承 ServiceImpl,又白拿 save/saveBatch/list/page/removeByIds...
public interface ISeckillService extends IService<SeckillGoods> {}
@Service
public class SeckillServiceImpl
extends ServiceImpl<SeckillGoodsMapper, SeckillGoods>
implements ISeckillService {}
启动类 @MapperScan("com.situ.mall.mapper") 统一扫描 Mapper 接口注册成 Bean,不用每个接口写 @Mapper。所以像 productService.updateById(product)、orderMapper.selectById(orderNo) 这些方法虽然代码里没写 SQL,但都真实可用。
复杂查询就在 Mapper 上写注解 SQL 或 XML(本项目主要用注解):
// ProductMapper:库存乐观锁扣减/回补
@Update("update product set stock = stock - #{count} where id = #{id} and stock >= #{count} and deleted = 0")
int deductStock(@Param("id") Long id, @Param("count") Integer count);
@Update("update product set stock = stock + #{count} where id = #{id} and deleted = 0")
int addStock(@Param("id") Long id, @Param("count") Integer count);
二、主键策略:雪花算法 ASSIGN_ID
实体上 @TableId(type = IdType.ASSIGN_ID),插入时由 MyBatis-Plus 用雪花算法生成 Long 型全局唯一 id,不依赖数据库自增。
// Order 的主键是订单号 order_no,同样用雪花
@TableId(value = "order_no", type = IdType.ASSIGN_ID)
private Long orderNo;
秒杀里甚至直接用 MP 的工具类提前生成 id:
long orderNo = IdWorker.getId(); // 先拿到雪花订单号,写进 MQ 消息,消费端再落库
为什么分布式项目偏爱雪花 id:
- 全局唯一、趋势递增:多实例、多分库同时插入也不冲突,且对 InnoDB 聚簇索引友好;
- 不暴露业务量:不像自增 id 那样让人一眼看出你有多少订单;
- 入库前就能拿到 id:可以先把 orderNo 写进 Redis/MQ,再异步落库(秒杀正是这么做的)。
雪花结构:1 位符号 + 41 位时间戳 + 10 位机器位 + 12 位序列号。注意要保证服务器时钟不回拨。
其他策略:
AUTO(数据库自增)、INPUT(自己 set)、UUID(字符串无序,索引不友好)。
三、逻辑删除 @TableLogic
商品、分类、订单等实体都有一个 deleted 字段并标 @TableLogic:
@TableLogic
private Integer deleted;
全局也配了 mybatis-plus.global-config.db-config.logic-delete-field: deleted。
开启后,MP 会自动改写 SQL:
deleteById(id)→UPDATE ... SET deleted=1 WHERE id=?- 所有查询自动追加
AND deleted=0(所以手写的deductStock里也要手动带上and deleted = 0)
好处:删除可恢复、有审计痕迹、外键数据不会悬空。deleted=1 的数据默认查不出来,等价于”回收站”。
四、分页插件(不配不生效!)
MP 的 selectPage 要靠插件才能真正分页,否则会把全表查出来。配置在 mall-common/.../config/MybatisPlusConfig.java:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 方言=MySQL
return interceptor;
}
}
用法(商品后台分页):
IPage<Product> page = new Page<>(query.getPage(), query.getLimit()); // 当前页、每页条数
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
.like(hasName, Product::getName, query.getName())
.eq(hasCategory, Product::getCategoryId, query.getCategoryId())
.orderByDesc(Product::getCreateTime);
productMapper.selectPage(page, wrapper);
long total = page.getTotal(); // 插件自动执行的 count 查询
List<Product> records = page.getRecords();
插件自动做两件事:把查询拼成 LIMIT,并额外执行一次 COUNT 得到总条数回填到 page。
五、公共字段自动填充(createTime/updateTime)
每张表都有 create_time、update_time,实体上声明填充时机:
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
再写一个 mall-common/.../config/MyMetaObjectHandler.java:
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", new Date(), metaObject); // 新增:两个时间都填
this.setFieldValByName("updateTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject); // 更新:只刷修改时间
}
}
这样业务代码里再也不用手动 setCreateTime/setUpdateTime,insert/update 时 MP 自动回调填入,统一且不会漏。
六、条件构造器(项目高频)
三种 Wrapper:
// LambdaQueryWrapper:方法引用,字段名重构安全(最常用)
new LambdaQueryWrapper<Order>()
.eq(OrderItem::getOrderNo, orderNo)
.orderByDesc(Order::getCreateTime);
// QueryWrapper:手写列名
new QueryWrapper<Admin>().eq("name", admin.getName());
// UpdateWrapper:只更新某些列(订单超时关单)
UpdateWrapper<Order> uw = new UpdateWrapper<>();
uw.eq("order_no", orderNo).set("status", OrderStatus.CANCELLED.getCode());
orderMapper.update(uw);
常用条件方法:eq / ne / like / between / ge / le / in / isNull / orderByAsc/Desc,且都带条件重载——第一个 boolean 为 true 才拼接该条件,是动态查询的关键:
.like(!ObjectUtils.isEmpty(name), Product::getName, name) // name 为空就不拼 LIKE
商品分页、购物车按用户+商品查同款、订单列表都大量使用。
七、手写”乐观锁”扣库存(注意不是 @Version)
MP 提供 @Version 乐观锁插件,但本项目没有用它,而是直接用一条带条件的 UPDATE 实现乐观思想:
@Update("update product set stock = stock - #{count} where id = #{id} and stock >= #{count} and deleted = 0")
int deductStock(@Param("id") Long id, @Param("count") Integer count);
- 不加
select ... for update行锁,而是在WHERE里要求stock >= count; - 返回影响行数:=1 扣减成功,=0 说明库存不足(并发下被别人抢光);
- Service 据此返回 boolean:
return productMapper.deductStock(id, count) > 0;
这是秒杀防超卖的最后一道防线(Redis Lua 预减之后,落库时再兜底一次),性能优于悲观锁。
八、代码生成器 MPGenerator
mall-common/.../MPGenerator.java 是个带 main 方法的独立工具(不属于应用启动流程)。连上数据库后按表自动生成 entity/mapper/service/controller 全套模板代码:
DataSourceConfig ...("jdbc:mysql://localhost:3306/new-mall","root","123456");
globalConfigBuilder.outputDir(projectPath + "/mall-common/src/main/java").author("dxk");
packageConfigBuilder.parent("com.situ.mall").entity("pojo");
strategyConfigBuilder.addInclude("oper_log".split(","));
strategyConfigBuilder.entityBuilder()
.idType(IdType.ASSIGN_ID) // 雪花主键
.naming(NamingStrategy.underline_to_camel) // 下划线转驼峰
.enableLombok() // 实体加 @Data
.logicDeleteColumnName("deleted"); // 识别逻辑删除字段
// 再配 create_time 的 INSERT 填充、update_time 的 INSERT_UPDATE 填充
改了表结构改一下 dbTables 表名重跑即可,省掉重复 CRUD 样板。
九、面试一句话总结
持久层用 MyBatis-Plus:Mapper 继承 BaseMapper、Service 继承 ServiceImpl 免写单表 CRUD;主键用雪花算法 ASSIGN_ID 保证分布式唯一且入库前可取;@TableLogic 逻辑删除;分页插件 + LambdaQueryWrapper 做动态分页查询;MetaObjectHandler 自动填充创建/修改时间;秒杀库存不用 @Version,而是手写
update ... where stock>=n凭影响行数判断成败的乐观锁做最终防超卖;新表用 MPGenerator 一键生成骨架。