Querydsl 與 JPA 標準
1. 概述
Querydsl 和 JPA Criteria 是用 Java 建構型別安全查詢的流行框架。它們都提供了表達靜態類型查詢的方法,使得編寫與資料庫互動的高效且可維護的程式碼變得更加容易。在這篇文章中,我們將從不同的角度對它們進行比較。
2. 設定
首先,我們需要為測試設定依賴項和配置。在所有範例中,我們將使用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>
2.1. 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>
2.2.查詢DSL
由於我們要將其與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. 簡單查詢
讓我們從對一個實體的簡單查詢開始,無需任何額外的邏輯。我們將使用下一個資料模型,根實體將是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
}
最後,我們將新增一個與我們的User:
進行多對一關聯的Task
實體:
@Entity
public class Task {
@Id
@GeneratedValue
private Long id;
private String description;
@ManyToOne
private GroupUser groupUser;
// getters and setters
}
3.1. 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()
方法,我們從資料庫中檢索了實體清單。正如我們所看到的,結果集合中存在預期數量的項目。
3.2.查詢DSL
讓我們準備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標準
在此範例中,我們將查詢所有使用名稱過濾它們的UserGroup
實體,名稱應位於兩個清單之一。我們將按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()
multiselect()
,其中我們指定將傳回的所有項目。我們使用此方法的第二個參數來表示UserGroup
ID 的總數。在where()
方法中,我們新增了將套用於查詢的篩選器。
然後我們呼叫orderBy()
方法,指定排序欄位和型別。最後,在groupBy()
方法中,我們指定一個欄位作為聚合資料的鍵。
正如我們所看到的,返回了一些UserGroup
項目。它們按預期順序放置,結果還包含聚合資料。
4.2.查詢DSL
現在,讓我們使用 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()
用於描述篩選、排序和分組欄位。
最後,我們用稍微更緊湊的語法實現了相同的結果。
**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
,每個包含 10 個GroupUsers
。每個GroupUser
將有一萬個Tasks
。
5.1. 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());
}
}
}
正如我們所看到的,所有預期的項目都是從資料庫中檢索的。
5.2.查詢DSL
讓我們使用 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. 修改數據
兩個框架都支援資料修改。我們可以利用它根據複雜和動態的標準來更新資料。讓我們看看它是如何工作的。
6.1. 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
實例,該實例用於建立Query.
我們設定所有欄位名稱和值都將被更新。最後,我們呼叫executeUpdate()
方法來執行更新查詢。正如我們所看到的,更新後的實體中有一個修改後的名稱欄位。
6.2.查詢DSL
現在,讓我們使用 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 提供了稍微更短且更具聲明性的語法。
7. 與 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>
7.1. 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());
}
我們可以看到結果清單完全包含預期的群組。
7.2.查詢DSL
我們可以使用 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());
}
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 標準來實現所有功能。
像往常一樣,完整的源代碼可以在 GitHub 上找到。