From bd5512454dc4614b694970dcf162550c45695a42 Mon Sep 17 00:00:00 2001 From: "ICKostiantyn.Ivanov" Date: Mon, 11 Mar 2024 12:35:51 +0100 Subject: [PATCH] BAEL-6163 - Add code examples for the article "Querydsl vs. JPA Criteria" --- persistence-modules/querydsl/pom.xml | 47 ++- .../entities/GroupUser.java | 59 ++++ .../querydslvsjpacriteria/entities/Task.java | 43 +++ .../entities/UserGroup.java | 47 +++ .../UserGroupJpaSpecificationRepository.java | 25 ++ .../UserGroupQuerydslPredicateRepository.java | 27 ++ .../QuerydslVSJPACriteriaIntegrationTest.java | 270 ++++++++++++++++++ .../TimingExtension.java | 34 +++ 8 files changed, 546 insertions(+), 6 deletions(-) create mode 100644 persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/GroupUser.java create mode 100644 persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/Task.java create mode 100644 persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/UserGroup.java create mode 100644 persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/repositories/UserGroupJpaSpecificationRepository.java create mode 100644 persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/repositories/UserGroupQuerydslPredicateRepository.java create mode 100644 persistence-modules/querydsl/src/test/java/com/baeldung/querydslvsjpacriteria/QuerydslVSJPACriteriaIntegrationTest.java create mode 100644 persistence-modules/querydsl/src/test/java/com/baeldung/querydslvsjpacriteria/TimingExtension.java diff --git a/persistence-modules/querydsl/pom.xml b/persistence-modules/querydsl/pom.xml index 72cdb3a48a..c40b97d1b9 100644 --- a/persistence-modules/querydsl/pom.xml +++ b/persistence-modules/querydsl/pom.xml @@ -15,6 +15,18 @@ + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-test + test + ${spring-boot.version} + com.querydsl @@ -36,6 +48,16 @@ ${hibernate-core.version} compile + + org.hibernate + hibernate-jpamodelgen + ${hibernate-core.version} + + + org.bsc.maven + maven-processor-plugin + 5.0 + commons-dbcp commons-dbcp @@ -103,24 +125,36 @@ ${maven-compiler-plugin.version} -proc:none + 17 + 17 - - com.mysema.maven - apt-maven-plugin - ${apt-maven-plugin.version} + org.bsc.maven + maven-processor-plugin + 5.0 + process process + generate-sources - target/generated-sources/java - com.querydsl.apt.jpa.JPAAnnotationProcessor + + org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor + com.querydsl.apt.jpa.JPAAnnotationProcessor + + + + org.hibernate + hibernate-jpamodelgen + ${hibernate-core.version} + + @@ -132,6 +166,7 @@ 1.4 1.1.3 6.4.2.Final + 3.2.3 \ No newline at end of file diff --git a/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/GroupUser.java b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/GroupUser.java new file mode 100644 index 0000000000..32f35cbeb7 --- /dev/null +++ b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/GroupUser.java @@ -0,0 +1,59 @@ +package com.baeldung.querydslvsjpacriteria.entities; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; + +@Entity +public class GroupUser { + + @Id + @GeneratedValue + private Long id; + + private String login; + + @ManyToMany(mappedBy = "groupUsers", cascade = CascadeType.PERSIST) + private Set userGroups = new HashSet<>(); + + @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "groupUser") + private Set tasks = new HashSet<>(0); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public Set getUserGroups() { + return userGroups; + } + + public void setUserGroups(Set userGroups) { + this.userGroups = userGroups; + } + + public Set getTasks() { + return tasks; + } + + public void setTasks(Set tasks) { + this.tasks = tasks; + } +} diff --git a/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/Task.java b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/Task.java new file mode 100644 index 0000000000..ddb522711c --- /dev/null +++ b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/Task.java @@ -0,0 +1,43 @@ +package com.baeldung.querydslvsjpacriteria.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +@Entity +public class Task { + + @Id + @GeneratedValue + private Long id; + + private String description; + + @ManyToOne + private GroupUser groupUser; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public GroupUser getUser() { + return groupUser; + } + + public void setUser(GroupUser groupUser) { + this.groupUser = groupUser; + } +} diff --git a/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/UserGroup.java b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/UserGroup.java new file mode 100644 index 0000000000..f6a8b52983 --- /dev/null +++ b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/entities/UserGroup.java @@ -0,0 +1,47 @@ +package com.baeldung.querydslvsjpacriteria.entities; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; + +@Entity +public class UserGroup { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToMany(cascade = CascadeType.PERSIST) + private Set groupUsers = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getGroupUsers() { + return groupUsers; + } + + public void setGroupUsers(Set groupUsers) { + this.groupUsers = groupUsers; + } +} diff --git a/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/repositories/UserGroupJpaSpecificationRepository.java b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/repositories/UserGroupJpaSpecificationRepository.java new file mode 100644 index 0000000000..72d14145af --- /dev/null +++ b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/repositories/UserGroupJpaSpecificationRepository.java @@ -0,0 +1,25 @@ +package com.baeldung.querydslvsjpacriteria.repositories; + +import java.util.List; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import com.baeldung.querydslvsjpacriteria.entities.UserGroup; +import com.baeldung.querydslvsjpacriteria.entities.UserGroup_; + +public interface UserGroupJpaSpecificationRepository extends JpaRepository, + JpaSpecificationExecutor { + + default List findAllWithNameInAnyList(List names1, List names2) { + return findAll(specNameInAnyList(names1, names2)); + } + + default Specification specNameInAnyList(List names1, List names2) { + return (root, q, cb) -> cb.or( + root.get(UserGroup_.name).in(names1), + root.get(UserGroup_.name).in(names2) + ); + } +} diff --git a/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/repositories/UserGroupQuerydslPredicateRepository.java b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/repositories/UserGroupQuerydslPredicateRepository.java new file mode 100644 index 0000000000..22d36e9cc8 --- /dev/null +++ b/persistence-modules/querydsl/src/main/java/com/baeldung/querydslvsjpacriteria/repositories/UserGroupQuerydslPredicateRepository.java @@ -0,0 +1,27 @@ +package com.baeldung.querydslvsjpacriteria.repositories; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; + +import com.baeldung.querydslvsjpacriteria.entities.QUserGroup; +import com.baeldung.querydslvsjpacriteria.entities.UserGroup; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Predicate; + +public interface UserGroupQuerydslPredicateRepository extends JpaRepository, QuerydslPredicateExecutor { + + default List findAllWithNameInAnyList(List names1, List names2) { + return StreamSupport + .stream(findAll(predicateInAnyList(names1, names2)).spliterator(), false) + .collect(Collectors.toList()); + } + + default Predicate predicateInAnyList(List names1, List names2) { + return new BooleanBuilder().and(QUserGroup.userGroup.name.in(names1)) + .or(QUserGroup.userGroup.name.in(names2)); + } +} diff --git a/persistence-modules/querydsl/src/test/java/com/baeldung/querydslvsjpacriteria/QuerydslVSJPACriteriaIntegrationTest.java b/persistence-modules/querydsl/src/test/java/com/baeldung/querydslvsjpacriteria/QuerydslVSJPACriteriaIntegrationTest.java new file mode 100644 index 0000000000..7827430211 --- /dev/null +++ b/persistence-modules/querydsl/src/test/java/com/baeldung/querydslvsjpacriteria/QuerydslVSJPACriteriaIntegrationTest.java @@ -0,0 +1,270 @@ +package com.baeldung.querydslvsjpacriteria; + +import static com.baeldung.querydslvsjpacriteria.entities.GroupUser_.tasks; +import static com.baeldung.querydslvsjpacriteria.entities.QGroupUser.groupUser; +import static com.baeldung.querydslvsjpacriteria.entities.QTask.task; +import static com.baeldung.querydslvsjpacriteria.entities.QUserGroup.userGroup; +import static com.baeldung.querydslvsjpacriteria.entities.UserGroup_.GROUP_USERS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.baeldung.querydslvsjpacriteria.entities.GroupUser; +import com.baeldung.querydslvsjpacriteria.entities.Task; +import com.baeldung.querydslvsjpacriteria.entities.UserGroup; +import com.baeldung.querydslvsjpacriteria.entities.UserGroup_; +import com.baeldung.querydslvsjpacriteria.repositories.UserGroupJpaSpecificationRepository; +import com.baeldung.querydslvsjpacriteria.repositories.UserGroupQuerydslPredicateRepository; +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Root; + +@EnableJpaRepositories(basePackages = {"com.baeldung.querydslvsjpacriteria.repositories"}) +@ContextConfiguration("/test-context.xml") +@ExtendWith({SpringExtension.class, TimingExtension.class}) +class QuerydslVSJPACriteriaIntegrationTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(QuerydslVSJPACriteriaIntegrationTest.class); + + private static EntityManagerFactory emf; + + private EntityManager em; + + private JPAQueryFactory queryFactory; + + @Autowired + private UserGroupJpaSpecificationRepository userGroupJpaSpecificationRepository; + + @Autowired + private UserGroupQuerydslPredicateRepository userQuerydslPredicateRepository; + + @BeforeAll + static void populateDatabase() { + emf = Persistence.createEntityManagerFactory("com.baeldung.querydsl.intro"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + + 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); + }); + + em.getTransaction().commit(); + em.close(); + } + + @BeforeEach + void setUp() { + em = emf.createEntityManager(); + em.getTransaction().begin(); + queryFactory = new JPAQueryFactory(em); + + createUserGroup("Group 1"); + createUserGroup("Group 4"); + } + + @Test + void givenJpaCriteria_whenGetAllTheUserGroups_thenExpectedNumberOfItemsShouldBePresent() { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(UserGroup.class); + Root root = cr.from(UserGroup.class); + CriteriaQuery select = cr.select(root); + + TypedQuery query = em.createQuery(select); + List results = query.getResultList(); + assertEquals(3, results.size()); + } + + @Test + void givenQueryDSL_whenGetAllTheUserGroups_thenExpectedNumberOfItemsShouldBePresent() { + List results = queryFactory.selectFrom(userGroup).fetch(); + assertEquals(3, results.size()); + } + + @Test + void givenJpaCriteria_whenGetTheUserGroups_thenExpectedAggregatedDataShouldBePresent() { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(Object[].class); + Root root = cr.from(UserGroup.class); + + CriteriaQuery 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 query = em.createQuery(select); + List 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]); + } + + @Test + void givenQueryDSL_whenGetTheUserGroups_thenExpectedAggregatedDataShouldBePresent() { + List 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())); + } + + @Test + void givenJpaCriteria_whenGetTheUserGroupsWithJoins_thenExpectedDataShouldBePresent() { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(UserGroup.class); + + query.from(UserGroup.class) + .join(GROUP_USERS, JoinType.LEFT) + .join(tasks, JoinType.LEFT); + + List result = em.createQuery(query).getResultList(); + assertUserGroups(result); + } + + private void assertUserGroups(List userGroups) { + assertEquals(3, userGroups.size()); + for (UserGroup group : userGroups) { + assertEquals(10, group.getGroupUsers().size()); + for (GroupUser user : group.getGroupUsers()) { + assertEquals(10000, user.getTasks().size()); + } + } + } + + @Test + void givenQueryDSL_whenGetTheUserGroupsWithJoins_thenExpectedDataShouldBePresent() { + List result = queryFactory + .selectFrom(userGroup) + .leftJoin(userGroup.groupUsers, groupUser) + .leftJoin(groupUser.tasks, task) + .fetch(); + + assertUserGroups(result); + } + + @Test + void givenJpaCriteria_whenModifyTheUserGroup_thenNameShouldBeUpdated() { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaUpdate criteriaUpdate = cb.createCriteriaUpdate(UserGroup.class); + Root 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()); + renameEntityBack(foundGroup, "Group 1"); + } + + private void renameEntityBack(UserGroup foundGroup, String name) { + foundGroup.setName(name); + em.merge(foundGroup); + } + + @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()); + renameEntityBack(foundGroup, "Group 1"); + } + + @Test + void givenJpaSpecificationRepository_whenGetTheUserGroups_thenExpectedDataShouldBePresent() { + List 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()); + } + + @Test + void givenQuerydslPredicateRepository_whenGetTheUserGroups_thenExpectedDataShouldBePresent() { + List 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()); + } + + private void createUserGroup(String name) { + UserGroup entity = new UserGroup(); + entity.setName(name); + userGroupJpaSpecificationRepository.save(entity); + } + + @AfterEach + void tearDown() { + em.getTransaction().commit(); + em.close(); + userGroupJpaSpecificationRepository.deleteAll(); + } + + @AfterAll + static void afterClass() { + emf.close(); + } +} diff --git a/persistence-modules/querydsl/src/test/java/com/baeldung/querydslvsjpacriteria/TimingExtension.java b/persistence-modules/querydsl/src/test/java/com/baeldung/querydslvsjpacriteria/TimingExtension.java new file mode 100644 index 0000000000..1847dfb5be --- /dev/null +++ b/persistence-modules/querydsl/src/test/java/com/baeldung/querydslvsjpacriteria/TimingExtension.java @@ -0,0 +1,34 @@ +package com.baeldung.querydslvsjpacriteria; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { + private static final Logger logger = LoggerFactory.getLogger(TimingExtension.class); + private static final String START_TIME = "start time"; + + @Override + public void beforeTestExecution(ExtensionContext context) { + getStore(context).put(START_TIME, System.currentTimeMillis()); + } + + @Override + public void afterTestExecution(ExtensionContext context) { + Method testMethod = context.getRequiredTestMethod(); + long startTime = getStore(context).remove(START_TIME, long.class); + long duration = System.currentTimeMillis() - startTime; + + logger.info(String.format("Method [%s] took %s ms.", testMethod.getName(), duration)); + } + + private Store getStore(ExtensionContext context) { + return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod())); + } +}