一、主键生成策略:解决分布式 ID 问题

1.1 核心注解:@TableId

在实体类主键字段上添加 @TableId 注解,通过 type 属性指定主键生成策略,支持局部配置(优先级高)全局配置

java

运行

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
public class User {
    // 局部配置:指定主键策略为数据库自增
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

1.2 常用主键策略(IdType 枚举)

IdType 值 描述 适用场景
AUTO 数据库 ID 自增 单库单表,MySQL 等支持自增
INPUT 手动输入主键值 主键由业务层自定义生成
ASSIGN_ID 雪花算法生成全局唯一 Long 型 ID(MP 默认) 分布式系统,需全局唯一 ID
ASSIGN_UUID 生成 32 位 UUID 字符串(无中划线) 分布式系统,主键为字符串类型
NONE 未设置,跟随全局配置 统一使用项目全局策略
重点:雪花算法(ASSIGN_ID)
  • 结构:64 位 Long 型 ID(1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位流水号);
  • 优势:全局唯一、有序递增、性能高,支持 1024 台机器,每台每毫秒生成 4096 个 ID,可用约 69 年;
  • 数据库要求:主键字段为 BIGINT 类型即可,无需自增。

1.3 全局配置(application.properties)

统一配置所有实体类的默认主键策略,无需每个实体类单独配置:

# 全局配置雪花算法(推荐分布式场景)
mybatis-plus:
  global-config:
    db-config:
      id-type: assign_id

# 若全局配置数据库自增
# id-type: auto

1.4 实战示例

策略 配置方式 数据库要求 插入数据注意事项
数据库自增 @TableId(type = IdType.AUTO) 主键字段需设置 AUTO_INCREMENT 无需手动 set 主键,MP 自动回填自增 ID
雪花算法 @TableId(type = IdType.ASSIGN_ID) 主键为 BIGINT 类型 无需手动 set 主键,MP 自动生成并回填
手动输入 @TableId(type = IdType.INPUT) 必须手动 set 主键值,否则报主键为空错误

// 手动输入主键示例
@Test
public void insertInput() {
    User user = new User();
    user.setId(7L); // 手动设置主键
    user.setName("李四");
    user.setAge(18);
    user.setEmail("lisi@qq.com");
    userMapper.insert(user); // 插入数据
}

二、条件构造器 Wrapper:实现复杂条件查询

2.1 核心构造器:QueryWrapper

用于构建 SELECT/DELETE/UPDATE 的条件,通过链式调用拼接条件,无需手写 SQL,支持 Lambda 表达式避免字段名写错。

2.2 常用条件方法(对应 SQL)

方法 描述 对应 SQL
eq(column, val) 等于 column = val
ne(column, val) 不等于 column != val
gt/ge(column, val) 大于 / 大于等于 column > val / >= val
lt/le(column, val) 小于 / 小于等于 column < val / <= val
between(c, v1, v2) 区间查询 column between v1 and v2
like(column, val) 模糊查询 column like %val%
likeRight(column, val) 左模糊 column like val%
isNotNull(column) 字段不为空 column is not null
inSql(column, sql) 子查询 column in (sql 语句)
orderByDesc(column) 按字段倒序 order by column desc

2.3 多场景实战示例

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;

@SpringBootTest
public class WrapperTest {
    @Autowired
    private UserMapper userMapper;

    // 示例1:姓名/邮箱不为空,年龄>=12
    @Test
    void query1() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.isNotNull("name")
               .isNotNull("email")
               .ge("age", 12);
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

    // 示例2:查询姓名为「李四」的单个用户(注意:结果超过1条会报错)
    @Test
    void query2() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("name", "李四");
        User user = userMapper.selectOne(wrapper);
        System.out.println(user);
    }

    // 示例3:统计年龄20~30之间的用户数量
    @Test
    void query3() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.between("age", 20, 30);
        Integer count = userMapper.selectCount(wrapper);
        System.out.println("符合条件数量:" + count);
    }

    // 示例4:模糊查询(姓名不含e,邮箱以t开头)
    @Test
    void query4() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.notLike("name", "e")
               .likeRight("email", "t");
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

    // 示例5:子查询(id >= 2)
    @Test
    void query5() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.inSql("id", "select id from user where id>=2");
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

    // 示例6:按id倒序查询
    @Test
    void query6() {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc("id");
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }
}

三、内置分页插件:实现物理分页

3.1 步骤 1:配置分页拦截器

创建 MP 配置类,注入分页拦截器并指定数据库类型:

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.core.toolkit.DbType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页拦截器,指定MySQL数据库
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

3.2 步骤 2:使用 Page 对象实现分页查询

核心是 Page<T> 对象,传入 selectPage 方法后自动返回分页结果(包含数据、总条数、总页码等)。

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;

@SpringBootTest
public class PageTest {
    @Autowired
    private UserMapper userMapper;

    // 分页查询:第2页,每页3条数据
    @Test
    void pageQuery() {
        // 创建Page对象:参数1=当前页码,参数2=每页条数
        Page<User> page = new Page<>(2, 3);
        // selectPage:参数1=Page对象,参数2=条件构造器(null=无条件)
        IPage<User> userPage = userMapper.selectPage(page, null);

        // 获取分页结果
        List<User> records = userPage.getRecords(); // 当前页数据
        long total = userPage.getTotal(); // 总记录数
        long current = userPage.getCurrent(); // 当前页码
        long pages = userPage.getPages(); // 总页码
        boolean hasNext = userPage.hasNext(); // 是否有下一页
        boolean hasPrev = userPage.hasPrevious(); // 是否有上一页

        // 输出结果
        System.out.println("当前页数据:");
        records.forEach(System.out::println);
        System.out.println("总记录数:" + total + ",当前页:" + current + ",总页码:" + pages);
        System.out.println("是否有下一页:" + hasNext + ",是否有上一页:" + hasPrev);
    }
}

3.3 分页结果核心方法(IPage<T>)

表格

方法 功能
getRecords() 获取当前页数据列表
getTotal() 获取总记录数
getCurrent() 获取当前页码
getPages() 获取总页码
hasNext() 是否有下一页
hasPrevious() 是否有上一页

四、逻辑删除:防止误删,数据可恢复

4.1 核心原理

  • 数据库添加逻辑删除字段(如 deleted),默认 0(未删除),删除时更新为 1(已删除);
  • 调用 MP 删除方法时,自动转为更新操作;
  • 调用查询方法时,自动过滤 deleted=1 的数据。

4.2 实战步骤

步骤 1:数据库添加逻辑删除字段

sql

-- 给user表添加deleted字段,默认0(未删除)
ALTER TABLE user ADD COLUMN deleted INT DEFAULT 0 COMMENT '逻辑删除:0=未删除,1=已删除';
步骤 2:实体类添加 @TableLogic 注解

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;

@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableLogic // 标识为逻辑删除字段
    private Integer deleted;
}
步骤 3:自定义逻辑删除值(可选)

properties

mybatis-plus:
  global-config:
    db-config:
      logic-not-delete-value: 0 # 未删除值(默认0)
      logic-delete-value: 1     # 已删除值(默认1)
步骤 4:测试逻辑删除

@Test
void logicDelete() {
    // 调用deleteById,实际执行UPDATE操作
    userMapper.deleteById(1L);
}

执行结果(SQL)

sql

UPDATE user SET deleted=1 WHERE id=? AND deleted=0

五、乐观锁:解决并发更新冲突

5.1 核心概念

表格

锁类型 核心思想 适用场景
乐观锁 无锁,更新时判断版本号 读多写少
悲观锁 操作前加锁,阻塞其他线程 写多读少

MP 乐观锁原理:通过版本号(version)字段控制,更新时判断版本号是否一致,一致则更新(版本号 + 1),不一致则更新失败。

5.2 实战步骤(商品价格并发更新)

步骤 1:创建商品表并插入测试数据

sql

CREATE TABLE product (
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称',
    price INT(11) DEFAULT 0 COMMENT '价格',
    version INT(11) DEFAULT 0 COMMENT '乐观锁版本号',
    PRIMARY KEY (id)
);
-- 插入测试数据:外星人笔记本,价格1000,版本号0
INSERT INTO product (id, name, price) VALUES (1, '外星人笔记本', 1000);
步骤 2:商品实体类添加 @Version 注解

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;

@Data
public class Product {
    @TableId
    private Long id;
    private String name;
    private Integer price;
    @Version // 标识为乐观锁版本号字段
    private Integer version;
}
步骤 3:配置乐观锁拦截器

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.core.toolkit.DbType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页拦截器
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 乐观锁拦截器
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
步骤 4:编写商品 Mapper 接口

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mp.pojo.Product;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
步骤 5:模拟并发更新测试
无锁测试(数据覆盖)

@Test
void testProductNoLock() {
    // 1.小李获取商品价格
    Product productLi = productMapper.selectById(1);
    System.out.println("小李获取的价格:" + productLi.getPrice()); // 1000
    // 2.小王获取商品价格
    Product productWang = productMapper.selectById(1);
    System.out.println("小王获取的价格:" + productWang.getPrice()); // 1000
    // 3.小李加500并更新
    productLi.setPrice(productLi.getPrice() + 500);
    productMapper.updateById(productLi); // 价格1500,版本号仍0
    // 4.小王减300并更新(覆盖数据)
    productWang.setPrice(productWang.getPrice() - 300);
    productMapper.updateById(productWang); // 价格700
    // 5.老板查询
    Product productBoss = productMapper.selectById(1);
    System.out.println("老板看到的价格:" + productBoss.getPrice()); // 700(错误)
}
乐观锁测试(避免数据覆盖)

@Test
void testProductWithLock() {
    // 1.小李获取商品价格
    Product productLi = productMapper.selectById(1);
    System.out.println("小李获取的价格:" + productLi.getPrice()); // 1000
    // 2.小王获取商品价格
    Product productWang = productMapper.selectById(1);
    System.out.println("小王获取的价格:" + productWang.getPrice()); // 1000
    // 3.小李加500并更新(成功,版本0→1)
    productLi.setPrice(productLi.getPrice() + 500);
    productMapper.updateById(productLi);
    // 4.小王减300并更新,失败后重试
    productWang.setPrice(productWang.getPrice() - 300);
    int result = productMapper.updateById(productWang);
    if (result == 0) {
        // 更新失败,重新查询最新数据
        Product productNew = productMapper.selectById(1);
        // 基于最新数据更新
        productNew.setPrice(productNew.getPrice() - 300);
        productMapper.updateById(productNew); // 价格1500→1200
    }
    // 5.老板查询
    Product productBoss = productMapper.selectById(1);
    System.out.println("老板看到的价格:" + productBoss.getPrice()); // 1200(正确)
}

六、总结

核心要点

  1. 主键策略:雪花算法(ASSIGN_ID)是分布式系统首选,通过 @TableId 局部配置或全局配置生效;
  2. 条件构造器:QueryWrapper 链式调用实现复杂条件查询,覆盖所有单表查询场景,无需手写 SQL;
  3. 物理分页:配置分页拦截器后,通过 Page 对象一键实现分页,自动封装分页结果;
  4. 逻辑删除@TableLogic 标识删除字段,将删除转为更新,防止数据误删且可恢复;
  5. 乐观锁@Version 标识版本号字段,解决读多写少场景的并发更新冲突,更新失败可重试。

进阶方向

  1. 代码生成器:自动生成实体类、Mapper、Service、Controller 代码;
  2. 通用 Service:继承 IService/ServiceImpl,封装更上层的业务逻辑;
  3. 性能分析插件:监控 SQL 执行时间,定位性能瓶颈;
  4. 动态表名插件:实现多租户、分表等场景的动态表名切换。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐