MyBatis-Plus
Pre-course instructions
applicable population: Developer or student who has mastered the basics of MyBatis and Spring Boot
Course example environment:Spring Boot 3.2.x、MyBatis-Plus 3.5.6、MySQL 8.x
curriculum 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 amount of XML/annotated SQL to add, delete, and change checking. MyBatis-Plus is enhanced based on MyBatis.No intrusion, only enhancement, encapsulates universal CRUD, bids 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’s enhanced tools, based on MyBatis, only enhancements and no changes, are created to simplify development and improve efficiency.
core features:
- non-invasive: Only enhanced without modifying the native MyBatis code, compatible with the original MyBatis project
- low loss: Activate automatic injection of universal CRUD, with almost no loss of performance
- Powerful CRUD: Built-in universal Mapper, universal Service, single table operation zero SQL
- condition constructor: Wrapper dynamically spliced SQL, bid farewell to hard-coded SQL
- advanced features: 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
recommended @MapperScan Unified scanning of the Mapper interface. You can also add on each interface @Mapper, 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 is completed through entity class annotations Mapping between entities and database tables and fields, replaces 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;
}When the primary key attribute is not called id, or when you need to explicitly specify a primary key policy, you should use the @TableId。
Primary key generation strategy
@TableId the type Property is used to specify the primary key policy. Policies must be consistent with database field types and table structures.
| primary key policy | description | applicable scenarios |
|---|---|---|
AUTO | To use a database self-increment primary key, database fields must support self-increment. | MySQL single library self-added primary key. |
NONE | The entity does not explicitly specify a policy and is handled according to global configuration. | The primary key policy is uniformly configured by the project. |
INPUT | The assignment is manually assigned by the business code before insertion. | Business code, external system primary key. |
ASSIGN_ID | by MyBatis-Plus IdentifierGenerator Generate a numeric ID. | Global ID in a distributed system. |
ASSIGN_UUID | Generates a UUID string without hyphen. | String primary key. |
ASSIGN_ID
default IdentifierGenerator Use a distributed ID algorithm based on time, work node, and serial number. Common structures can be summarized as:
1 位符号位 + 41 位时间戳 + 10 位工作节点标识 + 12 位序列号- Timestamps provide a rough increasing trend.
- Work node identities are used to distinguish different processes or servers.
- The serial number is used to distinguish between multiple IDs generated within the same milliseconds.
This kind of ID is not strictly continuous and self-increasing, but can only be regarded as an overall trend of increasing. When deploying distributed, you still need to pay attention to working node conflicts and system clock callbacks; when there are special requirements, you can implement customization IdentifierGenerator。
@Data
@TableName("user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
}ASSIGN_UUID
ASSIGN_UUID Used for a string primary key. MyBatis-Plus generates UUIDs without hyphens by default, so database fields are usually used with 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 built-in BaseMapper、IService and ServiceImpl, you 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: Update condition constructor (support 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);
}LambdaQueryWrapper (recommended, no hard coding)
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 commonly used conditions and methods
| method | SQLcorresponding | description |
|---|---|---|
| eq | = | equal to |
| ne | != | not equal to |
| gt/lt | >/< | over/under |
| ge/le | >=/<= | Greater than or equal to/less than or equal to |
| like | like ‘%xx%‘ | fuzzy query |
| in | in (xx,xx) | containing the query |
| isNull/isNotNull | is null / is not null | null value judgment |
MP core advanced features
Auto-fill function (creation/update time)
business scenarios: 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, MyBatis-Plus’s general method generates SQL according to logical deletion rules.
global configuration
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleteFlag
logic-delete-value: 1
logic-not-delete-value: 0Field names in global configuration are entity class attribute names. You can also use it only on a single entity field @TableLogic。
@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)
Paginated query requires registration PaginationInnerInterceptor, the plug-in will rewrite SQL based on the database dialect and perform a total query.
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 scenarios: Multiple users modify the same piece of data at the same time to prevent data overwrite and 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
- Add version field to database
ALTER TABLE `user` ADD COLUMN `version` int DEFAULT 1 COMMENT '乐观锁版本号';- Add version number annotations to entity classes
@Version
private Integer version;- 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 starts, MyBatis-Plus will parse the table information of the entity and inherit it through SQL Injector. BaseMapper The interface for registers a common mapping statement. When calling the Mapper method at runtime, it still enters the Mapper agent of MyBatis,MappedStatement, executors and JDBC processes.
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, and then generates them by MyBatis BoundSql And bind precompiled parameters.
Ordinary condition values are bound using parameters. field name, sort field,last()、apply() Content that can affect the structure of SQL still needs to be handled with caution, and unverified user input cannot be used directly.
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
call selectPage() If the paging plug-in is not correctly registered at the time, 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 confirm whether you need to introduce the JSQLParser support module separately according to the corresponding version document.
Common failure reasons for optimism lock
- unregistered
OptimisticLockerInnerInterceptor。 - The version field for the entity was not added
@Version。 - 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.
- call
update(entity, wrapper)Use the same Wrapper repeatedly when you are using it. - ignored updating the number of rows to be
0, did not treat it as a concurrent conflict.
Automatic filling of frequently asked questions
MetaObjectHandlerNot registered as a Spring Bean.- Field is not configured correctly
FieldFill。 - Field already has a value, and
strictInsertFill()orstrictUpdateFill()Not overwritten according to 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
AUTOThe database column corresponding to the policy is not set to self-increment.ASSIGN_UUIDUse a string primary key, but the database field length is insufficient.- custom
IdentifierGeneratorDuplicate node identities are used 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~