使用DSL构建查询

郭胜凯2025/11/21

说明

在使用数据库操作时,通常需要构建复杂的查询条件。使用DSL(领域特定语言)可以让我们以更直观和类型安全的方式来构建这些查询条件。 下面是如何使用DSL来构建查询的示例。

假设我们使用DSL来优化一下这个示例

多条件查询

@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private StudentMapper studentMapper;

    @Override
    public List<Student> searchStudent(String name, Integer minAge, Integer maxAge, Sex sex) {
        return studentMapper.select(Where
                .where(Student::getName, C.LIKE, name)
                .and(Student::getAge, C.GTE, minAge)
                .and(Student::getAge, C.LTE, maxAge)
                .and(Student::getSex, C.EQ, sex)
        );
    }
}

对应的SQL语句

SELECT
    ...
FROM
    STUDENT
WHERE
    NAME LIKE #{name}
    AND AGE >= #{minAge}
    AND AGE <= #{maxAge}
    AND SEX = #{sex}

注意

此处的SQL语句仅为静态示例,实际生成的SQL中, LIKE 比较符会根据入参进行动态适配。

比如:

  • 当传入的 name 不含通配符(如 "John")时,框架把绑定值改为 %John%,SQL 仍使用 NAME LIKE #{name}
  • 当传入的name参数包含通配符, 比如"%John"时, 生成的SQL会严格使用入参: NAME LIKE #{name}

动态条件查询

假设我们有一些查询条件是可选的,可以根据传入的参数动态添加条件:

@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private StudentMapper studentMapper;

    @Override
    public List<Student> searchStudent(String name, Integer minAge, Integer maxAge, Sex sex) {
        Where where = Where.where();
        if(StringUtil.isNotBlank(name)){
            where.and(Student::getName, C.LIKE, name);
        }
        if(minAge != null){
            where.and(Student::getAge, C.GTE, minAge);
        }
        if(maxAge != null){
            where.and(Student::getAge, C.LTE, maxAge);
        }
        if(sex != null){
            where.and(Student::getSex, C.EQ, sex);
        }
        return studentMapper.select(where);
    }
}

对应的SQL语句

SELECT
    ...
FROM
    STUDENT
<where>
    <if test="name != null and name != ''">
        AND NAME LIKE #{name}
    </if>
    <if test="minAge != null">
        AND AGE >= #{minAge}
    </if>
    <if test="maxAge != null">
        AND AGE &lt;= #{maxAge}
    </if>
    <if test="sex != null">
        AND SEX = #{sex}
    </if>
</where>

注意

此处的SQL语句仅为静态示例,实际生成的SQL中, LIKE 比较符会根据入参进行动态适配。

比如:

  • 当传入的 name 不含通配符(如 "John")时,框架把绑定值改为 %John%,SQL 仍使用 NAME LIKE #{name}
  • 当传入的name参数包含通配符, 比如"%John"时, 生成的SQL会严格使用入参: NAME LIKE #{name}

简化动态条件查询

正如上面的代码所示,我们可以根据传入的参数动态地添加查询条件,从而实现灵活的查询功能。 但随之而来的代码冗长和可读性下降的问题. 因此, 我们对连接符进行了封装, 可以避免这种冗余代码.

@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private StudentMapper studentMapper;

    @Override
    public List<Student> searchStudent(String name, Integer minAge, Integer maxAge, Sex sex) {
        Where where = Where.where()
                .ifAnd(Student::getName, C.LIKE, name)
                .ifAnd(Student::getAge, C.GTE, minAge)
                .ifAnd(Student::getAge, C.LTE, maxAge)
                .ifAnd(Student::getSex, C.EQ, sex);
        return studentMapper.select(where);
    }
}

对应的SQL语句

SELECT
    ...
FROM
    STUDENT
<where>
    <if test="name != null and name != ''">
        AND NAME LIKE #{name}
    </if>
    <if test="minAge != null">
        AND AGE >= #{minAge}
    </if>
    <if test="maxAge != null">
        AND AGE &lt;= #{maxAge}
    </if>
    <if test="sex != null">
        AND SEX = #{sex}
    </if>
</where>

注意

此处的SQL语句仅为静态示例,实际生成的SQL中, LIKE 比较符会根据入参进行动态适配。

比如:

  • 当传入的 name 不含通配符(如 "John")时,框架把绑定值改为 %John%,SQL 仍使用 NAME LIKE #{name}
  • 当传入的name参数包含通配符, 比如"%John"时, 生成的SQL会严格使用入参: NAME LIKE #{name}

通过这种方式,我们可以大大简化动态条件查询的代码,提高代码的可读性和维护性。 但仍然有一些问题, 那就是它不利于现代编辑器的自动填充.

注意: 当使用ifAndifOr来添加条件时, 如果传入的值为null或空字符串, 该条件将不会被添加到查询中. 如果您需要生成IS NULLIS NOT NULL条件, 请使用标准的andor方法.

利用单参方法提升类型安全和自动补全

在现代IDE中, 我们可以利用单参方法来实现更好的类型安全和自动补全支持. 通过这种方式, 我们可以直接引用实体类的属性方法, 避免了参数拼接带来的错误风险.

@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private StudentMapper studentMapper;

    @Override
    public List<Student> searchStudent(String name, Integer minAge, Integer maxAge, Sex sex) {
        return studentMapper.select(Where.where()
                .ifAnd(Student::getName).like(name)
                .ifAnd(Student::getAge).gte(minAge)
                .ifAnd(Student::getAge).lte(maxAge)
                .ifAnd(Student::getSex).eq(sex)
        );
    }
}

对应的SQL语句

同上.

更利于阅读的方法名

虽然上面的方式已经很好地解决了类型安全和自动补全的问题, 并且极大地简化了动态条件查询的代码, 但仍然存在一个问题, 那就是方法名不够直观.

为了解决这个问题, 我们对操作符方法进行了冗余声明, 使其更符合自然语言的表达习惯.

@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private StudentMapper studentMapper;

    @Override
    public List<Student> searchStudent(String name, Integer minAge, Integer maxAge, Sex sex) {
        return studentMapper.select(Where.where()
                .ifAnd(Student::getName).like(name)
                .ifAnd(Student::getAge).greaterThanOrEquals(minAge)
                .ifAnd(Student::getAge).lessThanOrEquals(maxAge)
                .ifAnd(Student::getSex).equalsFor(sex)
        );
    }
}

对应的SQL语句

同上.

至于使用哪种方法, 完全取决于个人喜好和团队的编码规范。无论选择哪种方式, 关键是要确保代码的可读性和可维护性。

总之, 最后这两种方式是我们推荐的使用方式, 它们不仅提高了代码的可读性, 还充分利用了现代IDE的功能, 使得开发过程更加高效和愉快。

空值和空集合

  • ifWhere/ifAnd/ifOr 只忽略 null 和空字符串 ""
  • 空白字符串、数字 0false 和空集合不会被自动忽略,业务应先做参数规范化。
  • eq(null) 生成 IS NULLne(null) 生成 IS NOT NULL
  • 其他比较符传入 null 会抛出 IllegalArgumentException
  • in(emptyCollection) 生成恒假条件 1=0
  • notIn(emptyCollection) 生成恒真条件 1=1

空集合对查询很方便,但用于删除时尤其危险:空的 notIn 不会限制数据范围。

嵌套 AND/OR

使用嵌套 Where 明确控制优先级:

Where roleBranch = Where.or(
    Where.where(User::getRole).eq(Role.TEACHER)
        .and(User::getStatus).eq(Status.ACTIVE),
    Where.where(User::getRole).eq(Role.STUDENT)
        .and(User::getStatus).eq(Status.DISABLED)
);

Where query = Where.where(User::getAge).gte(18)
    .and(roleBranch);

这表示 age >= 18 AND ((role=TEACHER AND status=ACTIVE) OR (role=STUDENT AND status=DISABLED))。混合 AND/OR 时不要依赖默认优先级猜测,应主动分组。

LIKE 参数

当值不包含 % 时,框架会把绑定值调整为 %value%;只要值中已经包含 %,就按原值绑定。例如 like("张") 是包含查询,like("张%") 是前缀查询。SQL 始终使用参数绑定,不会把值拼接进 SQL。

Last Updated 1/13/2026, 4:41:18 PM