From e84e9fbe7ce54f37e7b8a6e7a69402fa392b1261 Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Sun, 18 Dec 2022 12:26:14 +0200 Subject: [PATCH] Add DistinctListTransformer to Hibernate Types 6 #517 --- .../query/DistinctListTransformer.java | 32 ++ .../query/DistinctListTransformerTest.java | 362 ++++++++++++++++++ .../hibernate/util/AbstractTest.java | 34 ++ 3 files changed, 428 insertions(+) create mode 100644 hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/DistinctListTransformer.java create mode 100644 hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/DistinctListTransformerTest.java diff --git a/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/DistinctListTransformer.java b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/DistinctListTransformer.java new file mode 100644 index 000000000..69888764d --- /dev/null +++ b/hibernate-types-60/src/main/java/com/vladmihalcea/hibernate/query/DistinctListTransformer.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hibernate.query; + +import org.hibernate.query.ResultListTransformer; +import org.hibernate.transform.ResultTransformer; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * The {@link DistinctListTransformer} removes duplicates from the {@link List} + * of elements that were transformed by the {@link org.hibernate.query.TupleTransformer}. + *

+ * This is similar to the {@code DistinctResultTransformer} that was available in Hibernate 5. + * + * @author Vlad Mihalcea + * @since 2.21.0 + */ +public class DistinctListTransformer implements ResultListTransformer { + + public static final DistinctListTransformer INSTANCE = new DistinctListTransformer(); + + /** + * Deduplicates the provided List. + * + * @param collection collections to be deduplicated + * @return deduplicated List + */ + @Override + public List transformList(List collection) { + return (List) collection.stream().distinct().collect(Collectors.toList()); + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/DistinctListTransformerTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/DistinctListTransformerTest.java new file mode 100644 index 000000000..505b88647 --- /dev/null +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/query/DistinctListTransformerTest.java @@ -0,0 +1,362 @@ +package com.vladmihalcea.hibernate.query; + +import com.vladmihalcea.hibernate.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.query.TupleTransformer; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@SuppressWarnings("unchecked") +public class DistinctListTransformerTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2016, 11, 2, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + .addComment( + new PostComment() + .setId(1L) + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("A must-read for every Java developer!") + ) + ); + + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2019, 3, 19, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + .addComment( + new PostComment() + .setId(3L) + .setReview("It's like pair programming with Vlad!") + ) + ); + }); + } + + @Test + public void testParentChildDTOProjectionNativeQueryTupleTransformer() { + doInJPA( entityManager -> { + List postDTOs = entityManager.createNativeQuery( + "SELECT p.id AS p_id, " + + " p.title AS p_title, " + + " pc.id AS pc_id, " + + " pc.review AS pc_review " + + "FROM post p " + + "JOIN post_comment pc ON p.id = pc.post_id " + + "ORDER BY pc.id") + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer(new PostDTOTupleTransformer()) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getResultList(); + + assertEquals(2, postDTOs.size()); + assertEquals(2, postDTOs.get(0).getComments().size()); + assertEquals(1, postDTOs.get(1).getComments().size()); + + PostDTO post1DTO = postDTOs.get(0); + + assertEquals(1L, post1DTO.getId().longValue()); + assertEquals(2, post1DTO.getComments().size()); + assertEquals(1L, post1DTO.getComments().get(0).getId().longValue()); + assertEquals(2L, post1DTO.getComments().get(1).getId().longValue()); + + PostDTO post2DTO = postDTOs.get(1); + + assertEquals(2L, post2DTO.getId().longValue()); + assertEquals(1, post2DTO.getComments().size()); + assertEquals(3L, post2DTO.getComments().get(0).getId().longValue()); + } ); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + @Version + private Short version; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public Post setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return this; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public Post setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + return this; + } + + public Short getVersion() { + return version; + } + + public Post setVersion(Short version) { + this.version = version; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + public static class PostDTO { + + public static final String ID_ALIAS = "p_id"; + public static final String TITLE_ALIAS = "p_title"; + + private Long id; + + private String title; + + private List comments = new ArrayList<>(); + + public PostDTO() { + } + + public PostDTO(Long id, String title) { + this.id = id; + this.title = title; + } + + public PostDTO(Object[] tuples, Map aliasToIndexMap) { + this.id = AbstractTest.longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]); + this.title = AbstractTest.stringValue(tuples[aliasToIndexMap.get(TITLE_ALIAS)]); + } + + public Long getId() { + return id; + } + + public void setId(Number id) { + this.id = id.longValue(); + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + } + + public static class PostCommentDTO { + public static final String ID_ALIAS = "pc_id"; + public static final String REVIEW_ALIAS = "pc_review"; + + private Long id; + + private String review; + + public PostCommentDTO(Long id, String review) { + this.id = id; + this.review = review; + } + + public PostCommentDTO(Object[] tuples, Map aliasToIndexMap) { + this.id = AbstractTest.longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]); + this.review = AbstractTest.stringValue(tuples[aliasToIndexMap.get(REVIEW_ALIAS)]); + } + + public Long getId() { + return id; + } + + public void setId(Number id) { + this.id = id.longValue(); + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + public static class PostDTOTupleTransformer implements TupleTransformer { + + private Map postDTOMap = new LinkedHashMap<>(); + + @Override + public PostDTO transformTuple(Object[] tuple, String[] aliases) { + Map aliasToIndexMap = aliasToIndexMap(aliases); + Long postId = AbstractTest.longValue(tuple[aliasToIndexMap.get(PostDTO.ID_ALIAS)]); + + PostDTO postDTO = postDTOMap.computeIfAbsent( + postId, + id -> new PostDTO(tuple, aliasToIndexMap) + ); + postDTO.getComments().add(new PostCommentDTO(tuple, aliasToIndexMap)); + + return postDTO; + } + + private Map aliasToIndexMap(String[] aliases) { + Map aliasToIndexMap = new LinkedHashMap<>(); + for (int i = 0; i < aliases.length; i++) { + aliasToIndexMap.put(aliases[i].toLowerCase(Locale.ROOT), i); + } + return aliasToIndexMap; + } + } +} diff --git a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractTest.java b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractTest.java index 08cc6c942..6cf9a2c52 100644 --- a/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractTest.java +++ b/hibernate-types-60/src/test/java/com/vladmihalcea/hibernate/util/AbstractTest.java @@ -39,6 +39,9 @@ import javax.sql.DataSource; import java.io.Closeable; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -532,4 +535,35 @@ protected void doInJDBC(ConnectionVoidCallable callable) { } } } + + public static String stringValue(Object value) { + return value.toString(); + } + + public static int intValue(Object number) { + return ((Number) number).intValue(); + } + + public static long longValue(Object number) { + if(number instanceof String) { + return Long.parseLong((String) number); + } + return ((Number) number).longValue(); + } + + public static double doubleValue(Object number) { + return ((Number) number).doubleValue(); + } + + public static URL urlValue(String url) { + try { + return url != null ? new URL(url) : null; + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + public static LocalDateTime localDateTimeValue(Object value) { + return (LocalDateTime) value; + } }