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.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Why MyBatis-Plus Pro Is Gaining Popularity: Auto-Generated CRUD via BaseController

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.

Architecture comparison: MyBatis vs MyBatis-Plus vs MyBatis-Plus Pro
Architecture comparison: MyBatis vs MyBatis-Plus vs 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

BaseController

is 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.

SQL execution flow: Controller → Service → BaseMapper → SqlSession → Executor → Database
SQL execution flow: Controller → Service → BaseMapper → SqlSession → Executor → Database

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.

Wrapper internal flow: Lambda expression → condition building → SQL generation
Wrapper internal flow: Lambda expression → condition building → SQL generation

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
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.

framework comparisonSpring BootMyBatis-PlusQueryWrapperBaseControllerCRUD automationJava persistenceMyBatis-Plus Pro
Su San Talks Tech
Written by

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.

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.