在用户管理系统和日志分析这类典型业务场景中,模糊查询往往是高频操作。传统LIKE语句虽然简单直接,但缺乏对匹配模式的精确控制,容易导致查询性能低下和结果集过大的问题。MyBatis-Plus提供的likeLeft和likeRight方法,正是为解决这类痛点而生。
模糊查询的核心在于通配符的放置位置,这直接决定了查询的匹配行为和性能特征。假设我们有一个用户表t_user,其中包含username字段存储用户昵称:
sql复制CREATE TABLE t_user (
id BIGINT PRIMARY KEY,
username VARCHAR(64) NOT NULL,
create_time DATETIME
);
我们插入一组测试数据来观察不同方法的实际效果:
java复制// 测试数据准备
List<User> testUsers = Arrays.asList(
new User().setUsername("dev_张三"),
new User().setUsername("test_李四"),
new User().setUsername("admin_王五"),
new User().setUsername("guest_张三丰"),
new User().setUsername("temp_赵六")
);
userMapper.insertBatchSomeColumn(testUsers);
执行以下三种查询方式:
java复制// 1. 全模糊查询(like)
List<User> likeUsers = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.like(User::getUsername, "张三")
);
// 2. 左模糊查询(likeLeft)
List<User> likeLeftUsers = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.likeLeft(User::getUsername, "张三")
);
// 3. 右模糊查询(likeRight)
List<User> likeRightUsers = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.likeRight(User::getUsername, "dev")
);
查询结果对比如下:
| 查询类型 | SQL示例 | 匹配结果 | 适用场景 |
|---|---|---|---|
like |
username LIKE '%张三%' |
"dev_张三", "guest_张三丰" | 内容包含匹配 |
likeLeft |
username LIKE '%张三' |
"dev_张三" | 后缀匹配 |
likeRight |
username LIKE 'dev%' |
"dev_张三" | 前缀匹配 |
性能提示:
likeRight(前缀匹配)通常可以利用索引,而like和likeLeft往往导致全表扫描。在用户量大的系统中,这种差异会非常明显。
用户管理后台常见的搜索需求是按姓名或昵称进行筛选。传统做法是使用全模糊查询:
java复制// 不推荐的做法
List<User> users = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.like(User::getUsername, keyword)
);
这种方式存在两个明显问题:
对于中文姓名搜索,更合理的做法是使用likeRight实现前缀匹配:
java复制// 优化后的前缀搜索
public List<User> searchByPrefix(String prefix) {
return userMapper.selectList(
Wrappers.<User>lambdaQuery()
.likeRight(User::getUsername, prefix)
.last("LIMIT 50") // 限制结果数量
);
}
这种实现方式有几个优势:
username字段有索引时,LIKE '张%'可以利用索引LIMIT避免返回过多数据实际业务中,搜索往往需要结合其他条件。例如搜索特定时间段内注册的、昵称以某前缀开头的用户:
java复制public List<User> searchUsers(String prefix, LocalDateTime start, LocalDateTime end) {
return userMapper.selectList(
Wrappers.<User>lambdaQuery()
.likeRight(User::getUsername, prefix)
.between(User::getCreateTime, start, end)
.orderByDesc(User::getCreateTime)
);
}
对应的SQL执行计划会先利用时间范围缩小数据集,再对缩小后的数据集进行前缀匹配,这种顺序对性能至关重要。
系统日志表通常包含操作流水号、请求ID等字段,这些值往往具有特定的后缀模式。例如:
sql复制CREATE TABLE t_operation_log (
id BIGINT PRIMARY KEY,
trace_id VARCHAR(32) NOT NULL COMMENT '格式: 前缀_时间戳_随机数',
operation VARCHAR(64) NOT NULL,
create_time DATETIME
);
假设我们需要查找所有trace_id以"ERROR"结尾的异常日志,使用likeLeft是最佳选择:
java复制public List<OperationLog> findErrorLogs() {
return operationLogMapper.selectList(
Wrappers.<OperationLog>lambdaQuery()
.likeLeft(OperationLog::getTraceId, "ERROR")
.orderByDesc(OperationLog::getCreateTime)
);
}
生成的SQL条件为trace_id LIKE '%ERROR',这与业务需求完全匹配。相比全模糊查询LIKE '%ERROR%',它不会误匹配中间包含"ERROR"的正常日志。
更复杂的场景是查询特定时间段内、符合特定后缀模式的日志:
java复制public List<OperationLog> searchLogsBySuffix(
String suffix,
LocalDate date
) {
LocalDateTime start = date.atStartOfDay();
LocalDateTime end = date.plusDays(1).atStartOfDay();
return operationLogMapper.selectList(
Wrappers.<OperationLog>lambdaQuery()
.likeLeft(OperationLog::getTraceId, suffix)
.between(OperationLog::getCreateTime, start, end)
.orderByAsc(OperationLog::getCreateTime)
);
}
这种组合查询在大数据量日志系统中尤其重要,它先通过时间范围缩小数据量,再对结果集进行后缀匹配,避免全表扫描。
理解了基本用法后,我们来看几个提升查询效率的实战技巧。
要使likeRight发挥最大性能,需要合理的索引设计。对于用户表的username字段:
sql复制-- 适合前缀搜索的索引
ALTER TABLE t_user ADD INDEX idx_username_prefix (username(10));
-- 适合精确匹配的索引
ALTER TABLE t_user ADD INDEX idx_username (username);
索引长度需要根据实际数据特征确定。对于中文昵称,前10个字符通常能提供较好的区分度。
对于内容较长的字段(如文章内容、详细描述),直接使用模糊查询性能极差。更好的做法是:
LIKEjava复制// 不推荐对大文本使用LIKE
articleMapper.selectList(
Wrappers.<Article>lambdaQuery()
.like(Article::getContent, "关键词")
);
// 推荐做法:使用专门的搜索字段
articleMapper.selectList(
Wrappers.<Article>lambdaQuery()
.likeRight(Article::getSearchKeywords, "关键词")
);
实际业务中,搜索条件往往是动态的。MyBatis-Plus的条件构造器可以灵活组合:
java复制public List<User> dynamicSearch(UserQuery query) {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
if (StringUtils.isNotBlank(query.getKeyword())) {
if (query.isPrefixSearch()) {
wrapper.likeRight(User::getUsername, query.getKeyword());
} else if (query.isSuffixSearch()) {
wrapper.likeLeft(User::getUsername, query.getKeyword());
} else {
wrapper.like(User::getUsername, query.getKeyword());
}
}
if (query.getStartTime() != null) {
wrapper.ge(User::getCreateTime, query.getStartTime());
}
return userMapper.selectList(wrapper);
}
这种动态构建方式既保持了代码的清晰性,又满足了业务灵活性需求。