Java案例如何实现多条件查询?

wen java案例 27

Java案例:如何优雅实现多条件查询?从SQL拼接到MyBatis动态SQL的实战指南

📑 目录导读

  1. 多条件查询的常见痛点
  2. 纯JDBC实现(含防SQL注入)
  3. MyBatis动态SQL(企业级推荐)
  4. Spring Data JPA Specification
  5. 高频问答:面试与实战避坑
  6. 性能优化与最佳实践

多条件查询的常见痛点

在Java企业级开发中,多条件查询(又称“组合查询”、“动态查询”)是最常见的需求之一——用户可以在UI上勾选多个筛选条件,后台需要根据实际输入动态生成SQL,许多开发者初期会陷入这些误区:

Java案例如何实现多条件查询?

  • 暴力拼接字符串:直接String sql = "SELECT * FROM user WHERE 1=1",然后不断append,极易引发SQL注入
  • 代码膨胀:每个查询条件写一个if判断,一个10个条件的查询,代码量翻倍,维护成本极高
  • 索引失效:条件顺序不当或使用了LIKE '%keyword%'导致全表扫描

真实案例:某电商后台管理系统,商品列表需要按“类目”、“价格区间”、“上架时间”、“关键词”等8个条件组合查询,初期采用JDBC拼接,上线一周后因SQL注入被攻破,紧急重构为MyBatis动态SQL。


方案一:纯JDBC实现(含防SQL注入)

如果项目不依赖ORM框架,可以用PreparedStatement+条件容器模式实现安全的多条件查询。

核心代码示例:

public List<User> searchUsers(String name, Integer ageMin, Integer ageMax, String city) {
    StringBuilder sql = new StringBuilder("SELECT * FROM user WHERE 1=1");
    List<Object> params = new ArrayList<>();
    if (name != null && !name.isEmpty()) {
        sql.append(" AND name LIKE ?");
        params.add("%" + name + "%");
    }
    if (ageMin != null) {
        sql.append(" AND age >= ?");
        params.add(ageMin);
    }
    if (ageMax != null) {
        sql.append(" AND age <= ?");
        params.add(ageMax);
    }
    if (city != null && !city.isEmpty()) {
        sql.append(" AND city = ?");
        params.add(city);
    }
    // 执行PreparedStatement
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql.toString())) {
        for (int i = 0; i < params.size(); i++) {
            ps.setObject(i + 1, params.get(i));
        }
        return executeQuery(ps);
    }
}

✅ 优点:

  • 完全避免SQL注入(参数化查询)
  • 不依赖第三方库,适合遗留系统

❌ 缺点:

  • 条件增多时代码冗长
  • 动态排序、分页需额外处理

方案二:MyBatis动态SQL(企业级推荐)

MyBatis提供的<if><where><choose>标签是解决多条件查询的利器,结合<sql>片段可复用。

1 Mapper XML写法

<select id="searchUserByCondition" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%',#{name},'%')
        </if>
        <if test="ageMin != null">
            AND age >= #{ageMin}
        </if>
        <if test="ageMax != null">
            AND age &lt;= #{ageMax}  <!-- 注意小于号转义 -->
        </if>
        <if test="city != null and city != ''">
            AND city = #{city}
        </if>
    </where>
    <if test="orderBy != null and orderBy != ''">
        ORDER BY ${orderBy} ${direction}  <!-- 仅对字段名排序 -->
    </if>
</select>

2 进阶:使用<trim>自定义前缀后缀

<select id="searchWithTrim" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="name != null">AND name LIKE #{name}</if>
        <if test="code != null">AND code = #{code}</if>
    </trim>
</select>

3 动态排序与分页结合

PageHelper.startPage(pageNum, pageSize); // 分页插件
List<User> list = userMapper.searchUserByCondition(userQuery);
PageInfo<User> pageInfo = new PageInfo<>(list);

✅ 优点:

  • 高度可读,与SQL语法接近
  • 内置OGNL表达式,支持复杂逻辑
  • 与Spring Boot无缝集成

❌ 缺点:

  • XML文件可能膨胀(可通过注解@SelectProvider缓解)

方案三:Spring Data JPA Specification

如果使用JPA,可通过Specification接口实现类型安全的动态查询。

核心实现:

public class UserSpecification {
    public static Specification<User> buildQuery(UserQuery query) {
        return (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (query.getName() != null) {
                predicates.add(criteriaBuilder.like(root.get("name"), "%" + query.getName() + "%"));
            }
            if (query.getAgeMin() != null) {
                predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("age"), query.getAgeMin()));
            }
            if (query.getAgeMax() != null) {
                predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("age"), query.getAgeMax()));
            }
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
}

调用方式:

List<User> users = userRepository.findAll(UserSpecification.buildQuery(query));

✅ 优点:

  • 纯Java API,无XML,编译期类型安全检查
  • 适合喜欢面向对象风格的团队

❌ 缺点:

  • 复杂关联查询(JOIN)时Predicate构造较繁琐
  • 需熟悉JPA Criteria API

高频问答:面试与实战避坑

❓ Q1:WHERE 1=1 写法是否影响性能?

A:MySQL查询优化器会自动忽略WHERE 1=1(条件恒为true),因此对性能无影响,但代码可读性较差,推荐使用MyBatis的<where>标签或JPA的Specification自动处理。

❓ Q2:如何防止排序字段的SQL注入?

A:排序字段不能直接拼接字符串!推荐两种方案:

  • 使用白名单校验:只允许预定义的字段名(如"name","age","create_time"
  • MyBatis中用但不信任用户输入,结合枚举限制

❓ Q3:多表关联查询时,动态条件如何处理?

A:建议封装成VO(视图对象),在Mapper中通过<association><collection>映射。

// 商品+分类名查询
public class ProductQuery {
    private String productName;
    private String categoryName; // 关联分类表
    private BigDecimal priceMin;
}
<select id="searchProduct" resultMap="productMap">
    SELECT p.*, c.name as category_name
    FROM product p
    LEFT JOIN category c ON p.category_id = c.id
    <where>
        <if test="productName != null">AND p.name LIKE #{productName}</if>
        <if test="categoryName != null">AND c.name = #{categoryName}</if>
    </where>
</select>

❓ Q4:前端传空字符串与null如何处理?

A:必须区分!

  • 空字符串:表示用户输入了内容但清空(应忽略该条件)
  • null:表示用户未操作(同样忽略) MyBatis的test="param != null and param != ''"可以同时过滤。

性能优化与最佳实践

1 索引设计原则

-- 针对高频查询组合创建复合索引
CREATE INDEX idx_user_age_name ON user(age, name);
-- 注意:LIKE '%keyword' 无法使用索引,改为全文检索或ES

2 分页查询的陷阱

  • 不要使用LIMIT offset,size(大偏移量性能差),改用WHERE id > ? LIMIT size的分页方式
  • 结合子查询提前过滤主键

3 代码结构建议

com.example.query
├── UserQuery.java          // 查询参数DTO
├── QueryBuilder.java       // 查询构建器(可选)
├── UserMapper.java         // MyBatis接口
└── UserRepository.java     // JPA Repository

4 最终决策树

条件 推荐方案
已有MyBatis项目 MyBatis Dynamic SQL
纯JPA项目 Specification + Criteria
老系统JDBC PreparedStatement+参数列表
超复杂查询(如报表) 存储过程/Elasticsearch

通过以上四种方案的详细对比,你会发现MyBatis动态SQL是Java多条件查询的最佳实践——它在SQL灵活性、可维护性、安全性之间取得了完美平衡,建议结合项目现有技术栈选择,但无论哪种方案,核心原则始终是:参数化查询避免注入,动态条件需精准控制,索引与分页不能忽视

实践建议:从明天开始,把你的Controller中的查询参数都封装成*Query的DTO类,并统一在Service层构建条件,一周后你会感激这个重构带来的代码清晰度提升。

抱歉,评论功能暂时关闭!