你是否厌倦了在 Java 代码中拼接冗长且易错的 SQL 字符串?面对复杂的业务查询需求,传统的字符串拼接存在 SQL 注入风险且难以维护,而 JPA Criteria API 又因其繁琐的语法让人望而却步。
QueryDSL 应运而生,它提供了一种类型安全、可读性强的方式来构建动态查询。它不仅能与 JPA 无缝集成,还支持 MongoDB、SQL 等多种数据源,为复杂查询提供了一种优雅的解决方案。
QueryDSL:类型安全的查询构建方式
QueryDSL 是一个开源的 Java 框架,其核心思想是通过代码生成技术创建实体类的元模型(Q 类),开发者利用这些元模型来构建查询。所有字段名和类型都会在编译时被检查,从而杜绝了运行时因拼写错误或类型不匹配引发的异常。
与其他常见查询方式相比,QueryDSL 找到了一个绝佳的平衡点:
- 原生 SQL 拼接:简单直接但缺乏类型安全,容易引发 SQL 注入,维护复杂查询是噩梦。
- JPA Criteria API:类型安全但 API 极其繁琐,代码可读性差,学习曲线陡峭。
- Spring Data JPA 方法名查询:简单查询方便,但面对多变的动态条件束手无策。
- MyBatis 动态 SQL:功能强大但需编写 XML,类型安全性和 IDE 支持较弱。
QueryDSL 则兼具了类型安全和 API 流畅的优点,让动态查询的构建如同拼装积木一样直观。
快速起步:环境配置与基础查询
要开始使用 QueryDSL,首先需要在项目中添加依赖。以下是最新的 Maven 配置示例:
<dependencies>
<!-- QueryDSL核心依赖 -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>5.1.0</version>
</dependency>
<!-- QueryDSL JPA支持 -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.1.0</version>
<classifier>jakarta</classifier><!-- 针对Jakarta EE 9+ -->
</dependency>
<!-- QueryDSL注解处理器 -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.1.0</version>
<classifier>jakarta</classifier>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- Maven插件用于生成Q类 -->
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
对于 Gradle 项目,配置则更为简洁:
plugins {
id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}
dependencies {
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
}
// 配置QueryDSL插件
querydsl {
jpa = true
querydslSourcesDir = 'src/main/generated'
}
项目构建时,QueryDSL 会自动扫描 @Entity 注解的类,并生成对应的 Q 类。例如,对于 User 实体,会生成 QUser 类,其中包含了所有字段的元模型引用,用于后续的类型安全查询。
基础查询操作非常直观:
// 创建JPAQueryFactory(推荐注入使用)
@Repository
public class UserRepositoryImpl {
@PersistenceContext
private EntityManager entityManager;
private final JPAQueryFactory queryFactory;
public UserRepositoryImpl(EntityManager entityManager) {
this.queryFactory = new JPAQueryFactory(entityManager);
}
// 基础查询:查询所有活跃用户
public List<User> findAllActiveUsers() {
QUser user = QUser.user;
return queryFactory
.selectFrom(user)
.where(user.active.eq(true))
.fetch();
}
// 带条件的查询:根据用户名和邮箱查询
public User findUserByUsernameAndEmail(String username, String email) {
QUser user = QUser.user;
return queryFactory
.selectFrom(user)
.where(
user.username.eq(username)
.and(user.email.eq(email))
)
.fetchOne();
}
}
整个过程遵循流畅 API 设计,任何字段名或类型的错误都会在编译时被捕捉。
动态条件构建:Predicate 的强大之处
QueryDSL 真正的威力在于动态构建查询条件。通过 BooleanBuilder 或 ExpressionUtils,你可以轻松应对各种复杂的搜索场景。
使用 BooleanBuilder 动态构建
public List<User> findUsersByCriteria(UserSearchCriteria criteria) {
QUser user = QUser.user;
BooleanBuilder predicate = new BooleanBuilder();
// 动态添加条件
if (StringUtils.hasText(criteria.getUsername())) {
predicate.and(user.username.containsIgnoreCase(criteria.getUsername()));
}
if (StringUtils.hasText(criteria.getEmail())) {
predicate.and(user.email.eq(criteria.getEmail()));
}
if (criteria.getMinAge() != null) {
predicate.and(user.age.goe(criteria.getMinAge()));
}
if (criteria.getMaxAge() != null) {
predicate.and(user.age.loe(criteria.getMaxAge()));
}
if (criteria.getActive() != null) {
predicate.and(user.active.eq(criteria.getActive()));
}
// 处理关联关系条件
if (criteria.getRoleIds() != null && !criteria.getRoleIds().isEmpty()) {
predicate.and(user.roles.any().id.in(criteria.getRoleIds()));
}
return queryFactory
.selectFrom(user)
.where(predicate)
.orderBy(user.createdAt.desc())
.fetch();
}
使用 ExpressionUtils 实现更灵活的组合
对于需要分步构建或逻辑更复杂的条件,ExpressionUtils 提供了强大的工具方法。
public List<User> findUsersWithComplexConditions(ComplexSearchRequest request) {
QUser user = QUser.user;
// 构建基础条件
BooleanExpression baseCondition = user.active.eq(true);
// 构建时间范围条件
BooleanExpression timeCondition = null;
if (request.getStartDate() != null && request.getEndDate() != null) {
timeCondition = user.createdAt.between(
request.getStartDate(),
request.getEndDate()
);
}
// 构建关键字搜索条件(多字段模糊匹配)
BooleanExpression keywordCondition = null;
if (StringUtils.hasText(request.getKeyword())) {
String keyword = "%" + request.getKeyword().toLowerCase() + "%";
keywordCondition = user.username.lower().like(keyword)
.or(user.email.lower().like(keyword))
.or(user.displayName.lower().like(keyword));
}
// 使用ExpressionUtils.allOf组合条件(自动处理null值)
BooleanExpression combinedCondition = ExpressionUtils.allOf(
baseCondition,
timeCondition,
keywordCondition
);
return queryFactory
.selectFrom(user)
.where(combinedCondition)
.fetch();
}
其动态条件构建的核心流程可以概括为下图:

高级查询技巧:分页、排序与复杂关联
分页查询实现
分页是 Web 应用的标配,QueryDSL 可以与 Spring Data 的 Pageable 完美结合。
public Page<User> findUsersWithPagination(UserSearchCriteria criteria, Pageable pageable) {
QUser user = QUser.user;
BooleanBuilder predicate = buildPredicateFromCriteria(criteria);
// 执行分页查询
JPAQuery<User> query = queryFactory
.selectFrom(user)
.where(predicate);
// 获取总记录数
long total = query.fetchCount();
// 应用分页和排序
List<User> content = query
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(getOrderSpecifiers(user, pageable.getSort()))
.fetch();
return new PageImpl<>(content, pageable, total);
}
// 将Spring的Sort转换为QueryDSL的OrderSpecifier
private OrderSpecifier<?>[] getOrderSpecifiers(QUser user, Sort sort) {
if (sort == null || sort.isUnsorted()) {
return new OrderSpecifier[]{user.id.asc()};
}
return sort.stream()
.map(order -> {
PathBuilder<User> entityPath = new PathBuilder<>(User.class, "user");
Path<Object> path = entityPath.get(order.getProperty());
return new OrderSpecifier(
order.isAscending() ? Order.ASC : Order.DESC,
path
);
})
.toArray(OrderSpecifier[]::new);
}
复杂关联查询与 DTO 投影
QueryDSL 对 JPA 关联查询的支持非常出色,并能将结果直接映射到自定义的 DTO,避免不必要的字段传输。
public List<UserDTO> findUsersWithRolesAndPermissions() {
QUser user = QUser.user;
QRole role = QRole.role;
QPermission permission = QPermission.permission;
return queryFactory
.select(Projections.constructor(
UserDTO.class,
user.id,
user.username,
user.email,
Expressions.list(
Projections.constructor(
RoleDTO.class,
role.id,
role.name,
Expressions.list(
Projections.constructor(
PermissionDTO.class,
permission.id,
permission.code,
permission.name
)
).as("permissions")
)
).as("roles")
))
.from(user)
.leftJoin(user.roles, role)
.leftJoin(role.permissions, permission)
.where(user.active.eq(true))
.groupBy(user.id, role.id, permission.id)
.fetch();
}
// DTO类定义(使用Java Record简洁明了)
public record UserDTO(
Long id,
String username,
String email,
List<RoleDTO> roles
) {}
public record RoleDTO(
Long id,
String name,
List<PermissionDTO> permissions
) {}
public record PermissionDTO(
Long id,
String code,
String name
) {}
性能优化与最佳实践
强大的功能需要合理使用,以下是几个关键的优化点。
避免 N+1 查询问题
在关联查询时,务必使用 fetchJoin() 一次性加载所需数据。
// 正确做法:使用fetch join一次性加载关联数据
public List<User> findUsersWithRolesBestPractice() {
QUser user = QUser.user;
QRole role = QRole.role;
return queryFactory
.selectFrom(user)
.leftJoin(user.roles, role).fetchJoin() // 关键:使用fetchJoin
.where(user.active.eq(true))
.distinct() // 由于连接可能导致重复,使用distinct
.fetch();
}
查询结果投影优化
当不需要完整实体时,使用投影(Projections)或 Tuple 可以减少数据传输量,提升性能。
// 使用Tuple接收聚合统计结果
public List<Tuple> findUserStatistics() {
QUser user = QUser.user;
QOrder order = QOrder.order;
return queryFactory
.select(
user.id,
user.username,
order.count().as("orderCount"),
order.totalAmount.sum().as("totalAmount")
)
.from(user)
.leftJoin(user.orders, order)
.groupBy(user.id, user.username)
.having(order.count().gt(0))
.fetch();
}
与 Spring Boot 生态的深度集成
QueryDSL 与 Spring,特别是 Spring Data JPA 的集成极为紧密,让开发更加便捷。
QuerydslPredicateExecutor 接口
这是最常用的集成方式,让 Repository 直接支持 QueryDSL 的 Predicate 作为查询条件。
public interface UserRepository extends
JpaRepository<User, Long>,
QuerydslPredicateExecutor<User> {
// 自定义查询方法
List<User> findByActiveTrue();
}
// 在Service层使用
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> searchUsers(UserSearchCriteria criteria) {
QUser user = QUser.user;
BooleanBuilder predicate = new BooleanBuilder();
// ... 构建条件
// 使用QuerydslPredicateExecutor的方法
return (List<User>) userRepository.findAll(predicate,
PageRequest.of(0, 20, Sort.by("createdAt").descending()));
}
}
QuerydslBinderCustomizer 自定义绑定
对于需要定制化字段绑定逻辑的场景(如全局忽略某些字段、自定义字符串匹配方式),可以实现此接口。
public interface UserRepository extends
JpaRepository<User, Long>,
QuerydslPredicateExecutor<User>,
QuerydslBinderCustomizer<QUser> {
@Override
default void customize(QuerydslBindings bindings, QUser user) {
// 排除密码等敏感字段不被查询
bindings.excluding(user.password, user.salt);
// 自定义字符串字段的默认匹配方式为忽略大小写的包含
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value));
}
}
与 Spring Boot 3.x 的兼容性
最新的 QueryDSL 5.x 版本已完全支持 Spring Boot 3.x 和 Jakarta EE 9+。只需在配置中注入 Jakarta 规范的 EntityManager 即可。
@Configuration
public class QuerydslConfig {
@jakarta.persistence.PersistenceContext
private jakarta.persistence.EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
结语
QueryDSL 的出现,将 数据库 查询从充满风险的字符串操作,转变为一种类型安全、可组合、易维护的编码体验。它让开发者从繁琐的 SQL 拼接中解放出来,能够更专注于业务逻辑本身的表达。
在追求代码质量和开发效率的今天,掌握 QueryDSL 无疑是 Java 后端开发者提升 后端 开发体验的一把利器。如果你在实践过程中有其他心得或疑问,欢迎在云栈社区与大家交流分享。