Querydsl 和 JPA Criteria
1、概览
Querydsl 和 JPA Criteria 是在 Java 中构建类型安全查询的流行框架。它们都提供了以静态类型表达查询的方法,使编写与数据库交互的高效、可维护代码变得更容易。本文将从多个角度对它们进行比较。
2、设置
首先,设置依赖。在所有示例中,都使用 HyperSQL 数据库:
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.7.1</version>
</dependency>
添加 maven-processor-plugin
,使用 JPAMetaModelEntityProcessor
和 JPAAnnotationProcessor
为框架生成元数据。配置如下:
<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>
<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 Provider 指定依赖。这里选择最常用的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 的源码生成依赖,以支持代码生成功能:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<classifier>jakarta</classifier>
<version>5.0.0</version>
</dependency>
3、简单查询
让我们从对一个实体进行简单查询开始,不涉及任何额外的逻辑。
使用下面的数据模型,根实体(Root Entity)是一个 UserGroup
:
@Entity
public class UserGroup {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany(cascade = CascadeType.PERSIST)
private Set<GroupUser> groupUsers = new HashSet<>();
// Getter/Setter
}
在这个实体中,与 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);
// Getter/Setter
}
最后,添加一个 Task
实体,它与 User
是一对一关系:
@Entity
public class Task {
@Id
@GeneratedValue
private Long id;
private String description;
@ManyToOne
private GroupUser groupUser;
// Getter/Setter
}
3.1、JPA Criteria
现在,从数据库中 select
所有 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()
将值从数据库检索到持久化上下文(Persistence Context)中。最后,获得了相同的结果,但查询构建过程却大大缩短了。
4、过滤、排序和分组
现在深入研究一个更复杂的示例,来看看框架如何处理过滤、排序和数据聚合查询。
4.1、JPA Criteria
在这个例子中,查询所有的 UserGroup
实体,使用名 name
对它们进行过滤,name
应位于两个列表中的一个。查询结果将按照 UserGroup
name
降序排序。此外,还根据结果对每个 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()
方法中,添加了将应用于查询的 Filter。
然后,调用 orderBy()
方法,指定排序字段和类型。最后,在 groupBy()
方法中,指定一个字段作为汇总数据的 key。
你可以看到, 返回了一些 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()
方法中,指示 Query Builder 应用于哪个实体。与 JPA Criteria
类似,where()
、orderBy()
和 groupBy()
用于描述筛选、排序和分组字段。
最后,Querydsl 用一种略微紧凑的语法实现了同样的结果。
5、使用 JOIN 的复杂查询
在这个例子中,我们将创建复杂的查询,将所有的实体进行 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);
});
现在,在我们的数据库中,有三个 UserGroup
,每个 UserGroup
包含十个 GroupUser
。每个 GroupUser
有一万个 Task
。
5.1、JPA Criteria
使用 JPACriteriaBuider
进行查询:
@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()
方法指定了要连接的实体及其类型。执行完成后,得到一个 result List。
用下面的代码对其进行断言:
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, groupUser)
.leftJoin(groupUser.tasks, task)
.fetch();
assertUserGroups(result);
}
使用 leftJoin()
方法将连接到另一个实体。所有连接类型都有单独的方法。两种语法的字数都不多。在 Querydsl
的实现中,查询可读性更高一些。
6、修改数据
这两个框架都支持数据修改。可以利用它来根据复杂的动态 Criteria 更新数据。
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 整合
我们可以使用 Querydsl 和 JPA Criteria 在 Spring Data JPA Repository 中实现动态过滤。
添加 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 Repository,继承自 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)
);
}
}
在该 Repository 中,创建了一个方法,该方法可根据参数中的两个 name
列表中的任意一个过滤结果。
调用如下:
@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
实现相同的功能。
为同一个实体创建另一个 Spring Data JPA Repository:
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
作为多个结果的容器。如果想使用其他类型,就必须自己处理转换。
可以看到,该 Repository 的执行代码与 JPA Specification 的代码非常相似:
@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 和 JPA Criteria 的执行时间相近。在修改案例中,Querydsl 使用 JPQLSerializer
并预编译 JPQL 查询字符串,这导致了额外的开销。
9、总结
本文全面比较了 JPA Criteria 和 Querydsl 在不同场景下的表现。在许多情况下,由于 Querydsl 的语法稍显友好,因此它成为了更好的选择。如果项目中多几个依赖关系不是问题,可以将其视为提高代码可读性的好工具。另一方面,也可以使用 JPA Criteria 实现所有功能。
Ref:https://www.baeldung.com/jpa-criteria-querydsl-differences