MyBatis-Plus 从零到实战:一篇真正写给新手的完整指南

一、MyBatis-Plus 到底解决了什么问题?

在没有 MyBatis-Plus 之前,我们通常这样写 MyBatis:

  • 一个表一个 Mapper.xml

  • insert / update / selectById 都要自己写 SQL

  • 动态条件靠 <if><where>

结果就是:

  • CRUD SQL 大量重复

  • Mapper 层越来越臃肿

  • 新人很难快速上手

👉 MyBatis-Plus 的定位非常清晰

不改变 MyBatis 使用方式 的前提下,
封装 80% 的单表 CRUD 场景

它不是 ORM,也不会替你写业务 SQL,它解决的是:

重复、机械、容易出错的那部分数据库操作。


二、实体类与基础注解(一定要打牢)

2.1 示例表结构(贯穿全文)

CREATE TABLE t_user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50),
  age INT,
  status TINYINT,
  is_deleted TINYINT DEFAULT 0
);

2.2 @TableName:表名映射

@Data
@TableName("t_user")
public class User {
    private Long id;
    private String username;
    private Integer age;
    private Integer status;
}

👉 什么时候必须写 @TableName

  • 表名和类名不一致(t_user vs User

  • 表有前缀(t_ / sys_

不写就会出现:

Table 'xxx.user' doesn't exist


2.3 @TableId:主键映射(新手必踩坑)

@TableId(type = IdType.AUTO)
private Long id;

常见 IdType

  • AUTO:数据库自增
  • ASSIGN_ID:MP 雪花算法(推荐新项目)

insert 后 id 为 null?

👉 90% 原因:没写 @TableId 或类型不匹配


2.4 @TableField:字段映射与排除

数据库字段 ≠ Java 字段
@TableField("is_deleted")
private Integer deleted;

👉 原则一句话:

Java 命名服务语义,数据库命名服务历史。


排除非表字段
@TableField(exist = false)
private String roleName; // join 查询字段

不加 exist = false,SQL 会直接报错。

2.5 BaseEntity 封装统一字段(待补充)

id、is_deleted、create_time、update_time

在使用 MyBatis-Plus 时,将实体类(Entity)抽取出一个基类(BaseEntity)是为了实现统一字段管理,使代码更加简洁、易维护。具体的好处包括:

统一字段管理

BaseEntity 通常包含一些通用的字段,比如:

  • create_time:记录实体的创建时间。
  • update_time:记录实体的更新时间。
  • deleted:逻辑删除标志字段。

这些字段在大多数表中都可能出现,特别是在支持 逻辑删除审计字段(创建时间、更新时间) 的场景中。如果每个实体类都需要单独定义这些字段,会导致代码重复,维护起来也很麻烦。

减少代码重复

通过抽取 BaseEntity 类,可以将这些通用字段统一放在基类中,不需要每个实体类都重复定义。例如:

public class BaseEntity {
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private Boolean deleted;
}

然后,其他的实体类继承 BaseEntity,就可以直接拥有这些字段:

public class Question extends BaseEntity {
    private Long id;
    private String title;
    private String content;
}

减少冗余的数据库操作

在 MyBatis-Plus 中,使用基类(BaseEntity)后,可以借助 MyBatis-Plus 提供的自动填充(@TableField)功能,自动对字段(如 create_timeupdate_time)进行填充,减少了手动设置的工作量。

例如,使用 MyBatis-Plus 的 自动填充字段

public class BaseEntity {
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @TableLogic
    private Boolean deleted;
}
  • @TableField(fill = FieldFill.INSERT):表示 createTime 字段在插入时自动填充。
  • @TableField(fill = FieldFill.INSERT_UPDATE):表示 updateTime 字段在插入和更新时自动填充。
  • @TableLogic:表示启用逻辑删除,自动处理 deleted 字段。

简化增删改查操作

BaseEntity 还可以配合 MyBatis-Plus 的通用 Mapper(如 BaseMapper)来减少一些通用操作的重复代码,例如:

  • 插入数据时自动填充 create_timeupdate_time 字段。
  • 删除数据时自动更新 deleted 字段(逻辑删除)。

通过继承 BaseEntity,实体类就自动具有了这些通用字段,且 BaseMapper 会自动处理。

增强可维护性

如果需要修改这些通用字段(例如,修改时间戳字段的类型或处理逻辑删除的方式),只需要在 BaseEntity 中进行一次修改,而不需要修改每个实体类。这样可以减少维护成本,提升系统的可扩展性。

自动注入公共字段

在一些开发框架或项目中,BaseEntity 还可以配合 AOP事件监听器 自动填充公共字段。例如,可以在用户登录时,自动注入当前登录用户的 ID 作为 create_byupdate_by 字段。

自动填充时间字段
/**
 * 处理器,处理更新时间和插入时间
 */
@Component
public class MybatisPlusDateFillHandler 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());
    }

}
小结:
  • BaseEntity 让通用字段(如 create_timeupdate_timedeleted)的管理变得更加规范、统一,减少了代码重复,提高了开发效率。
  • MyBatis-Plus 提供了 自动填充逻辑删除 的功能,可以使这部分字段自动管理,减少了开发中的手动操作。

因此,将实体类抽取出 BaseEntity 类,是 MyBatis-Plus 提供的一种提升开发效率和代码可维护性的最佳实践。


三、Mapper 层:BaseMapper 的能力、边界与常用方法

3.1 什么是 BaseMapper?

public interface UserMapper extends BaseMapper<User> {
}

只要继承 BaseMapper,你就自动拥有一套单表通用 CRUD 能力,无需 XML、无需手写 SQL。

本质一句话:

BaseMapper 解决的是:如何“操作一张表”。


3.2 BaseMapper 常用方法(你每天都会用)

1️⃣ 新增
int insert(T entity);
  • 插入非空字段
  • 主键是否回填,取决于 @TableId

示例代码:

@Test  
public void test7(){  
    Student student = new Student();  
    student.setStudentNo("11");  
    student.setName("李11");  
    int insert = studentMapper.insert(student);  
    System.out.println(insert);  
    // 获取id  
    System.out.println("id = " + student.getId());  
}

2️⃣ 根据主键查询
T selectById(Serializable id);
  • 只适合主键查询

  • 条件固定为 where id = ?

示例代码:

@Test  
public void test8(){  
    Student student = studentMapper.selectById(1L);  
    System.out.println(student);  
}

3️⃣ 条件查询(最常用)
List<T> selectList(Wrapper<T> wrapper);
@Test  
public void test9(){  
    LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();  
    queryWrapper.eq(Student::getGender,1);  
    
    List<Student> students = studentMapper.selectList(queryWrapper);  
    students.forEach(System.out::println);  
}

⚠️ wrapper = null 等价于 全表查询


4️⃣ 更新
int updateById(T entity);
  • 只更新非空字段

  • where 条件只能是主键

示例代码:

@Test  
public void test10(){  
    Student student = new Student();  
    student.setId(5L);  
    student.setEmail("111@qq.com");  
    int i = studentMapper.updateById(student);  
    System.out.println(i);  
}

5️⃣ 条件更新
int update(T entity, Wrapper<T> updateWrapper);
  • 示例代码:
@Test  
public void test11(){  
    LambdaUpdateWrapper<Student> updateWrapper = new LambdaUpdateWrapper<>();  
    updateWrapper.set(Student::getEmail,"222@qq.com");  
    updateWrapper.eq(Student::getId,1);  
    int i = studentMapper.update(updateWrapper);  
    System.out.println(i);  
}

6️⃣ 删除(逻辑删除生效)
int deleteById(Serializable id);
  • 开启 @TableLogic
  • 实际执行的是 update
  • 未开启,则物理删除。

示例代码:

@Test  
public void test12(){  
    int i = studentMapper.deleteById(11);  
    System.out.println(i);  
}

3.3 BaseMapper 的能力边界

✅ 适合:

  • 单表操作

  • 返回 Entity

  • 条件不复杂

❌ 不适合:

  • 多表 join

  • VO / DTO 返回

  • 聚合统计


3.2 BaseMapper 适合 & 不适合

✅ 适合:

  • 单表

  • 返回 Entity

  • 条件不复杂

❌ 不适合:

  • 多表 join

  • VO / DTO

  • 聚合统计


3.3 自定义 Mapper(含分页)

IPage<UserVO> selectUserPage(Page<UserVO> page,
                            @Param("username") String username);
<select id="selectUserPage" resultType="UserVO">
  SELECT id, username, age
  FROM t_user
  WHERE is_deleted = 0
</select>

📌 不要写 limit,分页插件会自动加。


四、Service 层:业务边界与常用方法

一句非常实用的经验法则:

Controller → 只调 Service
Service → 优先用 Service 自带方法,不够再调 Mapper

4.1 为什么 Service 要继承 ServiceImpl?

@Service
public class UserServiceImpl
    extends ServiceImpl<UserMapper, User>
    implements UserService {
}

ServiceImpl 本质是:

对 BaseMapper 的一层通用业务封装

它帮你屏蔽了:

  • 注入 Mapper

  • 重复 CRUD 代码


4.2 Service 层常用方法(高频)

1️⃣ 保存
boolean save(T entity);
boolean saveOrUpdate(T entity);
  • 示例代码
@Test  
public void test2(){  
    Student student = new Student();  
    student.setGender(1);  
    student.setName("赵六");  
    student.setStudentNo("123456");  
  
    boolean save = studentService.save(student);  
    System.out.println(save);  
}
  • 保存或更新,根据主键ID进行判断,有则是更新没有则是保存。
@Test  
public void test13(){  
    Student student = new Student();  
    student.setGender(1);  
    student.setStudentNo("13");  
    student.setName("周13");  
    boolean b = studentService.saveOrUpdate(student);  
    System.out.println("b = " + b);  
}

2️⃣ 批量保存
boolean saveBatch(Collection<T> list);
  • 示例代码
@Test  
public void test3(){  
    Student student = new Student();  
    student.setName("赵六");  
    student.setStudentNo("1234566");  
  
    Student student1 = new Student();  
    student1.setName("钱7");  
    student1.setStudentNo("1234567");  
  
    Student student2 = new Student();  
    student2.setName("孙8");  
    student2.setStudentNo("12345678");  
  
    List<Student> list = List.of(student, student1, student2);  
  
    boolean save = studentService.saveBatch(list);  
    System.out.println(save);  
}

3️⃣ 根据 ID 查询
T getById(Serializable id);
  • 示例代码:
@SpringBootTest(classes = MybatisPlusMain.class)  
public class StudentTest {  
  
    @Autowired  
    private StudentService studentService;  
  
    @Test  
    public void test(){  
        Student byId = studentService.getById(1L);  
        System.out.println("byId = " + byId);  
    }  
}

4️⃣ 条件查询
List<T> list(Wrapper<T> wrapper);
@Test  
public void test1(){  
    LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();  
    queryWrapper.eq(Student::getGender,1);  
    
    List<Student> list = studentService.list(queryWrapper);  
    list.forEach(System.out::println);  
}

5️⃣ 更新
boolean updateById(T entity);
boolean update(Wrapper<T> wrapper);
  • 示例代码
@Test  
public void test4(){  
    Student student = new Student();  
    student.setId(4L);  
    student.setName("赵六1");  
    studentService.updateById(student);  
}  
  
@Test  
public void test5(){  
    LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();  
    wrapper.set(Student::getName,"赵六2");  
    wrapper.eq(Student::getId,11);  
    boolean update = studentService.update(wrapper);  
    System.out.println(update);  
}

6️⃣ 删除
boolean removeById(Serializable id);
boolean remove(Wrapper<T> wrapper);
  • 示例代码:
@Test  
public void test6(){  
    boolean b = studentService.removeById(13L);  
    System.out.println(b);  
  
    LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();  
    queryWrapper.eq(Student::getId,12L);  
    boolean remove = studentService.remove(queryWrapper);  
    System.out.println(remove);  
}

4.3 Service 层应该做什么?

  • 组合多个 Mapper

  • 控制事务

  • 屏蔽数据库结构

❌ 不要:

  • Controller 直接调 Mapper

  • Service 返回 Entity 给前端


4.4 一个推荐的 Service 写法示例

public List<UserVO> listActiveUsers() {
    return this.list(
        new LambdaQueryWrapper<User>()
            .eq(User::getStatus, 1)
    ).stream()
     .map(this::convertToVO)
     .toList();
}

这里体现了三点:

  • Wrapper 只存在于 Service

  • Controller 无感知 SQL

  • 返回的是 VO


4.2 Service 层应该做什么?

  • 组合多个 Mapper

  • 控制事务

  • 屏蔽表结构

❌ 不要:

  • Controller 直接调 Mapper

  • Service 返回 Entity 给前端


4.3 Service 返回 Entity 还是 VO?

public UserVO getUser(Long id) {
    User user = getById(id);
    return convertToVO(user);
}

👉 对外返回 VO / DTO,长期更安全。


五、Wrapper 条件构造器(QueryWrapper / LambdaQueryWrapper)

在 MyBatis-Plus 中,Wrapper 是整个查询体系的核心

很多新手第一次看到 Wrapper,都会被这些类名搞懵:

  • QueryWrapper

  • LambdaQueryWrapper

  • UpdateWrapper

  • LambdaUpdateWrapper

常见疑问:

  • 为什么要搞这么多 Wrapper?

  • QueryWrapper 和 LambdaQueryWrapper 到底有什么区别?

  • 我应该用哪个?

这一节我们只聚焦 最常用、最容易混淆的一对:QueryWrapper 与 LambdaQueryWrapper


5.1 QueryWrapper 与 LambdaQueryWrapper 的关系

先说结论:

LambdaQueryWrapper 不是新能力,而是 QueryWrapper 的“安全写法”。

它们的最终目的完全一致:

  • 构建 WHERE 条件

  • 拼接 SQL

  • 执行查询

例如,下面两段代码生成的 SQL 是一模一样的。

QueryWrapper 写法(字符串字段)
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", "Tom")
       .gt("age", 18);

List<User> users = userMapper.selectList(wrapper);
LambdaQueryWrapper 写法(方法引用)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, "Tom")
       .gt(User::getAge, 18);

List<User> users = userMapper.selectList(wrapper);

👉 SQL 层面没有任何区别。


5.2 为什么不推荐 QueryWrapper(字符串字段)

wrapper.eq("username", "Tom");

这行代码表面没问题,但隐藏了几个新手非常容易踩的坑。

❌ 1️⃣ 字段拼错,编译期发现不了
wrapper.eq("uesrname", "Tom"); // 拼错
  • 编译通过

  • 运行时报错或查不到数据


❌ 2️⃣ 字段重命名,代码不会报错

数据库字段修改后,所有字符串都需要人工排查。


❌ 3️⃣ 实体字段与数据库字段割裂
@TableField("user_name")
private String username;

Java 用 username,Wrapper 却写 "user_name",认知成本高。


5.3 LambdaQueryWrapper 的优势

LambdaQueryWrapper 用 方法引用代替字符串字段名

wrapper.eq(User::getUsername, "Tom");

优势总结:

  • ✅ 编译期安全

  • ✅ 支持 IDE 重构

  • ✅ 与实体语义完全一致


5.4 使用建议(直接记)

业务代码中,优先使用 LambdaQueryWrapper。

场景 推荐
日常业务 LambdaQueryWrapper
Service 层 LambdaQueryWrapper
临时调试 QueryWrapper

5.2 UpdateWrapper:更新条件

UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 1)
       .set("status", 0);

👉 Query = 查,Update = 改,职责清晰。


5.3 LambdaQueryWrapper(强烈推荐)

LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(User::getStatus, 1)
       .like(User::getUsername, "tom");

优势:

  • 编译期校验

  • 重构安全


5.4 condition:动态条件

wrapper.like(
    StringUtils.hasText(username),
    User::getUsername,
    username
);

告别 if + 拼 SQL。


六、逻辑删除:删除行为的改变

逻辑删除并不是 MyBatis-Plus 独有的概念,但 MP 把它做成了一个“默认行为”,这点对新手非常重要。


6.1 基于注解的逻辑删除(回顾)

@TableField("is_deleted")
@TableLogic(value = "0", delval = "1")
private Integer deleted;

当你执行:

userMapper.deleteById(1L);

实际执行的 SQL 是:

UPDATE t_user SET is_deleted = 1 WHERE id = ?;

并且:

  • 所有查询 SQL

  • 会自动加上 is_deleted = 0


6.2 全局配置逻辑删除(强烈推荐)

在真实项目中,不建议在每个实体上都写 @TableLogic 的 value / delval

更推荐的方式是:

application.yml 中做一次全局约定


6.3 application.yml 全局配置示例

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: is_deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

这段配置的含义是:

  • logic-delete-field

    • 默认的逻辑删除字段名
  • logic-delete-value

    • 表示“已删除”的值
  • logic-not-delete-value

    • 表示“未删除”的值

6.4 配置后,实体类可以极度简化

@TableField("is_deleted")
private Integer deleted;

甚至在字段名一致的情况下,可以 完全不写 @TableField

private Integer isDeleted;

MyBatis-Plus 会自动识别它是逻辑删除字段。


6.5 注解方式 vs 全局配置,如何选择?

方式 适合场景
@TableLogic 个别表逻辑删除规则不同
yml 全局配置 绝大多数业务项目(推荐)

6.6 逻辑删除的几个重要认知

  • ❌ 不要在 Wrapper 中手动加 is_deleted = 0

  • ❌ 不要把逻辑删除当成业务状态

  • ✅ 它只是 数据库层面的数据可见性规则

如果你把这一点想清楚,逻辑删除就不会再让你困惑。


七、分页:插件 + 正确使用方式

v3.5.9 起,PaginationInnerInterceptor 已分离出来。如需使用,则需单独引入 mybatis-plus-jsqlparser 依赖 , 具体请查看 安装 一章。

  • MybatisPlusv3.5.9开启将插件相关代码迁移到了mybatis-plus-jsqlparser依赖中,若要使用插件扩展,则需要安装相关依赖。

7.1 安装插件扩展依赖

<dependency>  
    <groupId>com.baomidou</groupId>  
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>  
    <version>3.5.15</version>  
</dependency>  
<!--  
    于 v3.5.9 起,PaginationInnerInterceptor 已分离出来。  
    如需使用,则需单独引入 mybatis-plus-jsqlparser 依赖  
-->  
<dependency>  
    <groupId>com.baomidou</groupId>  
    <artifactId>mybatis-plus-jsqlparser</artifactId>  
    <version>3.5.15</version>  
</dependency>

7.2 新增MybatisPlus配置类,添加分页插件

@Configuration  
public class MybatisPlusConfiguration {  
  
    @Bean  
    public MybatisPlusInterceptor mybatisPlusInterceptor() {  
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();  
        // 添加分页插件,指定数据库类型:mysql  
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));  
        return interceptor;  
    }  
  
}

7.3 Mapper 层

  • 接口
public interface StudentMapper extends BaseMapper<Student> {  
  
    IPage<Student> pageStudentList(Page<Student> page);  
  
    IPage<Student> pageStudentListByGender(Page<Student> page, @Param("gender") int gender);  
  
}
  • xml:不需要手写 limit
  
<select id="pageStudentList" resultType="com.framework.demo.entity.Student">  
    select * from student  
</select>  

<select id="pageStudentListByGender" resultType="com.framework.demo.entity.Student">  
    select * from student  
    <where>  
        <if test="gender !=null">  
            gender = #{gender}  
        </if>  
    </where>  
</select>

7.4 Service层

  • 接口
public interface StudentService extends IService<Student> {  
  
    /**  
     * service层:不需要返回IPage对象  
     * @param page 分页信息  
     */  
    void pageStudentList(Page<Student> page);  
  
  
    void pageStudentListByGender(Page<Student> page, int gender);  
  
}
  • 实现类:
@Service  
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService{  
  
    @Autowired  
    private StudentMapper studentMapper;  
  
    @Override  
    public void pageStudentList(Page<Student> page) {  
        // 我不需要将mapper结果返回给service,再由service层返回给controller  
        // 直接借助Page,传递给controller层  
        // service层,调用mapper查询业务  
        studentMapper.pageStudentList(page);  
    }  
  
    @Override  
    public void pageStudentListByGender(Page<Student> page, int gender) {  
        studentMapper.pageStudentListByGender(page,gender);  
    }  
      
}

7.5 Controller层:单表查询和自定义SQL查询

@RestController  
@Tag(name = "学生模块")  
public class StudentController {  
  
    @Autowired  
    private StudentService studentService;  
  
    @PostMapping("page1")  
    public Page<Student> page1(int pageNo, int pageSize) {  
  
        Page<Student> page = new Page<>(pageNo, pageSize);  
        // 单表查询  
        studentService.page(page);  
        return page;  
    }  
  
    @PostMapping("page2")  
    public Page<Student> page2(int pageNo, int pageSize) {  
  
        Page<Student> page = new Page<>(pageNo, pageSize);  
        // 自定义SQL查询、多表查询  
        studentService.pageStudentList(page);  
        return page;  
    }  
      
}
Logo

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

更多推荐