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.
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 pathLambdaQueryWrapper 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 ( category → category, createTime → create_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 cacheThe 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
MybatisPlusInterceptoris 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 = 0Configuration (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=0Logical delete is appropriate for audit‑preserving scenarios; raw SQL must be used to bypass the interceptor for true physical deletion.
MetaObjectHandler fill timing
MetaObjectHandlerdefines 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 updateBean 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
TableInfoHelpermanages 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();Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
