Querydsl與JPA標準的比較

banq發表於2024-03-22

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=<font>"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<>();
    <font>// getters and setters<i>
}

在此實體中,我們將與GroupUser建立多對多關係:

@Entity
public class GroupUser {
    @Id
    @GeneratedValue
    private Long id;
    private String login;
    @ManyToMany(mappedBy = <font>"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<i>
}

最後,我們將新增一個與我們的使用者進行多對一關聯的任務實體:

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

 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(<font>"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(<font>"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(<font>"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, <font>"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, <font>"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(<font>"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(<font>"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 查詢字串,這會導致額外的開銷。

相關文章