1. 概述
Querydsl 和 JPA Criteria 是 Java 中构建类型安全查询的流行框架。它们都提供静态类型查询表达方式,让编写高效、可维护的数据库交互代码更简单。本文将从多个维度对比这两个框架。
2. 环境搭建
首先需要为测试配置依赖项。所有示例将使用 HyperSQL Database:
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.7.1</version>
</dependency>
我们将使用 JPAMetaModelEntityProcessor 和 JPAAnnotationProcessor 生成元数据。为此添加 maven-processor-plugin 配置:
<plugin>
<groupId>org.bsc.maven</groupId>
<artifactId>maven-processor-plugin</artifactId>
<version>5.0</version>
<executions>
<execution>
<id>process</id>
<goals>
<goal>process</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<processors>
<processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</processors>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>6.2.0.Final</version>
</dependency>
</dependencies>
</plugin>
配置 EntityManager 属性:
<persistence-unit name="com.baeldung.querydsl.intro">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/>
<property name="hibernate.connection.url" value="jdbc:hsqldb:mem:test"/>
<property name="hibernate.connection.username" value="sa"/>
<property name="hibernate.connection.password" value=""/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
</properties>
</persistence-unit>
2.1. JPA Criteria
使用 EntityManager 需要指定 JPA 实现依赖。选择最流行的 Hibernate:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.2.0.Final</version>
</dependency>
添加代码生成支持依赖 Annotation Processor:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>6.2.0.Final</version>
</dependency>
2.2. Querydsl
由于要与 EntityManager 配合使用,仍需包含前述依赖。额外添加 Querydsl 依赖:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
</dependency>
添加代码生成支持依赖 APT based Source code generation:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<classifier>jakarta</classifier>
<version>5.0.0</version>
</dependency>
3. 简单查询
从无额外逻辑的单实体简单查询开始。使用以下数据模型,根实体为 UserGroup:
@Entity
public class UserGroup {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany(cascade = CascadeType.PERSIST)
private Set<GroupUser> groupUsers = new HashSet<>();
// getters and setters
}
该实体与 GroupUser 建立多对多关系:
@Entity
public class GroupUser {
@Id
@GeneratedValue
private Long id;
private String login;
@ManyToMany(mappedBy = "groupUsers", cascade = CascadeType.PERSIST)
private Set<UserGroup> userGroups = new HashSet<>();
@OneToMany(cascade = CascadeType.PERSIST, mappedBy = "groupUser")
private Set<Task> tasks = new HashSet<>(0);
// getters and setters
}
最后添加 Task 实体,与 User 建立多对一关系:
@Entity
public class Task {
@Id
@GeneratedValue
private Long id;
private String description;
@ManyToOne
private GroupUser groupUser;
// getters and setters
}
3.1. JPA Criteria
查询所有 UserGroup 记录:
@Test
void givenJpaCriteria_whenGetAllTheUserGroups_thenExpectedNumberOfItemsShouldBePresent() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<UserGroup> cr = cb.createQuery(UserGroup.class);
Root<UserGroup> root = cr.from(UserGroup.class);
CriteriaQuery<UserGroup> select = cr.select(root);
TypedQuery<UserGroup> query = em.createQuery(select);
List<UserGroup> results = query.getResultList();
Assertions.assertEquals(3, results.size());
}
通过 EntityManager 的 getCriteriaBuilder() 创建 CriteriaBuilder 实例。接着为 UserGroup 模型创建 CriteriaQuery 实例。调用 EntityManager 的 createQuery() 方法获得 TypedQuery 实例。通过 getResultList() 从数据库获取实体列表。结果集合中包含预期数量的记录。
3.2. Querydsl
准备 JPAQueryFactory 实例用于构建查询:
@BeforeEach
void setUp() {
em = emf.createEntityManager();
em.getTransaction().begin();
queryFactory = new JPAQueryFactory(em);
}
使用 Querydsl 执行相同查询:
@Test
void givenQueryDSL_whenGetAllTheUserGroups_thenExpectedNumberOfItemsShouldBePresent() {
List<UserGroup> results = queryFactory.selectFrom(QUserGroup.userGroup).fetch();
Assertions.assertEquals(3, results.size());
}
JPAQueryFactory 的 selectFrom() 方法开始构建实体查询。fetch() 将数据库值检索到持久化上下文中。最终获得相同结果,但查询构建过程显著缩短。
4. 过滤、排序与分组
探索框架如何处理过滤、排序和数据聚合查询。
4.1. JPA Criteria
查询所有 UserGroup 实体,按名称过滤(名称需在两个列表之一),结果按名称降序排序。同时按 UserGroup 名称聚合唯一 ID:
@Test
void givenJpaCriteria_whenGetTheUserGroups_thenExpectedAggregatedDataShouldBePresent() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> cr = cb.createQuery(Object[].class);
Root<UserGroup> root = cr.from(UserGroup.class);
CriteriaQuery<Object[]> select = cr
.multiselect(root.get(UserGroup_.name), cb.countDistinct(root.get(UserGroup_.id)))
.where(cb.or(
root.get(UserGroup_.name).in("Group 1", "Group 2"),
root.get(UserGroup_.name).in("Group 4", "Group 5")
))
.orderBy(cb.desc(root.get(UserGroup_.name)))
.groupBy(root.get(UserGroup_.name));
TypedQuery<Object[]> query = em.createQuery(select);
List<Object[]> results = query.getResultList();
assertEquals(2, results.size());
assertEquals("Group 2", results.get(0)[0]);
assertEquals(1L, results.get(0)[1]);
assertEquals("Group 1", results.get(1)[0]);
assertEquals(1L, results.get(1)[1]);
}
基础方法与之前 JPA Criteria 部分相同。使用 multiselect() 替代 selectFrom(),指定返回项。第二个参数用于聚合 UserGroup ID 数量。where() 方法添加查询过滤条件。orderBy() 指定排序字段和类型。groupBy() 指定聚合数据的分组字段。返回的 UserGroup 记录按预期排序且包含聚合数据。
4.2. Querydsl
使用 Querydsl 执行相同查询:
@Test
void givenQueryDSL_whenGetTheUserGroups_thenExpectedAggregatedDataShouldBePresent() {
List<Tuple> results = queryFactory
.select(userGroup.name, userGroup.id.countDistinct())
.from(userGroup)
.where(userGroup.name.in("Group 1", "Group 2")
.or(userGroup.name.in("Group 4", "Group 5")))
.orderBy(userGroup.name.desc())
.groupBy(userGroup.name)
.fetch();
assertEquals(2, results.size());
assertEquals("Group 2", results.get(0).get(userGroup.name));
assertEquals(1L, results.get(0).get(userGroup.id.countDistinct()));
assertEquals("Group 1", results.get(1).get(userGroup.name));
assertEquals(1L, results.get(1).get(userGroup.id.countDistinct()));
}
为实现分组功能,将 selectFrom() 拆分为两个方法:select() 指定分组字段和聚合函数,from() 指定查询实体。where()、orderBy() 和 groupBy() 描述过滤、排序和分组字段。通过更紧凑的语法获得相同结果。
5. 复杂 JOIN 查询
构建连接所有实体的复杂查询。结果包含 UserGroup 实体列表及其关联实体。
准备测试数据:
Stream.of("Group 1", "Group 2", "Group 3")
.forEach(g -> {
UserGroup userGroup = new UserGroup();
userGroup.setName(g);
em.persist(userGroup);
IntStream.range(0, 10)
.forEach(u -> {
GroupUser groupUser = new GroupUser();
groupUser.setLogin("User" + u);
groupUser.getUserGroups().add(userGroup);
em.persist(groupUser);
userGroup.getGroupUsers().add(groupUser);
IntStream.range(0, 10000)
.forEach(t -> {
Task task = new Task();
task.setDescription(groupUser.getLogin() + " task #" + t);
task.setUser(groupUser);
em.persist(task);
});
});
em.merge(userGroup);
});
数据库中包含三个 UserGroups,每个包含十个 GroupUsers,每个 GroupUser 拥有一万个 Tasks。
5.1. JPA Criteria
使用 JPA CriteriaBuider 执行查询:
@Test
void givenJpaCriteria_whenGetTheUserGroupsWithJoins_thenExpectedDataShouldBePresent() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<UserGroup> query = cb.createQuery(UserGroup.class);
query.from(UserGroup.class)
.<UserGroup, GroupUser>join(GROUP_USERS, JoinType.LEFT)
.join(tasks, JoinType.LEFT);
List<UserGroup> result = em.createQuery(query).getResultList();
assertUserGroups(result);
}
join() 方法指定连接实体及其类型。执行后获取结果列表,通过以下代码断言:
private void assertUserGroups(List<UserGroup> userGroups) {
assertEquals(3, userGroups.size());
for (UserGroup group : userGroups) {
assertEquals(10, group.getGroupUsers().size());
for (GroupUser user : group.getGroupUsers()) {
assertEquals(10000, user.getTasks().size());
}
}
}
所有预期记录均从数据库成功检索。
5.2. Querydsl
使用 Querydsl 实现相同目标:
@Test
void givenQueryDSL_whenGetTheUserGroupsWithJoins_thenExpectedDataShouldBePresent() {
List<UserGroup> result = queryFactory
.selectFrom(userGroup)
.leftJoin(userGroup.groupUsers).fetchJoin()
.leftJoin(userGroup.groupUsers.tasks).fetchJoin()
.fetch();
assertUserGroups(result);
}
使用 leftJoin() 方法添加实体连接,每种连接类型有独立方法。两种语法都不冗长,Querydsl 实现可读性稍强。
6. 数据修改
两个框架均支持数据修改。可基于复杂动态条件更新数据。
6.1. JPA Criteria
更新 UserGroup 名称:
@Test
void givenJpaCriteria_whenModifyTheUserGroup_thenNameShouldBeUpdated() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaUpdate<UserGroup> criteriaUpdate = cb.createCriteriaUpdate(UserGroup.class);
Root<UserGroup> root = criteriaUpdate.from(UserGroup.class);
criteriaUpdate.set(UserGroup_.name, "Group 1 Updated using Jpa Criteria");
criteriaUpdate.where(cb.equal(root.get(UserGroup_.name), "Group 1"));
em.createQuery(criteriaUpdate).executeUpdate();
UserGroup foundGroup = em.find(UserGroup.class, 1L);
assertEquals("Group 1 Updated using Jpa Criteria", foundGroup.getName());
}
使用 CriteriaUpdate 实例创建更新查询,设置要更新的字段名和值。调用 executeUpdate() 执行更新。更新实体的名称字段已成功修改。
6.2. Querydsl
使用 Querydsl 更新 UserGroup:
@Test
void givenQueryDSL_whenModifyTheUserGroup_thenNameShouldBeUpdated() {
queryFactory.update(userGroup)
.set(userGroup.name, "Group 1 Updated Using QueryDSL")
.where(userGroup.name.eq("Group 1"))
.execute();
UserGroup foundGroup = em.find(UserGroup.class, 1L);
assertEquals("Group 1 Updated Using QueryDSL", foundGroup.getName());
}
从 queryFactory 调用 update() 创建更新查询,通过 set() 设置实体字段新值。名称更新成功,Querydsl 再次提供更简洁的声明式语法。
7. Spring Data JPA 集成
可在 Spring Data JPA 仓库中使用 Querydsl 和 JPA Criteria 实现动态过滤。 先添加 Spring Data JPA starter 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.3</version>
</dependency>
7.1. JPA Criteria
为 UserGroup 创建 Spring Data JPA 仓库,扩展 JpaSpecificationExecutor:
public interface UserGroupJpaSpecificationRepository
extends JpaRepository<UserGroup, Long>, JpaSpecificationExecutor<UserGroup> {
default List<UserGroup> findAllWithNameInAnyList(List<String> names1, List<String> names2) {
return findAll(specNameInAnyList(names1, names2));
}
default Specification<UserGroup> specNameInAnyList(List<String> names1, List<String> names2) {
return (root, q, cb) -> cb.or(
root.get(UserGroup_.name).in(names1),
root.get(UserGroup_.name).in(names2)
);
}
}
仓库中创建方法,根据参数中的两个名称列表过滤结果:
@Test
void givenJpaSpecificationRepository_whenGetTheUserGroups_thenExpectedDataShouldBePresent() {
List<UserGroup> results = userGroupJpaSpecificationRepository.findAllWithNameInAnyList(
List.of("Group 1", "Group 2"), List.of("Group 4", "Group 5"));
assertEquals(2, results.size());
assertEquals("Group 1", results.get(0).getName());
assertEquals("Group 4", results.get(1).getName());
}
结果列表精确包含预期分组。
7.2. Querydsl
使用 Querydsl Predicate 实现相同功能。为同一实体创建另一个仓库:
public interface UserGroupQuerydslPredicateRepository
extends JpaRepository<UserGroup, Long>, QuerydslPredicateExecutor<UserGroup> {
default List<UserGroup> findAllWithNameInAnyList(List<String> names1, List<String> names2) {
return StreamSupport
.stream(findAll(predicateInAnyList(names1, names2)).spliterator(), false)
.collect(Collectors.toList());
}
default Predicate predicateInAnyList(List<String> names1, List<String> names2) {
return new BooleanBuilder().and(QUserGroup.userGroup.name.in(names1))
.or(QUserGroup.userGroup.name.in(names2));
}
}
QuerydslPredicateExecutor 仅提供 Iterable 作为多结果容器。若需其他类型需自行转换。客户端代码与 JPA 规范非常相似:
@Test
void givenQuerydslPredicateRepository_whenGetTheUserGroups_thenExpectedDataShouldBePresent() {
List<UserGroup> results = userQuerydslPredicateRepository.findAllWithNameInAnyList(
List.of("Group 1", "Group 2"), List.of("Group 4", "Group 5"));
assertEquals(2, results.size());
assertEquals("Group 1", results.get(0).getName());
assertEquals("Group 4", results.get(1).getName());
}
8. 性能对比
Querydsl 最终准备相同条件查询,但引入额外约定。测量该过程对查询性能的影响。 可使用 IDE 功能 或创建 计时扩展 测量执行时间。
多次执行所有测试方法,保存中位数结果:
Method [givenJpaSpecificationRepository_whenGetTheUserGroups_thenExpectedDataShouldBePresent] took 128 ms.
Method [givenQuerydslPredicateRepository_whenGetTheUserGroups_thenExpectedDataShouldBePresent] took 27 ms.
Method [givenJpaCriteria_whenGetAllTheUserGroups_thenExpectedNumberOfItemsShouldBePresent] took 1 ms.
Method [givenQueryDSL_whenGetAllTheUserGroups_thenExpectedNumberOfItemsShouldBePresent] took 3 ms.
Method [givenJpaCriteria_whenModifyTheUserGroup_thenNameShouldBeUpdated] took 13 ms.
Method [givenQueryDSL_whenModifyTheUserGroup_thenNameShouldBeUpdated] took 161 ms.
Method [givenJpaCriteria_whenGetTheUserGroupsWithJoins_thenExpectedDataShouldBePresent] took 887 ms.
Method [givenQueryDSL_whenGetTheUserGroupsWithJoins_thenExpectedDataShouldBePresent] took 728 ms.
Method [givenJpaCriteria_whenGetTheUserGroups_thenExpectedAggregatedDataShouldBePresent] took 5 ms.
Method [givenQueryDSL_whenGetTheUserGroups_thenExpectedAggregatedDataShouldBePresent] took 88 ms.
多数情况下两者执行时间相近。修改操作中 Querydsl 使用 JPQLSerializer 生成 JPQL 查询字符串,导致额外开销。
9. 结论
本文在多种场景下全面对比了 JPA Criteria 和 Querydsl。多数情况下 Querydsl 因更友好的语法成为优选。若项目能接受少量额外依赖,可将其作为提升代码可读性的工具。另一方面,使用 JPA Criteria 也能实现所有功能。
完整源代码见 GitHub。