在数据库查询中,模糊查询是最常用的功能之一。然而,当查询条件变得复杂多变时,静态SQL往往显得力不从心。今天我们来探讨如何通过动态SQL实现灵活、安全的模糊查询。
一、为什么需要动态SQL模糊查询?
1.1 传统模糊查询的局限性
sql
-- 静态SQL示例
SELECT * FROM users WHERE username LIKE '%张%'
AND email LIKE '%example.com%';
这种写法的问题在于:
-
当某个条件为空时,查询会失效
-
条件组合多变时,需要写大量重复代码
-
难以应对复杂的业务逻辑
1.2 动态SQL的优势
-
灵活性:根据实际参数动态生成SQL
-
可维护性:代码更简洁,易于维护
-
性能优化:避免不必要的查询条件
二、MyBatis动态SQL实现模糊查询
2.1 基础示例:单条件模糊查询
XML
<!-- MyBatis Mapper XML -->
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="keyword != null and keyword != ''">
AND (username LIKE CONCAT('%', #{keyword}, '%')
OR email LIKE CONCAT('%', #{keyword}, '%')
OR phone LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
</select>
2.2 多条件组合模糊查询
XML
<select id="advancedSearch" resultType="User">
SELECT * FROM users
<where>
<!-- 姓名模糊查询 -->
<if test="name != null and name != ''">
AND username LIKE CONCAT('%', #{name}, '%')
</if>
<!-- 邮箱模糊查询 -->
<if test="email != null and email != ''">
AND email LIKE CONCAT('%', #{email}, '%')
</if>
<!-- 电话号码模糊查询(支持中间四位*号) -->
<if test="phonePattern != null and phonePattern != ''">
AND phone LIKE REPLACE(#{phonePattern}, '*', '%')
</if>
<!-- 地址多字段模糊查询 -->
<if test="address != null and address != ''">
AND (
province LIKE CONCAT('%', #{address}, '%')
OR city LIKE CONCAT('%', #{address}, '%')
OR detail LIKE CONCAT('%', #{address}, '%')
)
</if>
</where>
ORDER BY create_time DESC
</select>
2.3 使用<choose>实现条件选择
XML
<select id="smartSearch" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="searchType == 'name' and keyword != null">
AND username LIKE CONCAT('%', #{keyword}, '%')
</when>
<when test="searchType == 'email' and keyword != null">
AND email LIKE CONCAT('%', #{keyword}, '%')
</when>
<when test="searchType == 'phone' and keyword != null">
AND phone LIKE CONCAT('%', #{keyword}, '%')
</when>
<otherwise>
AND status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>
三、Java代码中的动态构建
3.1 使用StringBuilder动态构建SQL
java
// 服务层代码示例
public List<User> dynamicSearch(UserSearchCriteria criteria) {
StringBuilder sql = new StringBuilder("SELECT * FROM users WHERE 1=1");
List<Object> params = new ArrayList<>();
// 姓名模糊查询
if (StringUtils.isNotBlank(criteria.getName())) {
sql.append(" AND username LIKE ?");
params.add("%" + criteria.getName() + "%");
}
// 邮箱模糊查询
if (StringUtils.isNotBlank(criteria.getEmail())) {
sql.append(" AND email LIKE ?");
params.add("%" + criteria.getEmail() + "%");
}
// 分页处理
if (criteria.getPageSize() > 0) {
sql.append(" LIMIT ?, ?");
params.add(criteria.getOffset());
params.add(criteria.getPageSize());
}
return jdbcTemplate.query(sql.toString(), params.toArray(),
new BeanPropertyRowMapper<>(User.class));
}
3.2 使用JPA Specification实现(Spring Data JPA)
java
// 使用Specification构建动态查询
public class UserSpecifications {
public static Specification<User> nameContains(String name) {
return (root, query, cb) ->
StringUtils.isBlank(name) ?
cb.conjunction() :
cb.like(root.get("username"), "%" + name + "%");
}
public static Specification<User> emailContains(String email) {
return (root, query, cb) ->
StringUtils.isBlank(email) ?
cb.conjunction() :
cb.like(root.get("email"), "%" + email + "%");
}
public static Specification<User> multiFieldSearch(String keyword) {
return (root, query, cb) -> {
if (StringUtils.isBlank(keyword)) {
return cb.conjunction();
}
String pattern = "%" + keyword + "%";
return cb.or(
cb.like(root.get("username"), pattern),
cb.like(root.get("email"), pattern),
cb.like(root.get("phone"), pattern)
);
};
}
}
// 使用示例
public List<User> searchUsers(String name, String email) {
return userRepository.findAll(
Specification.where(UserSpecifications.nameContains(name))
.and(UserSpecifications.emailContains(email))
);
}
四、高级技巧与优化
4.1 防止SQL注入
java
// 使用预编译语句,永远不要直接拼接用户输入
String safePattern = "%" + escapeSql(keyword) + "%";
// MyBatis自动处理参数,防止SQL注入
<if test="keyword != null">
AND username LIKE CONCAT('%', #{keyword}, '%')
</if>
4.2 性能优化建议
sql
-- 为经常查询的字段创建索引
CREATE INDEX idx_username ON users(username);
CREATE INDEX idx_email ON users(email);
-- 避免前导通配符导致索引失效的情况
-- 不推荐:LIKE '%keyword%'
-- 推荐:LIKE 'keyword%'(如果业务允许)
4.3 使用全文索引提升模糊查询性能
sql
-- MySQL全文索引示例
ALTER TABLE users ADD FULLTEXT INDEX ft_search (username, email);
-- 使用全文索引进行模糊查询
SELECT * FROM users
WHERE MATCH(username, email) AGAINST('+张* +example*' IN BOOLEAN MODE);
五、实际应用场景
5.1 电商商品搜索
XML
<select id="searchProducts" resultType="Product">
SELECT * FROM products
<where>
<if test="productName != null">
AND product_name LIKE CONCAT('%', #{productName}, '%')
</if>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<if test="minPrice != null">
AND price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND price <= #{maxPrice}
</if>
<!-- 模糊搜索商品描述 -->
<if test="keyword != null">
AND (
product_name LIKE CONCAT('%', #{keyword}, '%')
OR description LIKE CONCAT('%', #{keyword}, '%')
OR tags LIKE CONCAT('%', #{keyword}, '%')
)
</if>
</where>
ORDER BY
<choose>
<when test="sortBy == 'price'">price ${sortOrder}</when>
<when test="sortBy == 'sales'">sales_count DESC</when>
<otherwise>create_time DESC</otherwise>
</choose>
</select>
5.2 日志查询系统
java
public List<Log> searchLogs(LogQuery query) {
StringBuilder sql = new StringBuilder(
"SELECT * FROM system_logs WHERE 1=1");
// 模糊匹配操作内容
if (StringUtils.isNotBlank(query.getContent())) {
sql.append(" AND content LIKE ?");
params.add("%" + query.getContent() + "%");
}
// 模糊匹配用户IP
if (StringUtils.isNotBlank(query.getIp())) {
sql.append(" AND ip_address LIKE ?");
params.add(query.getIp() + "%"); // IP前缀匹配
}
// 时间范围查询
if (query.getStartTime() != null) {
sql.append(" AND create_time >= ?");
params.add(query.getStartTime());
}
return jdbcTemplate.query(sql.toString(),
params.toArray(),
new BeanPropertyRowMapper<>(Log.class));
}
六、最佳实践总结
-
安全性第一:始终使用参数化查询,防止SQL注入
-
性能优化:为频繁查询的字段建立索引,考虑使用全文搜索
-
代码可读性:保持SQL语句的清晰和可维护性
-
适度使用:避免过度复杂的动态SQL,必要时拆分查询
-
测试覆盖:确保各种条件组合都能正确工作
结语
动态SQL模糊查询是现代应用开发中不可或缺的技能。通过合理运用MyBatis动态标签、JPA Specification或自定义SQL构建,我们可以在保证安全性的同时,实现灵活高效的查询功能。记住,好的查询设计不仅能让程序跑得更快,也能让代码更易于维护和扩展。