1. Introduction
MyBatis-Plus (MP for short) is an enhancement tool for MyBatis. It only enhances MyBatis without changing it, built to simplify development and improve efficiency.
Core features:
- Non-intrusive: adding MP has no effect on an existing MyBatis project — smooth as silk.
- Low overhead: basic CRUD is auto-injected at startup, with essentially no performance loss.
- Powerful CRUD operations: built-in generic Mapper and generic Service; with just a little configuration you get most single-table CRUD.
- Supports Lambda-style calls: write query conditions safely and efficiently via lambda expressions, preventing mistyped field names.
- Built-in code generator: generate Mapper, Service, Controller, etc. with minimal configuration.
- Built-in pagination plugin: physical pagination based on MyBatis; developers don’t need to worry about the details — just configure and use.
Official site: https://baomidou.com/
2. Basic Usage
2.1 Add the dependency
Add to a Maven project’s pom.xml:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version> <!-- use the latest version -->
</dependency>
For a traditional Spring project, add the mybatis-plus core dependency and configure it yourself. A Spring Boot project typically uses the starter directly.
2.2 Define the entity class
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
2.3 Write the Mapper interface
Extending BaseMapper<T> gives you CRUD capability:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
At this point UserMapper already has many methods like insert, deleteById, updateById, selectById, selectList, with no XML needed.
2.4 Scan the Mappers
The Spring Boot main class needs @MapperScan to scan the Mapper package:
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. Common Annotations
3.1 @TableName
- Purpose: specify the mapped table name when the table name and entity class name differ.
- Attributes:
value: the table nameschema: the schemakeepGlobalPrefix: whether to keep the global table prefix (default false)autoResultMap: whether to auto-build a ResultMap (used with typeHandler, e.g. a JSON handler)
@TableName("t_user")
public class User { ... }
3.2 @TableId
- Purpose: mark the primary key field.
- Attributes:
value: the primary key column name (if it differs from the property name)type: the primary-key generation strategy, using theIdTypeenum; common ones:AUTO: database auto-increment, requires DB support for auto-increment keys.INPUT: user-supplied input.ASSIGN_ID(default): a Long ID generated by the Snowflake algorithm.ASSIGN_UUID: generated as a UUID (with hyphens removed).
public class User {
@TableId(value = "user_id", type = IdType.AUTO)
private Long id;
// ...
}
Note: if the entity’s primary-key property is named
idand the table’s primary-key column is alsoid, you can omit this annotation; the default key strategy isASSIGN_ID.
3.3 @TableField
- Purpose: mark an ordinary non-primary-key field.
- Attributes:
value: specify the database column name (when the field name differs from the column name).exist: whether it’s a database field (falsemeans it isn’t, and it won’t participate in SQL generation).condition: a custom query-condition rule (rarely used).fill: the field auto-fill strategy (used withMetaObjectHandler).select: whether to include it in select queries (default true).typeHandler: specify a type handler (e.g. JSON handling).
Common scenarios:
-
A boolean field whose property name starts with
is
If the entity hasBoolean isAdult, MP by default drops theisprefix and looks for a databaseadultcolumn, so you must specify the column name manually:@TableField("is_adult") private Boolean isAdult; -
A conflict with a database keyword
For example, an entity propertyorderconflicts with a MySQL keyword; addvaluewith backticks or rename it:@TableField("`order`") private Integer order; -
A field that doesn’t exist in the database
For example, a temporary field used in business logic:@TableField(exist = false) private String remark; // this field won't be saved to the database
4. Default Conventions
MP follows “convention over configuration”:
- The class name is converted to snake_case as the table name by default (e.g.
UserInfo→user_info). - The default key strategy is
ASSIGN_ID(Snowflake). - Field names are converted to snake_case to map columns (e.g.
createTime→create_time). - If a property is named
idand is of typeLong, it’s automatically recognized as the primary key. These conventions can be overridden via configuration or annotations.
5. Common Configuration
Common configuration in application.yml:
mybatis-plus:
# Type-alias package scan; usually just this is enough, no need to separately configure type-aliases-package under mybatis
type-aliases-package: com.example.entity
# Mapper XML file locations (separate multiple dirs with commas)
mapper-locations: classpath*:/mapper/**/*.xml
# Native MyBatis config
configuration:
# snake_case to camelCase (on by default)
map-underscore-to-camel-case: true
# caching (on by default)
cache-enabled: true
# Global config
global-config:
db-config:
# Global key strategy (priority: annotation > global config)
id-type: assign_id
# Global field-update strategy (not_null: only update non-null fields; ignored: all fields)
update-strategy: not_null
# Logical-delete config (detailed later)
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
Global config has lower priority than annotations: for example, if an entity uses @TableId(type = IdType.AUTO) to specify auto-increment, the annotation wins even if the global config sets assign_id.
6. Core Features
6.1 Condition constructors
The condition constructor is one of MP’s most powerful features, used to dynamically generate WHERE conditions. The main ones:
QueryWrapper<T>: builds query conditions, can return entity objects.UpdateWrapper<T>: builds update conditions, can set the set clause.LambdaQueryWrapper<T>/LambdaUpdateWrapper<T>: reference fields via lambda expressions, preventing errors from hardcoded strings.
Examples of common methods:
// Ordinary QueryWrapper example
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "name") // specify query columns
.like("name", "Zhang") // name like '%Zhang%'
.between("age", 18, 30) // age between 18 and 30
.eq("email", "test@baomidou.com") // email = ?
.orderByDesc("id"); // order by id desc
List<User> users = userMapper.selectList(wrapper);
// Lambda form, safer
LambdaQueryWrapper<User> lambdaQ = new LambdaQueryWrapper<>();
lambdaQ.like(User::getName, "Zhang")
.between(User::getAge, 18, 30);
List<User> lambdaUsers = userMapper.selectList(lambdaQ);
// Update some fields
LambdaUpdateWrapper<User> lambdaU = new LambdaUpdateWrapper<>();
lambdaU.eq(User::getId, 1)
.set(User::getAge, 25)
.set(User::getEmail, "new@email.com");
userMapper.update(null, lambdaU); // the first argument is the entity, can be null
6.2 Custom SQL
When you need to write complex SQL but still want to use MP’s condition constructor to dynamically assemble the WHERE conditions, combine custom XML with ${ew.customSqlSegment}.
Mapper interface:
@Mapper
public interface UserMapper extends BaseMapper<User> {
// The parameter must receive the Wrapper via @Param("ew"); the second parameter is customizable
List<User> selectByMyWhere(@Param("ew") Wrapper<User> wrapper, @Param("minAge") Integer minAge);
}
XML mapping file:
<select id="selectByMyWhere" resultType="com.example.entity.User">
SELECT * FROM user
${ew.customSqlSegment}
AND age > #{minAge}
</select>
When calling:
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, "Wang");
List<User> list = userMapper.selectByMyWhere(wrapper, 20);
${ew.customSqlSegment} is automatically replaced with the WHERE condition string generated by the Wrapper, and it correctly handles the where keyword — very convenient.
6.3 The Service interface
MP provides IService<T> and ServiceImpl<M extends BaseMapper<T>, T>, encapsulating higher-level business logic.
Define the Service interface:
public interface IUserService extends IService<User> {
// custom methods
}
Implementation class:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
// implement custom methods
}
Common operations:
save(entity): insertsaveBatch(list): batch insertremoveById(id): delete by IDremoveBatchByIds(ids): batch delete (multiple SQLs executed one by one)updateById(entity): update by IDupdateBatchById(list): batch updategetById(id): querylist(): query a listpage(page, wrapper): paginated query
Difference between removeByIds and removeBatchByIds?
Actually removeByIds internally calls removeBatchByIds by default, and there’s no essential functional difference (both execute one SQL DELETE FROM table WHERE id IN (?,?,...) for multiple IDs). But in older versions the former might delete one by one, while newer versions merge into a single IN statement. In practice, don’t overthink it — just use removeByIds.
Lambda update:
lambdaUpdate() returns a LambdaUpdateChainWrapper, and the chained operation is very clear:
userService.lambdaUpdate()
.eq(User::getId, 1)
.set(User::getAge, 28)
.update(); // execute the update
Batch insert and efficiency optimization:
IService’s saveBatch(list) submits multiple INSERT statements at once by default, but the underlying JDBC driver may still send them to the database one by one. To improve MySQL batch-insert performance, add a parameter to the JDBC connection URL:
jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true
Once enabled, the driver merges multiple SQLs into one longer SQL to send, significantly improving performance.
Drawbacks of this configuration?
- For an extremely large batch insert, the merged SQL length may exceed MySQL’s
max_allowed_packetlimit, causing execution to fail. - If an error occurs, it’s hard to pinpoint which row went wrong, hindering debugging.
- Batch operations may hold table locks for a long time, affecting concurrency.
- If the inserted data needs auto-increment key backfill, batch backfill may be unavailable or out of order in some driver versions.
Therefore the recommendation: control the batch size (e.g. 1000 rows) and use it together with rewriteBatchedStatements.
6.4 The pagination plugin
MP’s pagination is implemented via a plugin; you must first configure the MybatisPlusInterceptor and add the pagination inner interceptor.
Config class:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// pagination plugin
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
Using a paginated query:
Page<User> page = new Page<>(1, 10); // current page number, page size
Page<User> result = userMapper.selectPage(page, null); // a QueryWrapper can be passed
List<User> records = result.getRecords();
long total = result.getTotal();
Pagination at the Service layer:
IPage<User> page = userService.page(new Page<>(1, 10),
new LambdaQueryWrapper<User>().like(User::getName, "Li"));
Besides the data, the pagination result automatically includes the total count, page number, etc., ready for the frontend.
A generic pagination entity and MP conversion:
Real projects usually have a unified response class, and you need to convert IPage to a custom pagination object. You can write a utility method:
public class PageUtils {
public static <T> CommonPage<T> convert(IPage<T> page) {
CommonPage<T> result = new CommonPage<>();
result.setList(page.getRecords());
result.setTotal(page.getTotal());
result.setCurrent(page.getCurrent());
result.setSize(page.getSize());
return result;
}
}
7. Extended Features
7.1 Code generator
MP provides a powerful code generator that quickly generates Entity, Mapper, Service, Controller, etc. Using the official new version is recommended.
Add the dependency (you may need to add the generator module separately):
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
Example configuration (generating code):
public class CodeGenerator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/test", "root", "password")
.globalConfig(builder -> {
builder.author("baomidou") // set the author
.outputDir("D://"); // output directory
})
.packageConfig(builder -> {
builder.parent("com.example"); // parent package name
})
.strategyConfig(builder -> {
builder.addInclude("user"); // the table(s) to generate
})
.templateEngine(new VelocityTemplateEngine())
.execute();
}
}
Adjust the strategy as needed, for example enabling Lombok or setting a superclass.
7.2 The static utility Db
Starting from MP 3.5.3, the static utility class Db is provided, letting you do CRUD directly without injecting a Mapper or Service.
Simple usage:
// Query a list
List<User> users = Db.lambdaQuery(User.class).like(User::getName, "Li").list();
// Insert
User user = new User();
user.setName("Wang Wu");
Db.save(user);
Use case: resolving circular dependencies
In Spring, if Service A depends on Service B and B in turn depends on A, you get a circular dependency. If a Service just temporarily needs some table’s data in a method, you can use the Db static utility to operate the database directly, thereby avoiding injecting the other Service and effectively breaking the circular-dependency chain.
How does Spring resolve circular dependencies?
Spring mainly resolves circular dependencies of singleton beans via a three-level cache:
singletonObjects: level-1 cache, holds fully initialized beans.earlySingletonObjects: level-2 cache, holds early references (raw objects, property injection incomplete).singletonFactories: level-3 cache, holds ObjectFactories that can produce early references.
When A, during its creation, needs B, and B during its creation needs A, Spring can obtain A’s early reference via the three-level cache and expose it to B ahead of time, completing creation. But this requires the beans to be singletons and not constructor-injected — constructor injection can’t be resolved. TheDbutility offers a code-level decoupling alternative.
7.3 Logical delete
Logical delete marks data as deleted in the table rather than physically deleting it, making data recovery and auditing easier.
Global config (mentioned earlier):
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # entity field name
logic-delete-value: 1 # value after deletion
logic-not-delete-value: 0 # value when not deleted
Add the annotation to the entity:
@Data
public class User {
@TableLogic
private Integer deleted;
}
Executing userMapper.deleteById(1L); actually generates UPDATE user SET deleted=1 WHERE id=1 AND deleted=0; queries automatically append deleted=0.
Recommended practice in real work:
For core business that needs to retain historical data, use logical delete, and combine it with scheduled data migration to move logically-deleted “cold data” to an archive database, reducing the business table’s size and maintaining performance.
7.4 Enum handler
Intelligently maps Java enums to database fields, saving manual conversion.
1. Define the enum and mark the database-stored value with @EnumValue
public enum GenderEnum {
MALE(1, "Male"),
FEMALE(2, "Female");
@EnumValue // the value stored in the database
private final int code;
private final String desc;
// constructor, getters omitted
}
2. Globally configure the enum handler:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
3. Use the enum type in the entity:
public class User {
private GenderEnum gender;
}
On insert or query, gender is automatically converted to/from the database int type.
4. Handling the return value with @JsonValue (frontend-backend interaction):
If you need it displayed as a JSON string (e.g. “Male”) when returned to the frontend, annotate the enum with @JsonValue:
public enum GenderEnum {
MALE(1, "Male"),
FEMALE(2, "Female");
@EnumValue
private final int code;
@JsonValue
private final String desc;
}
When serializing to JSON, MALE is displayed as "Male".
7.5 JSON handler
An entity property can map directly to a JSON field, commonly used to store extension info.
Entity class:
@Data
@TableName(value = "user", autoResultMap = true) // must enable auto result mapping
public class User {
private Long id;
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, Object> extra; // JSON field
}
The database extra column type is json or varchar. On insert, the Map is auto-serialized to a JSON string; on query, it’s deserialized back to a Map.
Note: @TableName(autoResultMap = true) can’t be omitted, or the typeHandler won’t be called to handle that field on query.
7.6 Plugin features
MP’s plugin system is based on interceptors and can add various features.
Initialize the core interceptor:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. Pagination plugin
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 2. Optimistic-lock plugin
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 3. Block full-table update/delete plugin
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
}
- Pagination plugin: see 6.4.
- Optimistic-lock plugin: add
@Versionto an entity field, and the version is automatically compared on update. - Block full-table update/delete: when an
updateordeletehas nowherecondition, it’s blocked and an exception is thrown, effectively preventing mistakes.