blog: Refactoring, DTO projection, native queries.
Change-Id: Ie073cbe0f4e3da88af7cfb062f6c18d8f5c39df2
diff --git a/blog/SLIDES.adoc b/blog/SLIDES.adoc
new file mode 100644
index 0000000..fe5bd01
--- /dev/null
+++ b/blog/SLIDES.adoc
@@ -0,0 +1,122 @@
+= Highly Efficient Enterprise Data Access
+Matthias Andreas Benkard
+// Meta
+:experimental:
+:data-uri:
+:sectnums:
+:toc:
+:stem:
+:toclevels: 2
+:description: Slides for my Hibernate Patterns and Antipatterns talk
+:keywords: mulk
+// Settings
+:icons: font
+:source-highlighter: rouge
+
+
+[source,sql]
+----
+Hibernate:
+ select
+ post0_.id as id1_4_0_,
+ comments1_.id as id1_3_1_,
+ category3_.id as id1_2_2_,
+ post0_.author_id as author_i5_4_0_,
+ post0_.body as body2_4_0_,
+ post0_.publication_date as publicat3_4_0_,
+ post0_.title as title4_4_0_,
+ comments1_.author_name as author_n2_3_1_,
+ comments1_.post_id as post_id6_3_1_,
+ comments1_.publication_date as publicat3_3_1_,
+ comments1_.spam_status as spam_sta4_3_1_,
+ comments1_.text as text5_3_1_,
+ comments1_.post_id as post_id6_3_0__,
+ comments1_.id as id1_3_0__,
+ category3_.name as name2_2_2_,
+ categories2_.post_id as post_id1_5_1__,
+ categories2_.categories_id as categori2_5_1__
+ from
+ post post0_
+ left outer join
+ comment comments1_
+ on post0_.id=comments1_.post_id
+ left outer join
+ post_category categories2_
+ on post0_.id=categories2_.post_id
+ left outer join
+ category category3_
+ on categories2_.categories_id=category3_.id;
+----
+
+[source]
+----
+[2021-02-27 10:32:58] 60 rows retrieved starting from 1 in 263 ms (execution: 12 ms, fetching: 251 ms)
+
+; 60 = #posts (10) x #comments/post (3) x #categories (2)
+; cartesian explosion!
+----
+
+[source,sql]
+----
+post_id com_id cat_id aut_id body pubdate title author_name postid2 com_pubdate spamp com_text postid3 com_id cat_name postid4 cat_id
+4 16 44 1 "" 2021-02-27 10:32:16.129627 Post #0 Anonymous Coward 4 2021-02-27 10:32:16.133969 UNKNOWN First post 4 16 Category #1 4 44
+4 15 44 1 "" 2021-02-27 10:32:16.129627 Post #0 Anonymous Coward 4 2021-02-27 10:32:16.133963 UNKNOWN First post 4 15 Category #1 4 44
+4 14 44 1 "" 2021-02-27 10:32:16.129627 Post #0 Anonymous Coward 4 2021-02-27 10:32:16.133870 UNKNOWN First post 4 14 Category #1 4 44
+4 16 45 1 "" 2021-02-27 10:32:16.129627 Post #0 Anonymous Coward 4 2021-02-27 10:32:16.133969 UNKNOWN First post 4 16 Category #0 4 45
+4 15 45 1 "" 2021-02-27 10:32:16.129627 Post #0 Anonymous Coward 4 2021-02-27 10:32:16.133963 UNKNOWN First post 4 15 Category #0 4 45
+4 14 45 1 "" 2021-02-27 10:32:16.129627 Post #0 Anonymous Coward 4 2021-02-27 10:32:16.133870 UNKNOWN First post 4 14 Category #0 4 45
+5 19 44 2 "" 2021-02-27 10:32:16.129674 Post #1 Anonymous Coward 5 2021-02-27 10:32:16.135200 UNKNOWN First post 5 19 Category #1 5 44
+5 18 44 2 "" 2021-02-27 10:32:16.129674 Post #1 Anonymous Coward 5 2021-02-27 10:32:16.135192 UNKNOWN First post 5 18 Category #1 5 44
+5 17 44 2 "" 2021-02-27 10:32:16.129674 Post #1 Anonymous Coward 5 2021-02-27 10:32:16.135205 UNKNOWN First post 5 17 Category #1 5 44
+5 19 45 2 "" 2021-02-27 10:32:16.129674 Post #1 Anonymous Coward 5 2021-02-27 10:32:16.135200 UNKNOWN First post 5 19 Category #0 5 45
+5 18 45 2 "" 2021-02-27 10:32:16.129674 Post #1 Anonymous Coward 5 2021-02-27 10:32:16.135192 UNKNOWN First post 5 18 Category #0 5 45
+5 17 45 2 "" 2021-02-27 10:32:16.129674 Post #1 Anonymous Coward 5 2021-02-27 10:32:16.135205 UNKNOWN First post 5 17 Category #0 5 45
+6 22 44 3 "" 2021-02-27 10:32:16.129700 Post #2 Anonymous Coward 6 2021-02-27 10:32:16.136043 UNKNOWN First post 6 22 Category #1 6 44
+6 21 44 3 "" 2021-02-27 10:32:16.129700 Post #2 Anonymous Coward 6 2021-02-27 10:32:16.136038 UNKNOWN First post 6 21 Category #1 6 44
+6 20 44 3 "" 2021-02-27 10:32:16.129700 Post #2 Anonymous Coward 6 2021-02-27 10:32:16.136031 UNKNOWN First post 6 20 Category #1 6 44
+6 22 45 3 "" 2021-02-27 10:32:16.129700 Post #2 Anonymous Coward 6 2021-02-27 10:32:16.136043 UNKNOWN First post 6 22 Category #0 6 45
+6 21 45 3 "" 2021-02-27 10:32:16.129700 Post #2 Anonymous Coward 6 2021-02-27 10:32:16.136038 UNKNOWN First post 6 21 Category #0 6 45
+6 20 45 3 "" 2021-02-27 10:32:16.129700 Post #2 Anonymous Coward 6 2021-02-27 10:32:16.136031 UNKNOWN First post 6 20 Category #0 6 45
+7 25 44 1 "" 2021-02-27 10:32:16.129724 Post #3 Anonymous Coward 7 2021-02-27 10:32:16.136904 UNKNOWN First post 7 25 Category #1 7 44
+7 24 44 1 "" 2021-02-27 10:32:16.129724 Post #3 Anonymous Coward 7 2021-02-27 10:32:16.136897 UNKNOWN First post 7 24 Category #1 7 44
+7 23 44 1 "" 2021-02-27 10:32:16.129724 Post #3 Anonymous Coward 7 2021-02-27 10:32:16.136909 UNKNOWN First post 7 23 Category #1 7 44
+7 25 45 1 "" 2021-02-27 10:32:16.129724 Post #3 Anonymous Coward 7 2021-02-27 10:32:16.136904 UNKNOWN First post 7 25 Category #0 7 45
+7 24 45 1 "" 2021-02-27 10:32:16.129724 Post #3 Anonymous Coward 7 2021-02-27 10:32:16.136897 UNKNOWN First post 7 24 Category #0 7 45
+7 23 45 1 "" 2021-02-27 10:32:16.129724 Post #3 Anonymous Coward 7 2021-02-27 10:32:16.136909 UNKNOWN First post 7 23 Category #0 7 45
+8 28 44 2 "" 2021-02-27 10:32:16.129746 Post #4 Anonymous Coward 8 2021-02-27 10:32:16.137743 UNKNOWN First post 8 28 Category #1 8 44
+8 27 44 2 "" 2021-02-27 10:32:16.129746 Post #4 Anonymous Coward 8 2021-02-27 10:32:16.137739 UNKNOWN First post 8 27 Category #1 8 44
+8 26 44 2 "" 2021-02-27 10:32:16.129746 Post #4 Anonymous Coward 8 2021-02-27 10:32:16.137731 UNKNOWN First post 8 26 Category #1 8 44
+8 28 45 2 "" 2021-02-27 10:32:16.129746 Post #4 Anonymous Coward 8 2021-02-27 10:32:16.137743 UNKNOWN First post 8 28 Category #0 8 45
+8 27 45 2 "" 2021-02-27 10:32:16.129746 Post #4 Anonymous Coward 8 2021-02-27 10:32:16.137739 UNKNOWN First post 8 27 Category #0 8 45
+8 26 45 2 "" 2021-02-27 10:32:16.129746 Post #4 Anonymous Coward 8 2021-02-27 10:32:16.137731 UNKNOWN First post 8 26 Category #0 8 45
+9 31 44 3 "" 2021-02-27 10:32:16.129767 Post #5 Anonymous Coward 9 2021-02-27 10:32:16.138536 UNKNOWN First post 9 31 Category #1 9 44
+9 30 44 3 "" 2021-02-27 10:32:16.129767 Post #5 Anonymous Coward 9 2021-02-27 10:32:16.138548 UNKNOWN First post 9 30 Category #1 9 44
+9 29 44 3 "" 2021-02-27 10:32:16.129767 Post #5 Anonymous Coward 9 2021-02-27 10:32:16.138543 UNKNOWN First post 9 29 Category #1 9 44
+9 31 45 3 "" 2021-02-27 10:32:16.129767 Post #5 Anonymous Coward 9 2021-02-27 10:32:16.138536 UNKNOWN First post 9 31 Category #0 9 45
+9 30 45 3 "" 2021-02-27 10:32:16.129767 Post #5 Anonymous Coward 9 2021-02-27 10:32:16.138548 UNKNOWN First post 9 30 Category #0 9 45
+9 29 45 3 "" 2021-02-27 10:32:16.129767 Post #5 Anonymous Coward 9 2021-02-27 10:32:16.138543 UNKNOWN First post 9 29 Category #0 9 45
+10 34 44 1 "" 2021-02-27 10:32:16.129789 Post #6 Anonymous Coward 10 2021-02-27 10:32:16.139349 UNKNOWN First post 10 34 Category #1 10 44
+10 33 44 1 "" 2021-02-27 10:32:16.129789 Post #6 Anonymous Coward 10 2021-02-27 10:32:16.139354 UNKNOWN First post 10 33 Category #1 10 44
+10 32 44 1 "" 2021-02-27 10:32:16.129789 Post #6 Anonymous Coward 10 2021-02-27 10:32:16.139337 UNKNOWN First post 10 32 Category #1 10 44
+10 34 45 1 "" 2021-02-27 10:32:16.129789 Post #6 Anonymous Coward 10 2021-02-27 10:32:16.139349 UNKNOWN First post 10 34 Category #0 10 45
+10 33 45 1 "" 2021-02-27 10:32:16.129789 Post #6 Anonymous Coward 10 2021-02-27 10:32:16.139354 UNKNOWN First post 10 33 Category #0 10 45
+10 32 45 1 "" 2021-02-27 10:32:16.129789 Post #6 Anonymous Coward 10 2021-02-27 10:32:16.139337 UNKNOWN First post 10 32 Category #0 10 45
+11 37 44 2 "" 2021-02-27 10:32:16.129809 Post #7 Anonymous Coward 11 2021-02-27 10:32:16.140032 UNKNOWN First post 11 37 Category #1 11 44
+11 36 44 2 "" 2021-02-27 10:32:16.129809 Post #7 Anonymous Coward 11 2021-02-27 10:32:16.140025 UNKNOWN First post 11 36 Category #1 11 44
+11 35 44 2 "" 2021-02-27 10:32:16.129809 Post #7 Anonymous Coward 11 2021-02-27 10:32:16.140037 UNKNOWN First post 11 35 Category #1 11 44
+11 37 45 2 "" 2021-02-27 10:32:16.129809 Post #7 Anonymous Coward 11 2021-02-27 10:32:16.140032 UNKNOWN First post 11 37 Category #0 11 45
+11 36 45 2 "" 2021-02-27 10:32:16.129809 Post #7 Anonymous Coward 11 2021-02-27 10:32:16.140025 UNKNOWN First post 11 36 Category #0 11 45
+11 35 45 2 "" 2021-02-27 10:32:16.129809 Post #7 Anonymous Coward 11 2021-02-27 10:32:16.140037 UNKNOWN First post 11 35 Category #0 11 45
+12 40 44 3 "" 2021-02-27 10:32:16.129839 Post #8 Anonymous Coward 12 2021-02-27 10:32:16.140766 UNKNOWN First post 12 40 Category #1 12 44
+12 39 44 3 "" 2021-02-27 10:32:16.129839 Post #8 Anonymous Coward 12 2021-02-27 10:32:16.140786 UNKNOWN First post 12 39 Category #1 12 44
+12 38 44 3 "" 2021-02-27 10:32:16.129839 Post #8 Anonymous Coward 12 2021-02-27 10:32:16.140779 UNKNOWN First post 12 38 Category #1 12 44
+12 40 45 3 "" 2021-02-27 10:32:16.129839 Post #8 Anonymous Coward 12 2021-02-27 10:32:16.140766 UNKNOWN First post 12 40 Category #0 12 45
+12 39 45 3 "" 2021-02-27 10:32:16.129839 Post #8 Anonymous Coward 12 2021-02-27 10:32:16.140786 UNKNOWN First post 12 39 Category #0 12 45
+12 38 45 3 "" 2021-02-27 10:32:16.129839 Post #8 Anonymous Coward 12 2021-02-27 10:32:16.140779 UNKNOWN First post 12 38 Category #0 12 45
+13 43 44 1 "" 2021-02-27 10:32:16.129860 Post #9 Anonymous Coward 13 2021-02-27 10:32:16.141651 UNKNOWN First post 13 43 Category #1 13 44
+13 42 44 1 "" 2021-02-27 10:32:16.129860 Post #9 Anonymous Coward 13 2021-02-27 10:32:16.141655 UNKNOWN First post 13 42 Category #1 13 44
+13 41 44 1 "" 2021-02-27 10:32:16.129860 Post #9 Anonymous Coward 13 2021-02-27 10:32:16.141642 UNKNOWN First post 13 41 Category #1 13 44
+13 43 45 1 "" 2021-02-27 10:32:16.129860 Post #9 Anonymous Coward 13 2021-02-27 10:32:16.141651 UNKNOWN First post 13 43 Category #0 13 45
+13 42 45 1 "" 2021-02-27 10:32:16.129860 Post #9 Anonymous Coward 13 2021-02-27 10:32:16.141655 UNKNOWN First post 13 42 Category #0 13 45
+13 41 45 1 "" 2021-02-27 10:32:16.129860 Post #9 Anonymous Coward 13 2021-02-27 10:32:16.141642 UNKNOWN First post 13 41 Category #0 13 45
+----
diff --git a/blog/src/main/java/eu/mulk/demos/blog/DemoDataLoader.java b/blog/src/main/java/eu/mulk/demos/blog/DemoDataLoader.java
index cbaed9a..bbd5cb0 100644
--- a/blog/src/main/java/eu/mulk/demos/blog/DemoDataLoader.java
+++ b/blog/src/main/java/eu/mulk/demos/blog/DemoDataLoader.java
@@ -1,7 +1,12 @@
package eu.mulk.demos.blog;
import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import eu.mulk.demos.blog.authors.Author;
+import eu.mulk.demos.blog.comments.Comment;
+import eu.mulk.demos.blog.posts.Category;
+import eu.mulk.demos.blog.posts.Post;
import io.quarkus.runtime.StartupEvent;
import java.util.stream.Stream;
import javax.enterprise.context.ApplicationScoped;
@@ -47,7 +52,7 @@
post.comments =
nat(COMMENT_COUNT)
.map(x -> Comment.create(post, "Anonymous Coward", "First post"))
- .collect(toList());
+ .collect(toSet());
post.comments.forEach(em::persist);
}
@@ -55,7 +60,7 @@
var categories =
nat(CATEGORY_COUNT)
.map(x -> Category.create("Category #%d".formatted(x)))
- .collect(toList());
+ .collect(toSet());
categories.forEach(em::persist);
for (var post : posts) {
post.categories = categories;
diff --git a/blog/src/main/java/eu/mulk/demos/blog/PostResource.java b/blog/src/main/java/eu/mulk/demos/blog/PostResource.java
deleted file mode 100644
index 532e873..0000000
--- a/blog/src/main/java/eu/mulk/demos/blog/PostResource.java
+++ /dev/null
@@ -1,138 +0,0 @@
-package eu.mulk.demos.blog;
-
-import java.util.List;
-import java.util.Set;
-import javax.transaction.Transactional;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import org.hibernate.annotations.LazyToOne;
-import org.hibernate.annotations.LazyToOneOption;
-import org.jboss.logging.Logger;
-
-@Path("/posts")
-public class PostResource {
-
- static final Logger log = Logger.getLogger(PostResource.class);
-
- /**
- * Fetches all posts with no extra information.
- *
- * Simple. No surprises.
- */
- @GET
- @Produces(MediaType.TEXT_PLAIN)
- @Transactional
- public List<Post> getAll() {
- clearLog();
-
- return Post.findAll().list();
- }
-
- /**
- * Fetches all posts with comments included.
- *
- * Lazy fetching. Simple. No surprises.
- */
- @GET
- @Produces(MediaType.TEXT_PLAIN)
- @Transactional
- @Path("/q1")
- public List<Post> getAllWithComments() {
- clearLog();
-
- return Post.find(
- """
- SELECT p FROM Post p
- LEFT JOIN FETCH p.comments
- """)
- .list();
- }
-
- /**
- * Fetches all posts with author info included.
- *
- * <strong>Oops!</strong>
- *
- * {@link LazyToOne} with {@link LazyToOneOption#NO_PROXY} is needed to make this efficient.
- */
- @GET
- @Produces(MediaType.TEXT_PLAIN)
- @Transactional
- @Path("/q2")
- public List<Post> getAllWithAuthors() {
- clearLog();
-
- return Post.find(
- """
- SELECT p FROM Post p
- LEFT JOIN FETCH p.author
- """)
- .list();
- }
-
- /**
- * Fetches all posts with comments and category info included.
- *
- * <strong>Oops!</strong> Crashes.
- *
- * Either use {@link Set} and get bad performance or do it as in {@link
- * #getAllWithCommentsAndCategories2()}.
- */
- @GET
- @Produces(MediaType.TEXT_PLAIN)
- @Transactional
- @Path("/q3")
- public List<Post> getAllWithCommentsAndCategories() {
- clearLog();
-
- return Post.find(
- """
- SELECT p FROM Post p
- LEFT JOIN FETCH p.comments
- LEFT JOIN FETCH p.categories
- """)
- .list();
- }
-
- /**
- * Fetches all posts with comments and category info included.
- *
- * 2 queries, but hey, no cartesian explosion! Works really well.
- */
- @GET
- @Produces(MediaType.TEXT_PLAIN)
- @Transactional
- @Path("/q4")
- public List<Post> getAllWithCommentsAndCategories2() {
- clearLog();
-
- List<Post> posts = Post.find(
- """
- SELECT p FROM Post p
- LEFT JOIN FETCH p.comments
- """)
- .list();
-
- posts = Post.find(
- """
- SELECT DISTINCT p FROM Post p
- LEFT JOIN FETCH p.categories
- WHERE p IN (?1)
- """,
- posts)
- .list();
-
- return posts;
- }
-
- private static void clearLog() {
- log.infof("""
-
- -----------------------------------------------------
- -------------------- NEW REQUEST --------------------
- -----------------------------------------------------
- """);
- }
-}
diff --git a/blog/src/main/java/eu/mulk/demos/blog/Author.java b/blog/src/main/java/eu/mulk/demos/blog/authors/Author.java
similarity index 84%
rename from blog/src/main/java/eu/mulk/demos/blog/Author.java
rename to blog/src/main/java/eu/mulk/demos/blog/authors/Author.java
index d544d2e..e3c936f 100644
--- a/blog/src/main/java/eu/mulk/demos/blog/Author.java
+++ b/blog/src/main/java/eu/mulk/demos/blog/authors/Author.java
@@ -1,6 +1,7 @@
-package eu.mulk.demos.blog;
+package eu.mulk.demos.blog.authors;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
+import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToOne;
@@ -14,6 +15,7 @@
@OneToOne(fetch = FetchType.LAZY, mappedBy = "author")
@LazyToOne(LazyToOneOption.NO_PROXY)
+ @JsonbTransient
public BasicCredentials basicCredentials;
public static Author create(String name) {
diff --git a/blog/src/main/java/eu/mulk/demos/blog/BasicCredentials.java b/blog/src/main/java/eu/mulk/demos/blog/authors/BasicCredentials.java
similarity index 85%
rename from blog/src/main/java/eu/mulk/demos/blog/BasicCredentials.java
rename to blog/src/main/java/eu/mulk/demos/blog/authors/BasicCredentials.java
index 01471c6..92e7a37 100644
--- a/blog/src/main/java/eu/mulk/demos/blog/BasicCredentials.java
+++ b/blog/src/main/java/eu/mulk/demos/blog/authors/BasicCredentials.java
@@ -1,6 +1,7 @@
-package eu.mulk.demos.blog;
+package eu.mulk.demos.blog.authors;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
+import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.MapsId;
@@ -11,6 +12,7 @@
@OneToOne(fetch = FetchType.LAZY)
@MapsId
+ @JsonbTransient
public Author author;
public String username;
diff --git a/blog/src/main/java/eu/mulk/demos/blog/Comment.java b/blog/src/main/java/eu/mulk/demos/blog/comments/Comment.java
similarity index 67%
rename from blog/src/main/java/eu/mulk/demos/blog/Comment.java
rename to blog/src/main/java/eu/mulk/demos/blog/comments/Comment.java
index 251d6d8..415ac7b 100644
--- a/blog/src/main/java/eu/mulk/demos/blog/Comment.java
+++ b/blog/src/main/java/eu/mulk/demos/blog/comments/Comment.java
@@ -1,10 +1,14 @@
-package eu.mulk.demos.blog;
+package eu.mulk.demos.blog.comments;
+import eu.mulk.demos.blog.posts.Post;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import java.time.Instant;
import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.Column;
import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.FetchType;
import javax.persistence.ManyToOne;
@Entity
@@ -14,11 +18,14 @@
public Instant publicationDate;
+ @Enumerated(EnumType.STRING)
+ public SpamStatus spamStatus;
+
@Column(columnDefinition = "TEXT")
public String text;
+ @ManyToOne(fetch = FetchType.LAZY)
@JsonbTransient
- @ManyToOne
public Post post;
public static Comment create(Post post, String authorName, String text) {
@@ -27,6 +34,8 @@
c.publicationDate = Instant.now();
c.text = text;
c.post = post;
+ c.spamStatus = SpamStatus.UNKNOWN;
return c;
}
+
}
diff --git a/blog/src/main/java/eu/mulk/demos/blog/comments/SpamAssessmentService.java b/blog/src/main/java/eu/mulk/demos/blog/comments/SpamAssessmentService.java
new file mode 100644
index 0000000..efa9ac2
--- /dev/null
+++ b/blog/src/main/java/eu/mulk/demos/blog/comments/SpamAssessmentService.java
@@ -0,0 +1,33 @@
+package eu.mulk.demos.blog.comments;
+
+import static java.util.stream.Collectors.toMap;
+
+import java.util.Collection;
+import java.util.Map;
+import javax.enterprise.context.ApplicationScoped;
+
+/**
+ * Simulates a remote service that classifies {@link Comment}s as either {@link SpamStatus#SPAM} or
+ * {@link SpamStatus#HAM}.
+ */
+@ApplicationScoped
+public class SpamAssessmentService {
+
+ /**
+ * Classifies a list of {@link Comment}s as either {@link SpamStatus#SPAM} or * {@link
+ * SpamStatus#HAM}.
+ *
+ * @return a map mapping {@link Comment#id}s to {@link SpamStatus}es.
+ */
+ public Map<Long, SpamStatus> assess(Collection<Comment> comments) {
+ return comments.stream().collect(toMap(x -> x.id, this::assessOne));
+ }
+
+ private SpamStatus assessOne(Comment c) {
+ if (c.authorName.startsWith("Anonymous")) {
+ return SpamStatus.SPAM;
+ }
+
+ return SpamStatus.HAM;
+ }
+}
diff --git a/blog/src/main/java/eu/mulk/demos/blog/comments/SpamStatus.java b/blog/src/main/java/eu/mulk/demos/blog/comments/SpamStatus.java
new file mode 100644
index 0000000..a5cd7ee
--- /dev/null
+++ b/blog/src/main/java/eu/mulk/demos/blog/comments/SpamStatus.java
@@ -0,0 +1,7 @@
+package eu.mulk.demos.blog.comments;
+
+public enum SpamStatus {
+ UNKNOWN,
+ HAM,
+ SPAM,
+}
diff --git a/blog/src/main/java/eu/mulk/demos/blog/Category.java b/blog/src/main/java/eu/mulk/demos/blog/posts/Category.java
similarity index 89%
rename from blog/src/main/java/eu/mulk/demos/blog/Category.java
rename to blog/src/main/java/eu/mulk/demos/blog/posts/Category.java
index 0959d18..5870010 100644
--- a/blog/src/main/java/eu/mulk/demos/blog/Category.java
+++ b/blog/src/main/java/eu/mulk/demos/blog/posts/Category.java
@@ -1,4 +1,4 @@
-package eu.mulk.demos.blog;
+package eu.mulk.demos.blog.posts;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import javax.persistence.Entity;
diff --git a/blog/src/main/java/eu/mulk/demos/blog/Post.java b/blog/src/main/java/eu/mulk/demos/blog/posts/Post.java
similarity index 72%
rename from blog/src/main/java/eu/mulk/demos/blog/Post.java
rename to blog/src/main/java/eu/mulk/demos/blog/posts/Post.java
index 3ae3fa0..6fb2c11 100644
--- a/blog/src/main/java/eu/mulk/demos/blog/Post.java
+++ b/blog/src/main/java/eu/mulk/demos/blog/posts/Post.java
@@ -1,8 +1,11 @@
-package eu.mulk.demos.blog;
+package eu.mulk.demos.blog.posts;
+import eu.mulk.demos.blog.authors.Author;
+import eu.mulk.demos.blog.comments.Comment;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import java.time.Instant;
-import java.util.List;
+import java.util.Set;
+import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -21,13 +24,16 @@
public String body;
@ManyToOne(fetch = FetchType.LAZY)
+ @JsonbTransient
public Author author;
@ManyToMany(fetch = FetchType.LAZY)
- public List<Category> categories;
+ @JsonbTransient
+ public Set<Category> categories;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "post")
- public List<Comment> comments;
+ @JsonbTransient
+ public Set<Comment> comments;
public static Post create(Author author, String title) {
var p = new Post();
diff --git a/blog/src/main/java/eu/mulk/demos/blog/posts/PostResource.java b/blog/src/main/java/eu/mulk/demos/blog/posts/PostResource.java
new file mode 100644
index 0000000..ae8b4de
--- /dev/null
+++ b/blog/src/main/java/eu/mulk/demos/blog/posts/PostResource.java
@@ -0,0 +1,248 @@
+package eu.mulk.demos.blog.posts;
+
+import eu.mulk.demos.blog.authors.Author;
+import eu.mulk.demos.blog.comments.Comment;
+import eu.mulk.demos.blog.comments.SpamAssessmentService;
+import eu.mulk.demos.blog.comments.SpamStatus;
+import java.util.List;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.transaction.Transactional;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.hibernate.annotations.LazyToOne;
+import org.hibernate.annotations.LazyToOneOption;
+import org.jboss.logging.Logger;
+
+@Path("/posts")
+@Produces(MediaType.APPLICATION_JSON)
+public class PostResource {
+
+ static final Logger log = Logger.getLogger(PostResource.class);
+
+ @Inject
+ SpamAssessmentService spamAssessmentService;
+
+ @PersistenceContext
+ EntityManager entityManager;
+
+ /**
+ * Fetches all posts with no extra information.
+ *
+ * Simple. No surprises.
+ */
+ @GET
+ @Transactional
+ public List<Post> getAll() {
+ clearLog();
+
+ return Post.findAll().list();
+ }
+
+ /**
+ * Fetches all posts with comments included.
+ *
+ * Lazy fetching. Simple. No surprises.
+ */
+ @GET
+ @Transactional
+ @Path("/q1")
+ public List<Post> getAllWithComments() {
+ clearLog();
+
+ return Post.find(
+ """
+ SELECT p FROM Post p
+ LEFT JOIN FETCH p.comments
+ """)
+ .list();
+ }
+
+ /**
+ * Fetches all posts with author info included.
+ *
+ * <strong>Oops!</strong>
+ *
+ * {@link LazyToOne} with {@link LazyToOneOption#NO_PROXY} on {@link Author#basicCredentials} is
+ * needed to make this efficient.
+ */
+ @GET
+ @Transactional
+ @Path("/q2")
+ public List<Post> getAllWithAuthors() {
+ clearLog();
+
+ return Post.find(
+ """
+ SELECT p FROM Post p
+ LEFT JOIN FETCH p.author
+ """)
+ .list();
+ }
+
+ /**
+ * Fetches all posts with comments and category info included.
+ *
+ * <strong>Oops!</strong> Crashes.
+ *
+ * Either use {@link Set} and get bad performance or do it as in {@link
+ * #getAllWithCommentsAndCategories2()}.
+ */
+ @GET
+ @Transactional
+ @Path("/q3")
+ public List<Post> getAllWithCommentsAndCategories() {
+ clearLog();
+
+ return Post.find(
+ """
+ SELECT p FROM Post p
+ LEFT JOIN FETCH p.comments
+ LEFT JOIN FETCH p.categories
+ """)
+ .list();
+ }
+
+ /**
+ * Fetches all posts with comments and category info included.
+ *
+ * 2 queries, but hey, no cartesian explosion! Works really well.
+ */
+ @GET
+ @Transactional
+ @Path("/q4")
+ public List<Post> getAllWithCommentsAndCategories2() {
+ clearLog();
+
+ List<Post> posts = Post.find(
+ """
+ SELECT p FROM Post p
+ LEFT JOIN FETCH p.comments
+ """)
+ .list();
+
+ posts = Post.find(
+ """
+ SELECT DISTINCT p FROM Post p
+ LEFT JOIN FETCH p.categories
+ WHERE p IN (?1)
+ """,
+ posts)
+ .list();
+
+ return posts;
+ }
+
+ /**
+ * Fetches all posts with comments and category info included.
+ *
+ * 2 queries, but hey, no cartesian explosion! Works really well.
+ */
+ @POST
+ @Transactional
+ @Path("/q5")
+ public void updateCommentStatus() {
+ clearLog();
+
+ List<Comment> comments = Comment.find(
+ """
+ SELECT c FROM Comment c
+ WHERE c.spamStatus = 'UNKNOWN'
+ """)
+ .list();
+
+ var assessments = spamAssessmentService.assess(comments);
+
+ for (var assessment : assessments.entrySet()) {
+ Comment comment = Comment.findById(assessment.getKey());
+ comment.spamStatus = assessment.getValue();
+ }
+ }
+
+ /**
+ * Resets the {@link Comment#spamStatus} to {@link SpamStatus#UNKNOWN} on all comments.
+ *
+ * This issues a lot of UPDATE statements, but semantically speaking it works.
+ */
+ @POST
+ @Transactional
+ @Path("/q6")
+ public void resetCommentStatus() {
+ clearLog();
+
+ List<Comment> comments = Comment.find(
+ """
+ SELECT c FROM Comment c
+ WHERE c.spamStatus <> 'UNKNOWN'
+ """)
+ .list();
+ comments.forEach(c -> c.spamStatus = SpamStatus.UNKNOWN);
+ }
+
+ /**
+ * Resets the {@link Comment#spamStatus} to {@link SpamStatus#UNKNOWN} on all comments.
+ *
+ * This is exactly equivalent to {@link #resetCommentStatus()} and just as efficient or
+ * inefficient.
+ */
+ @POST
+ @Transactional
+ @Path("/q6")
+ public void resetCommentStatus2() {
+ clearLog();
+
+ Comment.update("UPDATE Comment c SET c.spamStatus = 'UNKNOWN' WHERE c.spamStatus <> 'UNKNOWN'");
+ }
+
+ /**
+ * Resets the {@link Comment#spamStatus} to {@link SpamStatus#UNKNOWN} on all comments.
+ *
+ * This is exactly equivalent to {@link #resetCommentStatus()} and just as efficient or
+ * inefficient.
+ */
+ @POST
+ @Transactional
+ @Path("/q7")
+ public void resetCommentStatus3() {
+ clearLog();
+
+ entityManager.createNativeQuery(
+ "UPDATE comment SET spam_status = 'UNKNOWN' WHERE spam_status <> 'UNKNOWN'")
+ .executeUpdate();
+ }
+
+ /**
+ * Fetches all posts with all the relevant info for an overview included.
+ */
+ @GET
+ @Transactional
+ @Path("/q8")
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<PostSummary> overview() {
+ clearLog();
+
+ return entityManager.createQuery(
+ """
+ SELECT NEW eu.mulk.demos.blog.posts.PostSummary(
+ p.author.name, p.title, p.publicationDate, size(p.comments))
+ FROM Post p
+ """,
+ PostSummary.class)
+ .getResultList();
+ }
+
+ private static void clearLog() {
+ log.infof("""
+
+ -----------------------------------------------------
+ -------------------- NEW REQUEST --------------------
+ -----------------------------------------------------
+ """);
+ }
+
+}
diff --git a/blog/src/main/java/eu/mulk/demos/blog/posts/PostSummary.java b/blog/src/main/java/eu/mulk/demos/blog/posts/PostSummary.java
new file mode 100644
index 0000000..f6ad3ec
--- /dev/null
+++ b/blog/src/main/java/eu/mulk/demos/blog/posts/PostSummary.java
@@ -0,0 +1,22 @@
+package eu.mulk.demos.blog.posts;
+
+import java.time.Instant;
+
+public final class PostSummary {
+
+ public final String authorName;
+ public final String title;
+ public final Instant publicationDate;
+ public final int commentCount;
+
+ public PostSummary(
+ String authorName,
+ String title,
+ Instant publicationDate,
+ int commentCount) {
+ this.authorName = authorName;
+ this.title = title;
+ this.publicationDate = publicationDate;
+ this.commentCount = commentCount;
+ }
+}
diff --git a/blog/src/main/resources/application.properties b/blog/src/main/resources/application.properties
index fbf150e..e8b4aaf 100644
--- a/blog/src/main/resources/application.properties
+++ b/blog/src/main/resources/application.properties
@@ -5,9 +5,15 @@
%dev.quarkus.datasource.username = demo
%dev.quarkus.datasource.password =
+quarkus.hibernate-orm.physical-naming-strategy = com.vladmihalcea.hibernate.type.util.CamelCaseToSnakeCaseNamingStrategy
+quarkus.hibernate-orm.dialect = io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect
+
%dev.quarkus.hibernate-orm.log.sql = true
%dev.quarkus.hibernate-orm.log.format-sql = true
%dev.quarkus.hibernate-orm.database.generation = drop-and-create
+%dev.quarkus.hibernate-orm.database.generation.create-schemas = true
-quarkus.hibernate-orm.physical-naming-strategy = com.vladmihalcea.hibernate.type.util.CamelCaseToSnakeCaseNamingStrategy
-quarkus.hibernate-orm.dialect = io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect
+quarkus.hibernate-orm.jdbc.statement-batch-size = 200
+quarkus.hibernate-orm.jdbc.statement-fetch-size = 1000
+quarkus.hibernate-orm.fetch.batch-size = 200
+quarkus.hibernate-orm.fetch.max-depth = 5