MyBatis-Plus

Published 2026-07-30 19:29 Updated 2026-07-30 19:29 3370 words 17 min read ... Page views

This article introduces the core features and usage scenarios of MyBatis-Plus, emphasizing that it is enhanced based on native MyBatis through non-intrusive methods to achieve advanced functions such as universal CRUD, conditional constructor, paging, logical deletion, and optimistic locking, which greatly improves development efficiency. It focuses on entity class annotations, common CRUD operations, Wrapper conditional constructor, automatic population, logical deletion and troubleshooting of common problems, and provides an in-depth analysis of its underlying execution principles and its relationship with MyBatis. It is suitable for developers who have mastered Spring Boot and MyBatis to quickly get started and cope with interviews.

MyBatis-Plus

Pre-course instructions

Applicable population: Developers or students who have mastered the basics of MyBatis and Spring Boot

Sample course environments: Spring Boot 3.2.x, MyBatis-Plus 3.5.6, MySQL 8.x

Course Objectives:

  • Master the core features of MyBatis-Plus, automatic CRUD, and conditional constructor
  • Proficient in advanced functions such as paging, sorting, logical deletion, and optimistic locking
  • Understand the underlying execution principles and automatic injection mechanism of MyBatis-Plus
  • Solve the high-frequency MyBatis-Plus interview principle questions

Core advantages (compared to native MyBatis):

Native MyBatis requires manual writing of a large number of XML/annotated SQL to implement additions, deletions and modifications. MyBatis-Plus is enhanced based on MyBatis, is non-invasive and only enhances, encapsulates universal CRUD, bid farewell to duplicate SQL, and greatly improves development efficiency.

MyBatis-Plus Quick Start

MyBatis-Plus Core Introduction

MyBatis-Plus (MP for short) is a MyBatis enhancement tool. Based on MyBatis, it only enhances and does no changes. It is created to simplify development and improve efficiency.

Core Features:

  • Non-intrusive: Only enhanced, without modifying native MyBatis code, compatible with original MyBatis project
  • Low loss: Activate automatic injection of universal CRUD, with almost no loss in performance
  • Powerful CRUD: Built-in universal Mapper, universal Service, single table operation zero SQL
  • Condition Constructor: Wrapper dynamically joins SQL, saying goodbye to hard-coded SQL
  • Advanced functions: paging, logical deletion, optimistic locking, automatic primary key generation, multi-tenancy, etc.

Environment building (Spring Boot and MP)

Introducing core dependencies

pom.xml core dependencies (Spring Boot parent project configured)

<!-- MyBatis-Plus 的 Spring Boot 3 Starter -->
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
   <version>3.5.6</version>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
   <groupId>com.mysql</groupId>
   <artifactId>mysql-connector-j</artifactId>
   <scope>runtime</scope>
</dependency>

<!-- Lombok,可选 -->
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
</dependency>

<!-- 测试依赖 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

Global configuration (application.yml)

spring:
 # 数据源配置
 datasource:
   url: jdbc:mysql://localhost:3306/java2601?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
   username: root
   password: root
   driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis-Plus 全局配置
mybatis-plus:
 # 映射文件路径
 mapper-locations: classpath:mapper/*.xml
 # 实体类别名包
 type-aliases-package: com.mp.demo.entity
 configuration:
   # 开启下划线转驼峰自动映射
   map-underscore-to-camel-case: true
   # 开发环境打印 SQL,生产环境通常关闭
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 global-config:
   db-config:
     # 主键自增策略
     id-type: auto
     # 数据库表前缀(可选)
     # table-prefix: tb_
     # 逻辑删除全局配置
     logic-delete-field: deleteFlag
     logic-delete-value: 1 # 已删除
     logic-not-delete-value: 0 # 未删除

Start class annotation

It is recommended to use @MapperScan to uniformly scan the Mapper interface. You can also add @Mapper to each interface, and you can choose one of the two methods.

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.Spring BootApplication;
@Spring BootApplication
@MapperScan("com.mp.demo.mapper")
public class MpDemoApplication {
   public static void main(String[] args) {
       SpringApplication.run(MpDemoApplication.class, args);
   }
}

Basic engineering structure

com.mp.demo
├── entity      // 数据库实体类
├── mapper      // Mapper 接口(继承BaseMapper)
├── service     // 业务层
│   └── impl    // 业务实现类(继承ServiceImpl)
└── controller  // 控制层

Core basics: Entity class annotations and primary key strategies

Database table preparation

CREATE TABLE `user` (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `name` varchar(30) DEFAULT NULL COMMENT '姓名',
 `age` int DEFAULT NULL COMMENT '年龄',
 `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
 `delete_flag` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识 0-未删除 1-已删除',
 `create_time` datetime DEFAULT NULL COMMENT '创建时间',
 `update_time` datetime DEFAULT NULL COMMENT '更新时间',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

Detailed explanation of entity class core annotations

MP completes the mapping of entities to database tables and fields through entity class annotations, replacing the native MyBatis manual mapping configuration.

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

import java.time.LocalDateTime;

@Data
// 对应数据库表名(若实体类名与表名一致可省略)
@TableName("user")
public class User {
   // 主键ID
   @TableId(type = IdType.AUTO)
   private Long id;
   // 姓名(字段名与数据库一致可省略注解)
   @TableField("name")
   private String name;
   // 年龄
   private Integer age;
   // 邮箱
   private String email;
   // 逻辑删除字段
   @TableLogic
   @TableField(fill = FieldFill.INSERT)
   private Integer deleteFlag;
   // 创建时间(自动填充)
   @TableField(fill = FieldFill.INSERT)
   private LocalDateTime createTime;
   // 更新时间(插入+更新自动填充)
   @TableField(fill = FieldFill.INSERT_UPDATE)
   private LocalDateTime updateTime;
}

@TableId should be used when the primary key attribute is not called id, or when a primary key policy needs to be explicitly specified.

Primary key generation strategy

The type attribute of @TableId is used to specify the primary key policy. Policies must be consistent with database field types and table structures.

Primary Key PolicyDescriptionApplication Scenario
AUTOuses the database self-increment primary key, and database fields must support self-increment.MySQL single library self-added primary key.
TheNONEentity does not explicitly specify a policy and is handled according to global configuration.
INPUTis assigned manually by the business code before insertion.business code, external system primary key.
ASSIGN_IDgenerates a numerical ID from MyBatis-Plus’s IdentifierGenerator.Global ID in distributed systems.
ASSIGN_UUIDgenerates a UUID string without hyphen.string primary key.

ASSIGN_ID

The default IdentifierGenerator uses a distributed ID algorithm based on time, work node, and serial number. Common structures can be summarized as:

1 位符号位 + 41 位时间戳 + 10 位工作节点标识 + 12 位序列号
  • The time stamp provides a roughly incremental trend.
  • Worker node identifiers are used to distinguish between different processes or servers.
  • Serial numbers are used to distinguish between multiple IDs generated in the same millisecond.

This ID is not strictly continuous self-increasing, and can only be considered as an overall trend. During distributed deployment, you still need to pay attention to working node conflicts and system clock callbacks; when there are special requirements, you can implement custom IdentifierGenerator.

@Data
@TableName("user")
public class User {
   @TableId(type = IdType.ASSIGN_ID)
   private Long id;

   private String name;
}

ASSIGN_UUID

ASSIGN_UUID is used for the string primary key. MyBatis-Plus generates UUIDs without hyphen by default, so database fields usually use CHAR(32) or VARCHAR(32).

@Data
@TableName("user")
public class User {
   @TableId(type = IdType.ASSIGN_UUID)
   private String id;

   private String name;
}

The advantages of UUIDs are that they are simple to generate and do not rely on the database; the disadvantages are that the string takes up a large amount of space, the values are disorderly, and the writing locality of clustered indexes is not as good as the trend increasing numerical ID. The primary key scheme should be selected based on the data size, index structure and system architecture, and should not absolute a certain strategy into a unified specification for all projects.

Universal CRUD

MP has built-in BaseMapper, IService and ServiceImpl, which can directly complete common single table CRUD. Complex queries, joined table queries and special performance requirements still require custom SQL.

BaseMapper data layer CRUD (core)

Mapper interface definition

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mp.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
   // 无需编写任何代码,继承BaseMapper即可拥有所有CRUD方法
}

Code examples of common CRUD methods

import com.mp.demo.entity.User;
import com.mp.demo.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.Spring BootTest;
import java.util.List;
@Spring BootTest
public class MapperCrudTest {
   @Autowired
   private UserMapper userMapper;
   // 新增
   @Test
   void testInsert() {
       User user = new User();
       user.setName("张三");
       user.setAge(20);
       user.setEmail("zhangsan@163.com");
       // 返回受影响行数
       int insert = userMapper.insert(user);
       System.out.println("新增ID:" + user.getId());
   }
   // 根据ID查询
   @Test
   void testSelectById() {
       User user = userMapper.selectById(1L);
       System.out.println(user);
   }
   // 查询所有
   @Test
   void testSelectList() {
       List<User> userList = userMapper.selectList(null);
       userList.forEach(System.out::println);
   }
   // 根据ID更新
   @Test
   void testUpdateById() {
       User user = new User();
       user.setId(1L);
       user.setAge(22);
       user.setEmail("update@163.com");
       int rows = userMapper.updateById(user);
       System.out.println("更新行数:" + rows);
   }
   // 根据ID删除
   @Test
   void testDeleteById() {
       int rows = userMapper.deleteById(1L);
       System.out.println("删除行数:" + rows);
   }
}

Service Business Layer CRUD

The business layer encapsulates capabilities such as batch operations and chain calls. Controllers should organize business through services and should not directly assume the data access logic.

Business layer interfaces and implementation classes

// 接口
import com.baomidou.mybatisplus.extension.service.IService;
import com.mp.demo.entity.User;
public interface UserService extends IService<User> {
}
// 实现类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mp.demo.entity.User;
import com.mp.demo.mapper.UserMapper;
import com.mp.demo.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

Example of core service methods

@Spring BootTest
public class ServiceCrudTest {
   @Autowired
   private UserService userService;
   // 批量新增
   @Test
   void testBatchSave() {
       List<User> list = new ArrayList<>();
       list.add(new User(null, "李四", 25, "lisi@163.com", null, null, null));
       list.add(new User(null, "王五", 28, "wangwu@163.com", null, null, null));
       // 批量插入
       boolean batch = userService.saveBatch(list);
       System.out.println("批量新增结果:" + batch);
   }
   // 新增或更新:主键非空且对应记录存在时更新,否则新增
   @Test
   void testSaveOrUpdate() {
       User user = new User(2L, "李四", 26, "lisi_new@163.com", null, null, null);
       boolean result = userService.saveOrUpdate(user);
   }
   // 批量删除
   @Test
   void testBatchDelete() {
       userService.removeByIds(Arrays.asList(3L,4L));
   }
}

Core focus: Condition constructor Wrapper

Wrapper is used to construct queries and update conditions, reducing duplicate code in simple dynamic SQL. Complex SQL should still use Mapper XML or custom methods.

Wrapper architecture

  • QueryWrapper: Query and delete condition constructor (no set field)
  • UpdateWrapper: Updates condition constructor (supports set field + condition)
  • LambdaQueryWrapper: Lambda query constructor (eliminate hard-coded field names, recommended)
  • LambdaUpdateWrapper: Lambda update builder

QueryWrapper conditional query example

// 条件:年龄大于20,姓名包含"李",按年龄降序
@Test
void testQueryWrapper() {
   QueryWrapper<User> wrapper = new QueryWrapper<>();
   wrapper.gt("age", 20)      // age > 20
          .like("name", "李") // name like '%李%'
          .orderByDesc("age");// order by age desc
   List<User> userList = userMapper.selectList(wrapper);
   userList.forEach(System.out::println);
}

Lambda Wrapper obtains fields through method references, which reduces misspelling of string field names and improves the security of refactoring.

// 等价上述条件,无硬编码字段
@Test
void testLambdaQueryWrapper() {
   LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
   wrapper.gt(User::getAge, 20)
          .like(User::getName, "李")
          .orderByDesc(User::getAge);
   List<User> userList = userMapper.selectList(wrapper);
}

UpdateWrapper dynamic updates

// 条件:年龄=25,更新邮箱和姓名
@Test
void testUpdateWrapper() {
   LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
   wrapper.eq(User::getAge, 25)
          .set(User::getName, "小李")
          .set(User::getEmail, "xiaoli@163.com");
   userMapper.update(null, wrapper);
}

Summary of common condition methods

MethodSQLCorrespondingDescription
eq=equals
ne!=is not equal to
gt/lt>/<is greater than/less than
ge/le>=/<=greater than or equal to/less than or equal to
likelike ‘%xx%‘fuzzy query
inin (xx,xx)contains queries
isNull/isNotNullis null / is not nullNull value judgment

MP core advanced features

Auto-fill function (creation/update time)

Business scenario: All tables have creation time and update time. There is no need to manually assign values and are automatically populated

Implementing the MetaObjectHandler fill processor

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
   // 插入时自动填充
   @Override
   public void insertFill(MetaObject metaObject) {
       this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
       this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
       this.strictInsertFill(metaObject, "deleteFlag", () -> 0, Integer.class);
   }
   // 更新时自动填充
   @Override
   public void updateFill(MetaObject metaObject) {
       this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
   }
}

Combined with the entity class@TableField(fill = xxx) annotation, fully automatic filling is achieved without code assignment.

logically deleted

Logical deletes do not actually remove database records, but instead update the deletion flag. After configuration is complete, the general method of MyBatis-Plus generates SQL according to the logical deletion rule.

global configuration

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

Field names in global configuration are entity class attribute names. You can also use @TableLogic only on a single entity field.

@TableLogic
private Integer deleteFlag;

Common SQL behaviors are as follows:

-- 逻辑删除前
DELETE FROM user WHERE id = ?;

-- 逻辑删除后生成的效果
UPDATE user
SET delete_flag = 1
WHERE id = ? AND delete_flag = 0;

-- 普通查询会过滤已删除记录
SELECT id, name, age
FROM user
WHERE delete_flag = 0;

Logical deletion of records will not be continued by the general query and update method by default. If the business needs to frequently query “deleted” data, you should consider whether this field is a deletion mark or a normal business status, and clearly handle it through custom SQL.

Pagination plug-in (required)

Paged queries require registration of PaginationInnerInterceptor, and the plug-in will rewrite SQL based on the database dialect and perform total queries.

paging plug-in configuration class

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MyBatis-PlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus 插件配置
*/
@Configuration
public class MpConfig {
   /**
    * 注册MP核心插件:分页插件 + 乐观锁插件
    */
   @Bean
   public MyBatis-PlusInterceptor mybatisPlusInterceptor() {
       MyBatis-PlusInterceptor interceptor = new MyBatis-PlusInterceptor();
       // 乐观锁插件
       interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
       // 多插件并用时,分页插件通常放在最后
       interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
       return interceptor;
   }
}

Example of paging query code

@Test
void testPage() {
   // 参数1:当前页,参数2:每页条数
   Page<User> page = new Page<>(1, 2);
   // 分页查询条件
   LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
   wrapper.gt(User::getAge, 18);
   Page<User> userPage = userMapper.selectPage(page, wrapper);
   // 分页结果参数
   System.out.println("当前页:" + userPage.getCurrent());
   System.out.println("每页条数:" + userPage.getSize());
   System.out.println("总条数:" + userPage.getTotal());
   System.out.println("总页数:" + userPage.getPages());
   System.out.println("数据列表:" + userPage.getRecords());
}

Optimistic locking (solving concurrent update issues)

Business scenario: Multiple users modify the same piece of data at the same time to prevent data overwrite loss

principle

Optimistic locking uses the version field to determine whether a record has been modified by another transaction. When updating, the old version number is carried, and the new version number is written after the matching is successful; when the matching fails, the number of updated rows is 0.

implementation steps

  1. Add version field to database
ALTER TABLE `user` ADD COLUMN `version` int DEFAULT 1 COMMENT '乐观锁版本号';
  1. Add version number annotations to entity classes
@Version
private Integer version;
  1. Activate the Optimistic Lock plugin (added in MyBatis-PlusInterceptor)
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

Typical SQL effects are as follows:

UPDATE user
SET age = 22, version = 2
WHERE id = 1 AND version = 1;

Flowchart of optimistic lock execution principle

正常更新流程(无并发冲突)
┌─────────┐  1.查询数据  ┌──────────┐
│ 客户端  │─────────────▶│ 数据库   │
└─────────┘              └────┬─────┘
      │                     │
      │ 2.返回version=1     │
      │◀────────────────────┘


┌────────────────────────────┐
│ 3.提交更新:携带id+version  │
└──────────────┬─────────────┘


┌────────────────────────────┐
│ 4.校验version一致,执行更新 │
│ 5.自动version+1 → version=2 │
└──────────────┬─────────────┘


           更新成功
并发冲突流程(多线程同时更新)
┌─────线程1─────┐        ┌─────线程2─────┐
查询version=1           查询version=1
   │                        │
更新携带version=1        更新携带version=1
   │                        │
   ▼                        ▼
执行更新、版本变为2       校验version≠1,更新失败

The underlying principles and frequently asked questions of MyBatis-Plus

Injection process of universal CRUD

When the project is started, MyBatis-Plus will parse the table information of the entity and register a common mapping statement for the interface that inherits BaseMapper through SQL Injector. When the Mapper method is called at runtime, it still enters the Mapper agent, MappedStatement, executor and JDBC processes of MyBatis.

Therefore, it is more accurate to say that the statement definitions of the generic CRUD are registered during the startup phase, and the actual SQL parameters, conditional fragments, and plug-in processing are still generated and executed based on the current parameters on each call. It cannot be simply understood as “all SQL is completed on a fixed basis at startup and there is no splicing cost at run time.”

Implementation principle of Wrapper

Wrapper saves the conditional fragments and parameters. When calling the Mapper method, MyBatis-Plus combines these contents into the corresponding mapping statement. MyBatis then generates BoundSql and binds the precompiled parameters.

Ordinary condition values are bound using parameters. Field names, sorting fields, last(), apply() and other contents that can affect the SQL structure still need to be handled with caution, and unverified user input cannot be directly used.

MyBatis’s relationship with MyBatis-Plus

MyBatis-Plus is based on the MyBatis extension, retaining native capabilities such as Mapper XML, annotated SQL, plug-ins, and type processors. It mainly provides universal CRUD, Wrapper, paging, logical deletion, optimistic locking, auto-filling and other functions. The two are not mutually exclusive frameworks.

Paging plug-in is not configured

If the paging plug-in is not registered correctly when calling selectPage(), SQL will not add database paging statements as expected, and may query far more data than the current page. You should check the plug-in Bean, database type, and dependent version.

When using multiple internal plug-ins, pay attention to the order; paging plug-ins are usually placed last. If you upgrade to 3.5.9 or higher, you should also follow the corresponding version document to confirm whether you need to introduce the JSQLParser support module separately.

Common failure reasons for optimism lock

  • OptimisticLockerInnerInterceptor is not registered.
  • @Version is not added to the version field of the entity.
  • The old version number was not read and carried before the update.
  • The method parameter form used does not meet the plug-in identification rules.
  • Reuse the same Wrapper repeatedly when calling update(entity, wrapper).
  • The update row number is ignored as 0 and is not treated as a concurrency conflict.

Automatic filling of frequently asked questions

  • MetaObjectHandler is not registered as a Spring Bean.
  • The field is not configured with the correct FieldFill.
  • The field already has a value, but strictInsertFill() or strictUpdateFill() is not overwritten by rules.
  • Java property names, field types and padding codes are inconsistent.

Logical Deletion Frequently Asked Questions

  • The global configuration fills in database column names rather than entity attribute names.
  • Database default values, insert fill values, and logical undelete values are inconsistent.
  • Custom SQL does not handle logical deletion conditions based on business needs.

Primary key common questions

  • The database column corresponding to the AUTO policy is not set to self-increment.
  • ASSIGN_UUID uses a string primary key, but the database field length is insufficient.
  • Custom IdentifierGenerator uses duplicate node identifiers on multiple nodes.
  • Misunderstanding trend increasing IDs for strictly continuous IDs and relying on their continuity to achieve business logic.

If you enjoyed this, leave a comment~

... Page views
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka