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 安全防护
- 永远不要直接拼接用户输入
- 使用参数化方式:
last("LIMIT {0}", limit)
- 对动态内容进行白名单校验
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 性能优化策略
避免全表锁:
// ❌ 危险:可能导致全表锁 .last("FOR UPDATE") // ✅ 优化:确保使用索引 .eq("id", 1001).last("FOR UPDATE")
分页优化:
// 传统分页(深分页性能差) .last("LIMIT 10000, 10") // 优化方案:基于游标的分页 .gt("id", lastId).last("LIMIT 10")
索引提示谨慎使用:
// 仅在优化器选择错误时使用 .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) |
无条件直接拼接 |
核心原则:
- 优先使用 MyBatis-Plus 的标准 API
- 仅在标准 API 无法实现时使用
last()
- 永远不要信任用户输入
- 考虑数据库兼容性
- 充分测试所有数据库特定语法
last()
方法是一把双刃剑——用得好可以解决复杂场景问题,用得不当会导致安全漏洞和兼容性问题。遵循本指南的最佳实践,您将能够安全高效地使用这一强大工具。