Why MyBatis-Plus Pro Is Gaining Popularity: Auto-Generated CRUD via BaseController
This article analyzes MyBatis-Plus Pro, a framework that extends MyBatis-Plus by providing a BaseController to auto-generate CRUD APIs, detailing its architecture, reflection-based query wrapper generation, pros/cons, ideal use cases, and best practices for avoiding common pitfalls.
Introduction
MyBatis is widely used but often requires verbose XML for single-table CRUD operations, leading to repetitive code and low development efficiency. A typical scenario involves writing XML mappers with select, insert, update, and delete statements for each entity, plus pagination and list queries.
The author recounts a developer's frustration: "Why do I need to write over 100 lines of XML for a simple user table? Single-table operations are just CRUD and pagination—can't it be done in one line of code?"
MyBatis-Plus (MP) alleviates this by providing BaseMapper with built-in CRUD methods, eliminating XML for basic operations. However, developers still need to write Service and Controller layers, resulting in repeated boilerplate code across modules.
MyBatis-Plus Pro goes further: by inheriting a single BaseController, all CRUD, pagination, listing, sorting, and conditional queries become available out of the box.
Why We Need MyBatis-Plus Pro
Pain Point 1: Service Layer Repetition
Even with MP's IService interface, each ServiceImpl must call BaseMapper methods, not eliminating the underlying repetitive logic.
Pain Point 2: Controller Layer Boilerplate
Adding a new module requires copying and modifying Controller code—class names, paths, parameters. For seven or eight modules, this takes hours and is error-prone.
Pain Point 3: Inconsistent Code Quality
Different developers implement validation and exception handling differently, leading to production errors.
Pain Point 4: Repetitive Conditional Query Code
Each Controller configures QueryWrapper with eq, like, orderBy —logic that is nearly identical across modules.
The guiding principle: "The best code is no code." Automating these repetitive tasks frees 90% of effort for core business logic.
What Is MyBatis-Plus Pro?
MyBatis-Plus Pro is an enhancement built on top of MyBatis-Plus , adhering to "stand on the shoulders of giants"—only enhancing, not changing. It adds template functionality on top of MP, using Spring Boot to extract common features into a ready-to-use base class system.
Core idea: After MyBatis-Plus eliminated Mapper-layer code, Pro continues to eliminate Service and Controller layer repetition , letting developers focus solely on business logic.
Note: MyBatis-Plus Pro currently has two interpretations: community secondary encapsulations (like the version by Fugui) and official Pro features added in MP iterations. This article focuses on the former's design philosophy and practical value.
The article includes an architecture diagram illustrating the relationship among MyBatis, MyBatis-Plus, and MyBatis-Plus Pro.
Quick Start
Step 1: Add Dependency
MyBatis-Plus Pro builds on MP, so first include the MP starter:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.15</version>
</dependency>Official v3.5.15 released on 2025-11-30 supports Spring Boot 4.0.0 and Jackson 3.0; recommend latest stable version.
Step 2: Utility Class
The utility class provides three key functions:
CamelCase to underscore conversion — solves automatic mapping between Java field names and database column names.
Reflection-based entity field value extraction — dynamically retrieves field values for building QueryWrapper.
Automatic QueryWrapper generation from entity — zero-code query condition construction.
Core logic: getQueryWrapper uses reflection ( entity.getClass().getDeclaredFields()) to iterate fields, skips final fields, uses field.setAccessible(true) to access private fields, converts camelCase field names to underscore column names, and adds eq conditions for non-null values.
public class ApprenticeUtil {
private static Pattern humpPattern = Pattern.compile("[A-Z]");
private static Pattern linePattern = Pattern.compile("_(\\w)");
// camelCase to underscore (userName -> user_name)
public static String humpToLine(String str) {
Matcher matcher = humpPattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
}
matcher.appendTail(sb);
return sb.toString();
}
// underscore to camelCase (user_name -> userName)
public static String lineToHump(String str) {
str = str.toLowerCase();
Matcher matcher = linePattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}
// Generate QueryWrapper from non-null fields — core method
public static <E> QueryWrapper<E> getQueryWrapper(E entity) {
Field[] fields = entity.getClass().getDeclaredFields();
QueryWrapper<E> eQueryWrapper = new QueryWrapper<>();
for (Field field : fields) {
// ignore final fields
if (Modifier.isFinal(field.getModifiers())) {
continue;
}
field.setAccessible(true);
try {
Object obj = field.get(entity);
if (obj != null) {
String name = humpToLine(field.getName());
eQueryWrapper.eq(name, obj);
}
} catch (IllegalAccessException e) {
return null;
}
}
return eQueryWrapper;
}
}Step 3: Generic BaseController
BaseControlleris the engine, integrating core CRUD logic:
public class BaseController<S extends IService<T>, T> {
@Autowired
protected S baseService;
@PostMapping("add")
public Result add(@RequestBody T entity) {
return Result.success(baseService.save(entity));
}
@PostMapping("update")
public Result update(@RequestBody T entity) {
return Result.success(baseService.updateById(entity));
}
@GetMapping("delete")
public Result delete(String id) {
return Result.success(baseService.removeById(id));
}
@GetMapping("detail")
public Result detail(String id) {
return Result.success(baseService.getById(id));
}
@PostMapping("list")
public Result list(@RequestBody T entity) {
QueryWrapper<T> wrapper = ApprenticeUtil.getQueryWrapper(entity);
if (wrapper == null) {
wrapper = new QueryWrapper<>();
}
return Result.success(baseService.list(wrapper));
}
@PostMapping("page")
public Result page(@RequestBody PageDto<T> pageDto) {
T entity = pageDto.getEntity();
QueryWrapper<T> wrapper = ApprenticeUtil.getQueryWrapper(entity);
if (wrapper == null) {
wrapper = new QueryWrapper<>();
}
IPage<T> page = new Page<>(pageDto.getPageNo(), pageDto.getPageSize());
return Result.success(baseService.page(page, wrapper));
}
}Execution flow: Client POST to /add → Spring parses JSON to entity → baseService.save(entity) calls MP's IService implementation, which holds a BaseMapper. save auto-detects: null primary key → insert; non-null → update. MP injects basic CRUD at startup, so zero SQL written.
The list method uses ApprenticeUtil.getQueryWrapper(entity) to auto-build conditions. Example: passing a User with only name="Zhang San" generates WHERE name = 'Zhang San'. All non-null fields become conjunctive conditions — "query whatever fields are provided, zero code."
Step 4: Actual Usage
Create a ProductController by simply extending BaseController:
// Entity
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
private LocalDateTime createTime;
}
// Service interface
public interface ProductService extends IService<Product> {}
// Service implementation (can be auto-generated)
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {}
// Controller — just this one line!
@RestController
@RequestMapping("/product")
public class ProductController extends BaseController<ProductService, Product> {}A full CRUD Controller requires only inheritance — add, update, delete, detail, list, pagination all auto-generated, zero code written.
Deep Dive into Underlying Principles
4.1 Complete SQL Execution Chain
A flowchart illustrates the full process from Controller call to database result return.
4.2 BaseMapper Injection Mechanism
MP's magic works via:
Dynamic Proxy : At Spring startup, all BaseMapper interfaces are proxied via MapperProxyFactory using JDK dynamic proxy. Calling productMapper.selectById(1L) invokes MapperProxy.invoke, which intercepts and analyzes the method as a query operation.
Auto-generated MappedStatement : MP's SqlInjector generates MappedStatement for each generic method at startup. MappedStatement is MyBatis's low-level object containing SQL template, parameter types, result types — equivalent to one XML SQL statement. These are injected into MyBatis Configuration, enabling XML-free execution.
Interceptor Chain : MybatisPlusInterceptor builds a chain injecting enhancements (pagination, optimistic locking, multi-tenancy) before Executor runs, using interceptor and chain-of-responsibility patterns.
Complete Method Resolution : MP distinguishes generic vs custom methods and finds the corresponding MappedStatement for execution.
4.3 Wrapper Condition Constructor Internals
A flowchart details the internal execution of the condition constructor.
Lambda expressions provide type safety: using User::getName instead of string "name" catches field name errors at compile time, not runtime — implementing "convention over configuration."
Pros and Cons
5.1 Advantages
Exponential development efficiency boost : A module from interface to working CRUD needs only inheriting BaseController and injecting Service — 5 lines of code. Compared to native MyBatis (XML, Mapper, Service, ServiceImpl, Controller), workload reduced by 80%+.
Unified code style, lower maintenance : All modules share the same BaseController logic. New hires don't need to study each Controller's quirks — structure and handling are identical.
Zero XML configuration : MP already removed XML for base CRUD; Pro removes Controller-layer config too.
Feature-complete, out-of-the-box : Pagination plugin built-in; pass pageNo and pageSize for full pagination results. Wrapper supports eq, like, between, orderBy, covering most business scenarios.
Non-invasive design : Pro enhances MP; you can still write native MyBatis Mapper and XML for complex SQL — fully compatible.
5.2 Disadvantages
Complex multi-table joins remain painful : For three-plus table joins, Wrapper code becomes more convoluted than SQL. Wrapper is designed for single-table conditions, not multi-table JOIN semantics.
Potential performance pitfalls : BaseMapper methods like selectById, selectList default to SELECT *. In MySQL optimization, SELECT * is an anti-pattern — increases network/memory overhead, especially with TEXT/BLOB columns. Official docs warn of minor startup injection overhead, but extreme scenarios need caution.
Magic string risks : QueryWrapper<User>().eq("id", 1) hardcodes column names as strings. Database column rename goes undetected until runtime. Hence official push for LambdaWrapper using method references.
Over-encapsulation concerns : Some feel MP invades Service layer; when business logic gets complex, generic methods become constraints.
SQL readability and tuning difficulty : Complex Wrapper-generated SQL is less intuitive for debugging. Without MP internals knowledge, locating slow queries is harder than reading explicit XML SQL.
Use Cases and Selection Guide
6.1 Recommended Scenarios
Small/medium projects, rapid iteration : High change rate, speed priority, less extreme performance demand.
Admin/back-office systems : Core functionality is data CRUD, many modules, simple business logic.
Projects dominated by basic CRUD : 80%+ single-table operations (config management, dictionary, master data).
Small teams wanting consistent style : Quick scaffolding with uniform structure.
6.2 Scenarios to Avoid or Use Cautiously
Core business, extreme SQL performance required : Trading, payment, high-concurrency order systems needing per-SQL tuning. MP's SELECT * can bottleneck.
Complex reporting/statistics systems : Dozens of lines of complex SQL, multi-table joins, subqueries, aggregations. Native SQL is clearer and more controllable.
Security-sensitive systems requiring SQL audit : Finance, government where every SQL must be reviewed. Auto-generated SQL may not meet audit standards; use handwritten XML.
6.3 Architecture Selection Comparison
CRUD Development Speed : Native MyBatis = Slow (manual SQL); MyBatis-Plus = Fast (BaseMapper); MyBatis-Plus Pro = Very Fast (fully automatic)
Code Volume : Native MyBatis = High; MyBatis-Plus = Less; MyBatis-Plus Pro = Very Low
Multi-table SQL Flexibility : Native MyBatis = High (full control); MyBatis-Plus = High (native compatible); MyBatis-Plus Pro = High (native compatible)
Learning Curve : Native MyBatis = Low; MyBatis-Plus = Medium; MyBatis-Plus Pro = Medium
Customization Level : Native MyBatis = High; MyBatis-Plus = High; MyBatis-Plus Pro = Medium-High
Code Style Consistency : Native MyBatis = Poor; MyBatis-Plus = Average; MyBatis-Plus Pro = Good
Suitable Projects : Native MyBatis = All; MyBatis-Plus = All; MyBatis-Plus Pro = Medium/Large + Rapid Dev
Database Control : Native MyBatis = Full Control; MyBatis-Plus = Single-table encapsulated; MyBatis-Plus Pro = Single-table encapsulated
This comparison helps make informed technical choices — each framework has strengths and limits; choose based on actual project context.
Pitfall Avoidance Guide
Real-world lessons:
Pitfall 1: Don't Overuse Generic Query Methods
Using selectList for everything causes SELECT * on high-frequency interfaces, hurting performance. Important queries should explicitly specify columns via select().
Pitfall 2: Prefer Lambda over String Version
Use LambdaQueryWrapper instead of QueryWrapper; method references ensure type safety and avoid implicit errors on column rename.
// ✅ Recommended (type-safe)
userMapper.selectList(Wrappers.<User>lambdaQuery()
.eq(User::getStatus, 1)
.like(User::getUsername, "zhang")
.orderByDesc(User::getCreateTime));
// ❌ Not recommended (string prone to typos)
userMapper.selectList(Wrappers.<User>query()
.eq("status", 1)
.like("username", "zhang"));Pitfall 3: Always Use MP's Pagination Plugin
Don't write LIMIT manually. MP's built-in plugin uses physical pagination with multi-database compatibility — cleaner code, better performance.
Pitfall 4: Abandon Wrapper for Complex SQL, Return to Native
For three-plus table joins, don't struggle — write SQL directly in Mapper XML. Clear, controllable, tunable — that's real productivity.
Pitfall 5: Follow CamelCase Naming for Entity Fields
MP's auto-mapping relies on camelCase convention; stick to standard naming to avoid extra configuration.
Conclusion
MyBatis-Plus Pro's core goal: liberate developers from massive repetitive code. It inherits MP's non-invasive, high-efficiency philosophy, and through the clever BaseController design, automates Controller and Service layer repetition. Combined with reflection-based utilities and Wrapper condition constructors, it enables automatic conditional queries based on non-null entity fields — a qualitative leap in developer experience.
Of course, there is no silver bullet. Pro falls short in complex multi-table joins and extreme performance tuning, but as the industry acknowledges: it solves 80% of simple CRUD repetition; the remaining 20% complex scenarios can still use handwritten SQL. Combining both is best practice.
MyBatis-Plus enhances MyBatis without changing it, simplifying development while retaining all MyBatis flexibility.
Final thought for all developers: Improving efficiency isn't laziness — it's redirecting energy to higher-value business work.
MyBatis-Plus Official Docs: https://baomidou.com MyBatis-Plus Pro Project: https://gitee.com/nirui-gitee/mybatis-plus-pro
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
