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 name
    • schema: the schema
    • keepGlobalPrefix: 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 the IdType enum; 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 id and the table’s primary-key column is also id, you can omit this annotation; the default key strategy is ASSIGN_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 (false means 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 with MetaObjectHandler).
    • select: whether to include it in select queries (default true).
    • typeHandler: specify a type handler (e.g. JSON handling).

Common scenarios:

  1. A boolean field whose property name starts with is
    If the entity has Boolean isAdult, MP by default drops the is prefix and looks for a database adult column, so you must specify the column name manually:

    @TableField("is_adult")
    private Boolean isAdult;
    
  2. A conflict with a database keyword
    For example, an entity property order conflicts with a MySQL keyword; add value with backticks or rename it:

    @TableField("`order`")
    private Integer order;
    
  3. 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. UserInfouser_info).
  • The default key strategy is ASSIGN_ID (Snowflake).
  • Field names are converted to snake_case to map columns (e.g. createTimecreate_time).
  • If a property is named id and is of type Long, 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): insert
  • saveBatch(list): batch insert
  • removeById(id): delete by ID
  • removeBatchByIds(ids): batch delete (multiple SQLs executed one by one)
  • updateById(entity): update by ID
  • updateBatchById(list): batch update
  • getById(id): query
  • list(): query a list
  • page(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_packet limit, 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. The Db utility 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 @Version to an entity field, and the version is automatically compared on update.
  • Block full-table update/delete: when an update or delete has no where condition, it’s blocked and an exception is thrown, effectively preventing mistakes.