Querydsl与JPA标准的比较

Querydsl和JPA Criteria是用 Java 构建类型安全查询的流行框架。它们都提供了表达静态类型查询的方法,使得编写与数据库交互的高效且可维护的代码变得更加容易。在这篇文章中,我们将从不同的角度对它们进行比较。

首先,我们需要为测试设置依赖项和配置。在所有示例中,我们将使用HyperSQL 数据库:

<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>

JPA标准
要使用EntityManager,我们需要指定任何 JPA 提供程序的依赖关系。让我们选择Hibernate作为最流行的:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.2.0.Final</version>
</dependency>

为了支持代码生成功能,我们将添加注释处理器依赖项:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <version>6.2.0.Final</version>
</dependency>

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>

简单查询
让我们从对一个实体的简单查询开始,无需任何额外的逻辑。我们将使用下一个数据模型,根实体将是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
}

最后,我们将添加一个与我们的用户进行多对一关联的任务实体:

@Entity
public class Task {
    @Id
    @GeneratedValue
    private Long id;
    private String description;
    @ManyToOne
    private GroupUser groupUser;
    // getters and setters
}

 JPA标准
现在,让我们从数据库中选择所有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()方法,我们从数据库中检索了实体列表。正如我们所看到的,结果集合中存在预期数量的项目。

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()将数据库中的值检索到持久性上下文中。最后,我们获得了相同的结果,但我们的查询构建过程明显缩短了。

过滤、排序和分组
让我们深入研究一个更复杂的示例。我们将探讨我们的框架如何处理过滤、排序和数据聚合查询。

JPA标准
在此示例中,我们将查询所有使用名称过滤它们的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项目。它们按预期顺序放置,结果还包含聚合数据。

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()方法中,我们指示查询构建器应应用哪个实体。与 JPA Criteria 类似,where()、orderBy()和groupBy()用于描述过滤、排序和分组字段。

最后,我们用稍微更紧凑的语法实现了相同的结果。

使用 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,每个包含 10 个GroupUsers。每个GroupUser将有一万个任务。

JPA标准
现在,让我们使用 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());
        }
    }
}
正如我们所看到的,所有预期的项目都是从数据库中检索的。

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 实现中,我们的查询稍微更具可读性。

修改数据
两个框架都支持数据修改。我们可以利用它根据复杂和动态的标准更新数据。让我们看看它是如何工作的。

JPA标准
让我们用新名称更新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()方法来运行更新查询。正如我们所看到的,更新后的实体中有一个修改后的名称字段。

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());
}

我们通过调用update()方法从queryFactory创建更新查询。然后,我们使用set()方法为实体字段设置新值。我们已成功更新名称。与前面的示例类似,Querydsl 提供了稍微更短且更具声明性的语法。

与 Spring Data JPA 集成
我们可以使用 Querydsl 和 JPA Criteria 在 Spring Data JPA 存储库中实现动态过滤。让我们首先添加Spring Data JPA 启动器依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.2.3</version>
</dependency>

JPA标准
让我们为扩展JpaSpecificationExecutor的UserGroup创建一个 Spring Data JPA 存储库:

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());
}

我们可以看到结果列表完全包含预期的组。

Querydsl
我们可以使用 Querydsl Predicate实现相同的功能。让我们为同一实体创建另一个 Spring Data JPA 存储库:

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());
}

性能
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 查询字符串,这会导致额外的开销。