last() 方法是 MyBatis-Plus 条件构造器中一个特殊而强大的方法,允许在 SQL 语句末尾直接拼接自定义内容。

一、核心概念

1.1 last() 方法的作用

  • 直接拼接 SQL:在生成的 SQL 语句末尾添加自定义 SQL 片段
  • 特殊场景处理:解决标准 API 无法覆盖的数据库特定语法需求
  • 灵活扩展:突破条件构造器的功能限制

1.2 适用场景

  • 添加数据库特定的限制语句(如 MySQL 的 LIMIT
  • 添加数据库特定的锁机制(如 FOR UPDATE
  • 添加数据库特定的优化提示(如 USE INDEX
  • 添加复杂 SQL 片段(如 PROCEDURE ANALYSE()

1.3 方法签名

Children last(boolean condition, String lastSql, Object... params)

二、详细操作步骤

2.1 基础使用

// 添加 LIMIT 子句
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1)
       .last("LIMIT 10");  // 限制返回10条记录

// 添加 FOR UPDATE 锁
wrapper.eq(User::getId, 1001)
       .last("FOR UPDATE");  // 行级锁

2.2 带参数的动态拼接

int pageSize = 10;
int offset = 20;

// 安全参数化拼接
wrapper.last(true, "LIMIT {0} OFFSET {1}", pageSize, offset);

// 生成的SQL:... LIMIT 10 OFFSET 20

2.3 条件化使用


wrapper.eq(User::getActive, 1)
       .last(useOptimization, "USE INDEX (idx_active_status)");  // 按需使用索引提示```

### 2.4 复杂场景示例
```java
// 分页查询(不使用Page对象)
wrapper.orderByDesc(User::getCreateTime)
       .last("LIMIT 10 OFFSET 20");

// 数据库特定分析
wrapper.select("id", "name", "COUNT(*) as total")
       .groupBy(User::getDepartmentId)
       .last("PROCEDURE ANALYSE()");  // MySQL特定分析函数

// 锁机制
wrapper.eq(User::getId, 1001)
       .last("LOCK IN SHARE MODE");  // MySQL共享锁

三、常见错误与解决

3.1 SQL 注入漏洞

// ❌ 危险:用户输入直接拼接
String userInput = request.getParameter("order");
wrapper.last("ORDER BY " + userInput);

// ✅ 安全解决方案
String safeOrder = validateOrderField(userInput); // 白名单校验
wrapper.last("ORDER BY " + safeOrder);

3.2 多次调用冲突

// ❌ 错误:多次调用会覆盖
wrapper.last("LIMIT 10")
       .last("FOR UPDATE"); // 只保留最后的"FOR UPDATE"

// ✅ 正确:合并到单次调用
wrapper.last("LIMIT 10 FOR UPDATE");

3.3 分页插件冲突

// ❌ 错误:与分页插件同时使用
Page<User> page = new Page<>(1, 10);
wrapper.last("LIMIT 5"); // 覆盖分页插件的LIMIT

// ✅ 正确:二选一
// 方案1:使用分页插件
Page<User> page = new Page<>(1, 10);
mapper.selectPage(page, wrapper);

// 方案2:使用last()
wrapper.last("LIMIT 10 OFFSET 20");
mapper.selectList(wrapper);

四、关键注意事项

4.1 安全防护

  1. 永远不要直接拼接用户输入
  2. 使用参数化方式:last("LIMIT {0}", limit)
  3. 对动态内容进行白名单校验

4.2 数据库兼容性

// MySQL
.last("LIMIT 10")

// Oracle
.last("OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY")

// PostgreSQL
.last("LIMIT 10 OFFSET 20")

// SQL Server
.last("OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY")

4.3 位置敏感性

last() 总是在 SQL 语句的最末尾添加内容:

wrapper.select("id", "name")
       .eq("status", 1)
       .last("FOR UPDATE");

// 生成SQL: 
// SELECT id,name FROM user WHERE status=1 FOR UPDATE

五、高级使用技巧

5.1 动态 SQL 构建

public LambdaQueryWrapper<User> buildQuery(SearchRequest request) {
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    
    // 基础条件
    wrapper.eq(StringUtils.isNotBlank(request.getDept()), 
              User::getDepartment, request.getDept());
    
    // 动态添加优化提示
    if (request.isUseIndexHint()) {
        wrapper.last("USE INDEX (idx_dept_status)");
    }
    
    // 动态分页
    if (request.getPageSize() > 0) {
        int offset = (request.getPage() - 1) * request.getPageSize();
        wrapper.last("LIMIT {0} OFFSET {1}", 
                    request.getPageSize(), offset);
    }
    
    return wrapper;
}

5.2 数据库诊断工具

// 表分析(MySQL)
wrapper.last("PROCEDURE ANALYSE()");

// 执行计划解释
wrapper.last("EXPLAIN FORMAT=JSON");

5.3 复杂锁机制

@Transactional
public User lockUserForUpdate(Long userId) {
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(User::getId, userId)
           .last("FOR UPDATE NOWAIT");  // 非阻塞锁
    
    return userMapper.selectOne(wrapper);
}

六、最佳实践与性能优化

6.1 安全最佳实践

场景 安全做法 风险做法
排序字段 白名单校验字段名 直接拼接用户输入
分页参数 last("LIMIT {0} OFFSET {1}", p1, p2) "LIMIT " + pageSize
动态表名 业务层校验表名合法性 直接拼接表名

6.2 性能优化策略

  1. 避免全表锁

    // ❌ 危险:可能导致全表锁
    .last("FOR UPDATE")
    
    // ✅ 优化:确保使用索引
    .eq("id", 1001).last("FOR UPDATE")
    
  2. 分页优化

    // 传统分页(深分页性能差)
    .last("LIMIT 10000, 10")
    
    // 优化方案:基于游标的分页
    .gt("id", lastId).last("LIMIT 10")
    
  3. 索引提示谨慎使用

    // 仅在优化器选择错误时使用
    .last("USE INDEX (idx_created_status)")
    

6.3 多数据库支持方案

public class DbAwareQueryWrapper<T> extends LambdaQueryWrapper<T> {
    private final DbType dbType;

    public DbAwareQueryWrapper(DbType dbType) {
        this.dbType = dbType;
    }

    public void applyLimit(int offset, int limit) {
        switch (dbType) {
            case MYSQL:
                last("LIMIT {0} OFFSET {1}", limit, offset);
                break;
            case ORACLE:
                last("OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, limit);
                break;
            case POSTGRE_SQL:
                last("LIMIT {0} OFFSET {1}", limit, offset);
                break;
            case SQL_SERVER:
                last("OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, limit);
                break;
        }
    }
}

// 使用示例
DbAwareQueryWrapper<User> wrapper = new DbAwareQueryWrapper<>(DbType.MYSQL);
wrapper.applyLimit(20, 10);

七、完整示例

7.1 安全分页查询

public List<User> searchUsers(UserQuery query) {
    // 参数校验
    if (query.getPage() < 1) throw new IllegalArgumentException("Invalid page");
    if (query.getPageSize() < 1 || query.getPageSize() > 100) 
        throw new IllegalArgumentException("Invalid page size");
    
    // 计算偏移量
    int offset = (query.getPage() - 1) * query.getPageSize();
    
    // 构建查询
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(StringUtils.isNotBlank(query.getDept()), 
              User::getDepartment, query.getDept())
           .like(StringUtils.isNotBlank(query.getName()), 
                 User::getName, query.getName() + "%")
           .orderByDesc(User::getCreateTime)
           .last("LIMIT {0} OFFSET {1}", query.getPageSize(), offset);
    
    return userMapper.selectList(wrapper);
}

7.2 高级锁机制

@Transactional
public void transferBalance(Long fromId, Long toId, BigDecimal amount) {
    // 锁定转出账户(悲观锁)
    LambdaQueryWrapper<Account> fromWrapper = new LambdaQueryWrapper<>();
    fromWrapper.eq(Account::getId, fromId)
               .last("FOR UPDATE");
    Account fromAccount = accountMapper.selectOne(fromWrapper);
    
    // 锁定转入账户
    LambdaQueryWrapper<Account> toWrapper = new LambdaQueryWrapper<>();
    toWrapper.eq(Account::getId, toId)
             .last("FOR UPDATE");
    Account toAccount = accountMapper.selectOne(toWrapper);
    
    // 执行转账逻辑
    fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
    toAccount.setBalance(toAccount.getBalance().add(amount));
    
    accountMapper.updateById(fromAccount);
    accountMapper.updateById(toAccount);
}

总结:last() 方法使用矩阵

场景 推荐用法 绝对避免
分页控制 .last("LIMIT {0} OFFSET {1}", size, offset) .last("LIMIT " + userInput)
锁机制 .eq("id", value).last("FOR UPDATE") .last("FOR UPDATE")(无索引条件)
数据库提示 .last("USE INDEX (index_name)") 随意添加未经验证的提示
排序控制 .last("ORDER BY create_time DESC") .last("ORDER BY " + userInput)
动态SQL 配合条件参数:.last(condition, sql) 无条件直接拼接

核心原则

  1. 优先使用 MyBatis-Plus 的标准 API
  2. 仅在标准 API 无法实现时使用 last()
  3. 永远不要信任用户输入
  4. 考虑数据库兼容性
  5. 充分测试所有数据库特定语法

last() 方法是一把双刃剑——用得好可以解决复杂场景问题,用得不当会导致安全漏洞和兼容性问题。遵循本指南的最佳实践,您将能够安全高效地使用这一强大工具。