MyBatis-Plus 使用笔记

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 一键生成骨架。

上一篇
下一篇