Backend Development 14 min read

Mastering MyBatisPlus: 12 Practical Tips to Unlock New Knowledge

This article presents twelve practical MyBatisPlus optimization techniques—including avoiding isNull, specifying select fields, batch operations, using EXISTS, safe ordering, LambdaQuery, between, index‑aware sorting, pagination, null‑handling, performance tracking, enum mapping, logical deletion, and optimistic locking—to help Java developers write cleaner, more efficient backend code.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Mastering MyBatisPlus: 12 Practical Tips to Unlock New Knowledge

Mastering MyBatisPlus: 12 Practical Tips

Introduction

The author compares writing raw MyBatis code to cooking a bland soup, while MyBatisPlus acts like a skilled chef that streamlines preparation, making the resulting code smoother and more flavorful.

Avoid Using isNull for Null Checks

// ❌ Not recommended
LambdaQueryWrapper
wrapper1 = new LambdaQueryWrapper<>();
wrapper1.isNull(User::getStatus);

// ✅ Recommended: use a concrete default value
LambdaQueryWrapper
wrapper2 = new LambdaQueryWrapper<>();
wrapper2.eq(User::getStatus, UserStatusEnum.INACTIVE.getCode());

Using explicit default values improves readability and maintainability.

NULL values can invalidate indexes, increase CPU overhead, and waste storage.

Specify Select Fields Explicitly

// ❌ Not recommended
// Default selects all fields
List
users1 = userMapper.selectList(null);

// ✅ Recommended: specify needed fields
LambdaQueryWrapper
wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getId, User::getName, User::getAge);
List
users2 = userMapper.selectList(wrapper);

Avoids unnecessary network transfer.

Enables index‑covering queries and reduces memory usage.

Replace Loops with Batch Operations

// ❌ Not recommended
for (User user : userList) {
    userMapper.insert(user);
}

// ✅ Recommended
userService.saveBatch(userList, 100); // process 100 records per batch

// ✅ Better: custom batch size
userService.saveBatch(userList, BatchConstants.BATCH_SIZE);

Reduces connection creation/destruction overhead.

Executes within a single transaction for consistency.

Improves throughput by minimizing round‑trips.

Use EXISTS for Sub‑queries

// ❌ Not recommended
wrapper.inSql("user_id", "select user_id from order where amount > 1000");

// ✅ Recommended
wrapper.exists("select 1 from order where order.user_id = user.id and amount > 1000");

// ✅ Better with LambdaQueryWrapper
wrapper.exists(orderService.lambdaQuery()
    .gt(Order::getAmount, 1000)
    .apply("order.user_id = user.id"));

EXISTS leverages indexes and stops after the first match.

IN sub‑queries load all data into memory, hurting performance.

Replace last() with orderBy()

// ❌ Not recommended: SQL injection risk
wrapper.last("ORDER BY " + sortField + " " + sortOrder);

// ✅ Recommended: safe Lambda ordering
wrapper.orderBy(true, true, User::getStatus);
wrapper.orderByAsc(User::getStatus).orderByDesc(User::getCreateTime);

Avoids SQL injection and preserves statement semantics.

Maintains readability and MyBatis‑Plus safety checks.

Use LambdaQuery for Type Safety

// ❌ Not recommended
QueryWrapper
wrapper1 = new QueryWrapper<>();
wrapper1.eq("name", "张三").gt("age", 18);

// ✅ Recommended
LambdaQueryWrapper
wrapper2 = new LambdaQueryWrapper<>();
wrapper2.eq(User::getName, "张三").gt(User::getAge, 18);

// ✅ Fluent style
userService.lambdaQuery()
    .eq(User::getName, "张三")
    .gt(User::getAge, 18)
    .list();

Compile‑time checks prevent misspelled field names.

IDE offers better code completion and refactoring support.

Replace ge/le with between

// ❌ Not recommended
wrapper.ge(User::getAge, 18).le(User::getAge, 30);

// ✅ Recommended
wrapper.between(User::getAge, 18, 30);

// ✅ Dynamic condition
wrapper.between(ageStart != null && ageEnd != null,
               User::getAge, ageStart, ageEnd);

Generates simpler SQL and lets the optimizer handle range queries efficiently.

Improves readability.

Sort by Indexed Fields

// ❌ Not recommended (no index on lastLoginTime)
wrapper.orderByDesc(User::getLastLoginTime);

// ✅ Recommended: primary key or indexed column
wrapper.orderByDesc(User::getId);

// ✅ Better: combine indexed columns
wrapper.orderByDesc(User::getStatus) // status has index
       .orderByDesc(User::getId);   // primary key

Index‑based sorting avoids costly file‑sort operations.

Supports streaming reads.

Set Pagination Parameters Properly

// ❌ Not recommended
wrapper.last("limit 1000"); // fetches too many rows

// ✅ Recommended
Page
page = new Page<>(1, 10);
userService.page(page, wrapper);

// ✅ Better: conditional pagination
Page
result = userService.lambdaQuery()
    .eq(User::getStatus, "active")
    .page(new Page<>(1, 10));

Controls memory usage and improves user‑experience.

Reduces network load.

Handle Null Values in Conditions

// ❌ Not recommended
if (StringUtils.isNotBlank(name)) {
    wrapper.eq("name", name);
}
if (age != null) {
    wrapper.eq("age", age);
}

// ✅ Recommended
wrapper.eq(StringUtils.isNotBlank(name), User::getName, name)
       .eq(Objects.nonNull(age), User::getAge, age);

// ✅ More expressive
wrapper.eq(StringUtils.isNotBlank(name), User::getName, name)
       .eq(Objects.nonNull(age), User::getAge, age)
       .eq(User::getDeleted, false)
       .orderByDesc(User::getCreateTime);

Gracefully skips empty conditions, reducing boilerplate.

Prevents generation of redundant SQL.

Track Query Performance

// ❌ Not recommended: manual timing
public List
listUsers(QueryWrapper
wrapper) {
    long startTime = System.currentTimeMillis();
    List
users = userMapper.selectList(wrapper);
    long endTime = System.currentTimeMillis();
    log.info("Query took: {}ms", (endTime - startTime));
    return users;
}

// ✅ Recommended: try‑with‑resources timer
public List
listUsersWithPerfTrack(QueryWrapper
wrapper) {
    try (PerfTracker.TimerContext ignored = PerfTracker.start()) {
        return userMapper.selectList(wrapper);
    }
}

// Performance tracker utility
@Slf4j
public class PerfTracker {
    private final long startTime;
    private final String methodName;
    private PerfTracker(String methodName) {
        this.startTime = System.currentTimeMillis();
        this.methodName = methodName;
    }
    public static TimerContext start() {
        return new TimerContext(Thread.currentThread().getStackTrace()[2].getMethodName());
    }
    public static class TimerContext implements AutoCloseable {
        private final PerfTracker tracker;
        private TimerContext(String methodName) {
            this.tracker = new PerfTracker(methodName);
        }
        @Override
        public void close() {
            long executeTime = System.currentTimeMillis() - tracker.startTime;
            if (executeTime > 500) {
                log.warn("Slow query warning: method {} took {}ms", tracker.methodName, executeTime);
            }
        }
    }
}

Separates business logic from monitoring.

Ensures timing is recorded even on exceptions.

Enum Mapping

// Define enum
public enum UserStatusEnum {
    NORMAL(1, "正常"),
    DISABLED(0, "禁用");
    @EnumValue
    private final Integer code;
    private final String desc;
}

// Entity uses enum directly
public class User {
    private UserStatusEnum status;
}

// Query example
userMapper.selectList(
    new LambdaQueryWrapper
()
        .eq(User::getStatus, UserStatusEnum.NORMAL)
);

Provides type safety and automatic conversion.

Eliminates magic numbers.

Logical Deletion

@TableLogic
private Integer deleted;

// ✅ Recommended: automatically filter deleted rows
public List
getActiveUsers() {
    return userMapper.selectList(null); // filters deleted=1 automatically
}

// Manual delete (actually updates the flag)
userService.removeById(1L);

Preserves data, supports recovery, and reduces manual delete logic.

Optimistic Locking

public class Product {
    @Version
    private Integer version;
}

// ✅ Recommended: version handled automatically
public boolean reduceStock(Long productId, Integer count) {
    LambdaUpdateWrapper
wrapper = new LambdaUpdateWrapper<>();
    wrapper.eq(Product::getId, productId)
           .ge(Product::getStock, count);
    Product product = new Product();
    product.setStock(product.getStock() - count);
    return productService.update(product, wrapper);
}

Prevents concurrent update conflicts.

Handles version increment automatically.

Increment/Decrement with setIncrBy / setDecrBy

// ❌ Not recommended: raw SQL string
userService.lambdaUpdate()
    .setSql("integral = integral + 10")
    .update();

// ✅ Recommended: type‑safe increment
userService.lambdaUpdate()
    .eq(User::getId, 1L)
    .setIncrBy(User::getIntegral, 10)
    .update();

// ✅ Recommended: type‑safe decrement
userService.lambdaUpdate()
    .eq(User::getId, 1L)
    .setDecrBy(User::getStock, 5)
    .update();

Avoids manual SQL concatenation and injection risks.

Improves code clarity and maintainability.

Conclusion

Just as a well‑crafted soup requires careful ingredients and technique, applying these twelve MyBatisPlus tips turns ordinary code into elegant, high‑performance backend solutions.

Javabackend developmentDatabase OptimizationORMMyBatisPlus
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

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