MyBatis-Plus Interview Self-Test: Full Core Analysis and Pitfall Checklist

This article presents a comprehensive interview self‑test for MyBatis‑Plus, covering its relationship with MyBatis, BaseMapper CRUD implementation, SqlMethod and AbstractMethod roles, MapperProxy interception, LambdaQueryWrapper type safety, Wrapper condition handling, interceptor chain, pagination mechanisms, logical delete rewriting, MetaObjectHandler fill timing, thread‑safety pitfalls, and best‑practice recommendations.

CodeSmart Hoops
CodeSmart Hoops
CodeSmart Hoops
MyBatis-Plus Interview Self-Test: Full Core Analysis and Pitfall Checklist

MyBatis‑Plus vs MyBatis

MyBatis‑Plus (MP) is an enhancement library that wraps MyBatis without replacing it. It adds automatic CRUD, Java‑code condition building, and a pagination plugin while keeping full compatibility with native MyBatis XML or annotation SQL.

MyBatis: manual SQL, dynamic <if>/<where>, manual pagination.

MyBatis‑Plus: BaseMapper generates CRUD, LambdaQueryWrapper builds conditions in Java, PaginationInnerInterceptor rewrites SQL automatically.

MP inserts an interceptor into MyBatis's Executor chain, rewriting SQL and parameters before execution using MyBatis's native plugin mechanism.

BaseMapper CRUD without XML

BaseMapper is a generic interface that declares 17 common methods (e.g., insert, deleteById, updateById, selectById) but has no concrete implementation class. At runtime MP registers a MappedStatement for each method via AbstractMethod subclasses. Spring scans @Mapper interfaces, creates a JDK dynamic proxy ( MapperProxy), and routes method calls to the corresponding MappedStatement.

Spring scans @Mapper interfaces
 → MapperFactoryBean.getObject()
   → sqlSession.getMapper(Mapper interface)
     → MapperProxyFactory.newInstance(sqlSession)
       → Proxy.newProxyInstance (creates JDK proxy)
         → MapperProxy.invoke()
           → if method belongs to BaseMapper
               → locate MappedStatement and execute CRUD
           → else
               → execute custom SQL (XML/annotation)

SqlMethod enum and AbstractMethod

SqlMethod defines SQL templates for all CRUD operations. Placeholders %s are replaced at runtime with table name, column list, etc.

INSERT_ONE("insert", "插入一条数据", "<script>INSERT INTO %s %s VALUES %s</script>"),
DELETE_BY_ID("deleteById", "根据 ID 删除一条数据", "DELETE FROM %s WHERE %s=#{%s}"),
SELECT_BY_ID("selectById", "根据 ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s}")

AbstractMethod and its subclasses ( Insert, SelectById, DeleteById …) fill entity metadata (table name, primary‑key column, field list) into the templates and create a MappedStatement that is registered with MyBatis.

public class Insert extends AbstractMethod {
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        String sql = String.format("INSERT INTO %s (%s) VALUES (%s)",
            tableInfo.getTableName(),
            tableInfo.getKeyColumn(),
            tableInfo.getKeyProperty());
        return this.addInsertMappedStatement(mapperClass, modelClass, sql);
    }
}

Thus SqlMethod defines *what* the SQL looks like; AbstractMethod supplies concrete table/field details.

MapperProxy interception of BaseMapper

When business code calls mapper.selectById(1), the call goes through the JDK dynamic proxy. Its invoke() method checks whether the method is defined in BaseMapper. If so, it looks up the pre‑registered MappedStatement and delegates to MyBatis Executor; otherwise it executes custom XML/annotation SQL.

mapper.selectById(1)
 → MapperProxy.invoke("selectById", [1])
   → is BaseMapper method?
       → yes: find MappedStatement "selectById" → Executor executes
       → no: execute custom SQL path

LambdaQueryWrapper type safety

LambdaQueryWrapper uses method references (e.g., MpProduct::getCategory) which are Lambda expressions. MP extracts the method name via SerializedLambda, strips the get prefix, lower‑cases the first letter to obtain the property name ( category), then maps it to the column name ( categorycategory, createTimecreate_time) using an internal cache ( EntityResolver). This provides compile‑time verification of field existence.

wrapper.eq(MpProduct::getCategory, "Electronics")
// steps:
// 1. Method reference captured as SerializedLambda
// 2. Extract "getCategory"
// 3. Remove "get" → "Category" → lower‑case first letter → "category"
// 4. Resolve to column name via cache

The extracted column names are cached globally; the first call parses the Lambda, subsequent calls read from the cache with negligible overhead.

Wrapper condition parameter

The condition argument controls whether a particular fragment is added to the generated SQL. When condition == false, the fragment is omitted, enabling concise dynamic queries.

public Children eq(boolean condition, R column, Object val) {
    if (condition) {
        addCondition(column, EQUALS, val);
    }
    return typedThis;
}

// Example usage with optional filters
LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(category), MpProduct::getCategory, category)
       .gt(minPrice != null, MpProduct::getPrice, minPrice)
       .lt(maxPrice != null, MpProduct::getPrice, maxPrice);

MybatisPlusInterceptor interception chain

MybatisPlusInterceptor

is a container that holds a list of InnerInterceptor instances. Interceptors are executed in the order they are added; each focuses on a single responsibility (e.g., pagination rewrite, logical delete) and does not interfere with others. This mirrors MyBatis's native Interceptor plugin mechanism.

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
    // more InnerInterceptor can be added
    return interceptor;
}

PaginationInnerInterceptor SQL rewrite

The interceptor implements InnerInterceptor and intercepts the prepare method of StatementHandler. If the parameter object contains a Page, it optionally executes a count query, then builds a pagination‑specific SQL using the appropriate dialect and replaces the original SQL.

@Intercepts({@Signature(
    type = StatementHandler.class,
    method = "prepare",
    args = {Connection.class, Integer.class}
)})
public class PaginationInnerInterceptor implements InnerInterceptor {
    @Override
    public void prepare(StatementHandler delegate, Connection connection) {
        String originalSql = delegate.getBoundSql().getSql();
        if (needPage(delegate)) {
            Page<?> page = getPage(delegate);
            if (page.isSearchCount()) {
                Long total = performCount(connection, delegate, page);
                page.setTotal(total);
            }
            String pageSql = dialect.buildPaginationSql(originalSql, page.getOffset(), page.getSize());
            replaceSql(delegate, pageSql);
        }
    }
}

Dialect handling examples:

H2: ... LIMIT 10 OFFSET 0 MySQL: ... LIMIT 0, 10 Oracle:

... ROW_NUMBER() OVER (...)

Physical vs. logical pagination

Physical pagination adds LIMIT/OFFSET to the SQL so the database returns only the current page. It is recommended for large data sets and offers high performance.

Logical pagination fetches the full result set and slices it in memory; suitable for small data sets or when a full‑set calculation is required, but performance is low.

MP uses physical pagination by default. When the offset becomes very large (e.g., LIMIT 100000, 10), the database still scans the preceding rows, degrading performance. In such cases cursor‑based pagination is advised:

LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(lastCreateTime != null, MpProduct::getCreateTime, lastCreateTime)
       .orderByDesc(MpProduct::getCreateTime)
       .last("LIMIT 20");

@TableLogic SQL rewrite

The LogicalDeleteInterceptor parses the original SQL before execution and rewrites it according to the operation type:

DELETE FROM mp_product WHERE id = 1
    → UPDATE mp_product SET deleted = 1 WHERE id = 1 AND deleted = 0
SELECT * FROM mp_product WHERE id = 1
    → SELECT * FROM mp_product WHERE id = 1 AND deleted = 0
INSERT INTO mp_product (name) VALUES ('x')
    → INSERT INTO mp_product (name, deleted) VALUES ('x', 0)
UPDATE mp_product SET name = 'y' WHERE id = 1
    → UPDATE mp_product SET name = 'y' WHERE id = 1 AND deleted = 0

Configuration (application.properties):

mybatis-plus.global-config.db-config.logic-delete-field=deleted
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

Logical delete is appropriate for audit‑preserving scenarios; raw SQL must be used to bypass the interceptor for true physical deletion.

MetaObjectHandler fill timing

MetaObjectHandler

defines two methods:

public interface MetaObjectHandler {
    void insertFill(MetaObject metaObject);
    void updateFill(MetaObject metaObject);
}

Invocation flow:

During an insert operation, MP checks fields annotated with fill = FieldFill.INSERT or FieldFill.INSERT_UPDATE and calls insertFill.

During an update operation, MP checks fields annotated with fill = FieldFill.UPDATE or FieldFill.INSERT_UPDATE and calls updateFill.

The fill result is bound to the parameter object and executed as part of the SQL.

Example entity annotations:

@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime; // only on insert

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime; // on insert and update

Bean definition example:

@Bean
public MetaObjectHandler metaObjectHandler() {
    return new MetaObjectHandler() {
        @Override
        public void insertFill(MetaObject metaObject) {
            LocalDateTime now = LocalDateTime.now();
            this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
            this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
        }
        @Override
        public void updateFill(MetaObject metaObject) {
            this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        }
    };
}

Wrapper thread‑safety issue and solution

Wrapper maintains conditions in a linked list. Reusing the same Wrapper instance across threads causes conditions to accumulate, producing incorrect SQL.

// ❌ Wrong: reuse Wrapper as a member variable
public class ProductService {
    private LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
    public List<MpProduct> search(String name) {
        wrapper.eq(MpProduct::getName, name); // concurrent calls add conditions
        return mpProductMapper.selectList(wrapper);
    }
}

Solution: create a new Wrapper for each query or use the chain API, which internally creates a fresh instance.

// ✅ Correct: new Wrapper per query
public List<MpProduct> search(String name) {
    LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(MpProduct::getName, name);
    return mpProductMapper.selectList(wrapper);
}

// Chain API example
List<MpProduct> result = Wrappers.<MpProduct>lambdaQuery()
    .eq(MpProduct::getName, name)
    .list();

Risk of selectList(null) on large tables

selectList(null)

translates to SELECT * FROM table with no WHERE clause. On tables containing millions of rows this loads the entire result set into JVM memory, leading to:

OutOfMemoryError (OOM)

Heavy GC pressure

Prolonged DB connection usage, affecting other requests

// ❌ Risky code
List<MpProduct> all = mpProductMapper.selectList(null);

// ✅ Use pagination for large data
Page<MpProduct> page = new Page<>(1, 100);
mpProductMapper.selectPage(page, null);

Pagination plugin activation conditions

The pagination plugin intercepts only queries whose method parameters contain a Page object. Two prerequisites are required:

Register MybatisPlusInterceptor with a PaginationInnerInterceptor (see configuration above).

Pass a Page instance to the mapper method.

// ❌ Not intercepted (no Page parameter)
List<MpProduct> list = mpProductMapper.selectList(wrapper);

// ✅ Intercepted (Page parameter present)
Page<MpProduct> page = new Page<>(1, 10);
mpProductMapper.selectPage(page, wrapper);

TableInfoHelper caching mechanism

TableInfoHelper

manages entity metadata and caches it in a global ConcurrentHashMap to avoid repeated reflection.

private static final Map<Class<?>, TableInfo> TABLE_INFO_CACHE = new ConcurrentHashMap<>();

First‑time parsing workflow:

Read @TableName → obtain table name.

Read @TableId → obtain primary‑key column and strategy.

Scan all fields, convert camelCase to snake_case, build field list.

Encapsulate results into a TableInfo object and store it in the cache.

Subsequent accesses retrieve TableInfo directly from the cache, eliminating reflection overhead.

TableInfo tableInfo = TableInfoHelper.getTableInfo(EntityClass.class);
String tableName = tableInfo.getTableName(); // "mp_product"
String keyColumn = tableInfo.getKeyColumn(); // "id"
List<TableFieldInfo> fields = tableInfo.getFieldList();
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PaginationInterceptorMyBatis-PlusBaseMapperWrapperLambdaQueryWrapperLogicalDeleteMetaObjectHandler
CodeSmart Hoops
Written by

CodeSmart Hoops

A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.