blog: More slides.

Change-Id: I4145cd3acdf3a4bbe072e34fa856171081045b19
diff --git a/blog/src/main/asciidoc/SLIDES.adoc b/blog/src/main/asciidoc/SLIDES.adoc
index 26d74cf..e0652c8 100644
--- a/blog/src/main/asciidoc/SLIDES.adoc
+++ b/blog/src/main/asciidoc/SLIDES.adoc
@@ -3,7 +3,6 @@
 // Meta
 :experimental:
 :data-uri:
-:sectnums:
 :toc:
 :stem:
 :toclevels: 2
@@ -18,20 +17,27 @@
 //:revealjs_height: 1200
 //:revealjs_customtheme: SLIDES.css
 :customcss: SLIDES.css
-:source-highlighter: rouge
-//:source-highlighter: highlightjs
+//:source-highlighter: rouge
+:source-highlighter: highlightjs
+//:highlightjs-theme: https://cdn.jsdelivr.net/npm/reveal.js@3.9.2/lib/css/zenburn.css
+//:highlightjs-theme: https://cdn.jsdelivr.net/npm/reveal.js@3.9.2/lib/css/monokai.css
+//:highlightjs-theme: tomorrow.css
+:highlightjs-theme: grayscale.css
+:stem:
 
 
+[%notitle]
 == Setting the Stage
 
 image:UML.png[]
 
+[%notitle]
 [.columns]
 === Posts
 
+[source,java,data-lines=]
 [.column]
---
-[source,java]
+[.is-two-thirds]
 ----
 @Entity
 public class Post extends PanacheEntity {
@@ -40,21 +46,27 @@
   public Instant publicationDate;
   public String body;
 
-  @ManyToOne(fetch = FetchType.LAZY)
+  @ManyToOne(fetch = FetchType.LAZY)         //<1>
   public Author author;
 
-  @ManyToMany(fetch = FetchType.LAZY)
-  public Set<Category> categories;
+  @ManyToMany(fetch = FetchType.LAZY)        //<2>
+  public Collection<Category> categories;
 
   @OneToMany(fetch = FetchType.LAZY,
-             mappedBy = "post")
-  public Set<Comment> comments;
+             mappedBy = "post")              //<3>
+  public Collection<Comment> comments;
 }
 ----
---
 
 [.column]
---
+<1> lazy is good (can always `JOIN FETCH` later)
+<2> `@ManyToMany` always implies a join table
+<3> mapping without a join table, non-owning side
+
+
+[%notitle]
+=== Comments and Categories
+
 [source,java]
 ----
 @Entity
@@ -80,9 +92,9 @@
   public String name;
 }
 ----
---
 
-[.stretch]
+
+[%notitle]
 === Authors, Login Credentials
 
 [source,java]
@@ -92,7 +104,7 @@
 
   public String name;
 
-  @OneToOne(fetch = FetchType.LAZY,
+  @OneToOne(fetch = FetchType.LAZY,        //<1>
             mappedBy = "author")
   @LazyToOne(LazyToOneOption.NO_PROXY)
   public BasicCredentials basicCredentials;
@@ -113,52 +125,91 @@
 }
 ----
 
+
+[%notitle]
+//[.columns]
 == Cartesian Blowup
 
-[source,sql]
+[source,java]
 ----
-    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;
+@Transactional
+public List<Post> getAllWithCommentsAndCategories() {
+
+  return Post.find(
+      """
+      SELECT p FROM Post p
+        LEFT JOIN FETCH p.comments
+        LEFT JOIN FETCH p.categories
+      """)
+      .list();
+}
 ----
 
+[%notitle]
+=== Bad SQL
+
+[source,sql]
+//[.column]
+//[.is-two-thirds]
+----
+select
+    post0_.id,
+    comments1_.id,
+    category3_.id,
+    post0_.author_id,
+    post0_.body,
+    post0_.publication_date,
+    post0_.title,
+    comments1_.author_name,
+    comments1_.post_id,
+    comments1_.publication_date,
+    comments1_.spam_status,
+    comments1_.text,
+    comments1_.post_id,
+    comments1_.id,
+    category3_.name,
+    categories2_.post_id,
+    categories2_.categories_id
+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;
+----
+
+//[.column]
+//[.is-one-third]
+
+[%notitle]
+=== Bad Results (1)
+
+--
 [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!
+[2021-02-27 10:32:58] 60 rows retrieved
 ----
 
-[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
+stem:[60 = ubrace|"posts"|_10 *
+ubrace|("comments")/("post")|_3 * ubrace|"categories"|_2]
+
+Cartesian explosion! 🙀
+
+What gives?
+--
+
+[%notitle]
+=== Bad Results (2)
+
+[%header,format=tsv]
+[.supersmall]
+|===
+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
@@ -219,4 +270,441 @@
 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
+|===
+
+
+[%notitle]
+=== Fix: Sequential Fetch Queries
+
+[source,java]
 ----
+@Transactional
+public List<Post> getAllWithCommentsAndCategories2() {
+
+  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;
+}
+----
+
+
+[%notitle]
+[.columns]
+=== Good SQL
+
+[source,sql]
+[.column]
+----
+select
+    post0_.id,
+    comments1_.id,
+    post0_.author_id,
+    post0_.body,
+    post0_.publication_date,
+    post0_.title,
+    comments1_.author_name,
+    comments1_.post_id,
+    comments1_.publication_date,
+    comments1_.spam_status,
+    comments1_.text,
+    comments1_.post_id,
+    comments1_.id
+from
+    post post0_
+left outer join
+    comment comments1_
+        on post0_.id=comments1_.post_id
+----
+
+[source,sql]
+[.column]
+----
+select
+    distinct post0_.id,
+    cat2_.id,
+    post0_.author_id,
+    post0_.body,
+    post0_.publication_date,
+    post0_.title,
+    cat2_.name,
+    cats1_.post_id,
+    cats1_.categories_id
+from
+    post post0_
+left outer join
+    post_category cats1_
+        on post0_.id=cats1_.post_id
+left outer join
+    category cat2_
+        on cats1_.categories_id=cat2_.id
+where
+    post0_.id in (
+        ? , ? , ? , ? , ? , ? , ? , ? ,
+        ? , ? , ? , ? , ? , ? , ? , ? ,
+        ? , ? , ? , ? , ? , ? , ? , ? ,
+        ? , ? , ? , ? , ? , ?
+    )
+----
+
+
+[%notitle]
+== Update without `#persist`
+
+[source,java]
+----
+@Transactional
+public void resetCommentStatus() {
+
+  List<Comment> comments = Comment.find(
+      """
+      SELECT c FROM Comment c
+       WHERE c.spamStatus <> 'UNKNOWN'
+      """)
+      .list();
+
+  comments.forEach(c -> c.spamStatus = SpamStatus.UNKNOWN);
+}
+----
+
+
+[%notitle]
+=== Good SQL
+
+[source,sql]
+----
+select
+    comment0_.id,
+    comment0_.author_name,
+    comment0_.post_id,
+    comment0_.publication_date,
+    comment0_.spam_status,
+    comment0_.text
+from
+    comment comment0_
+where
+    comment0_.spam_status<>'UNKNOWN'
+----
+
+[source,sql]
+----
+update
+    comment
+set
+    author_name=?,
+    post_id=?,
+    publication_date=?,
+    spam_status=?,
+    text=?
+where
+    id=?
+
+update
+...
+----
+
+
+[%notitle]
+== First-Level Caching for the Win
+
+[source,java]
+----
+@Transactional
+public void updateCommentStatus() {
+
+  List<Comment> comments = Comment.find(                      //<1>
+      """
+      SELECT c FROM Comment c
+       WHERE c.spamStatus = 'UNKNOWN'
+      """)
+      .list();
+
+  var assessments = spamAssessmentService.assess(comments);   //<2>
+
+  for (var assessment : assessments.entrySet()) {
+    Comment comment = Comment.findById(assessment.getKey());  //<3>
+    comment.spamStatus = assessment.getValue();
+  }
+}
+----
+
+
+[%notitle]
+=== Good SQL
+
+[source,sql]
+----
+select
+    comment0_.id,
+    comment0_.author_name,
+    comment0_.post_id,
+    comment0_.publication_date,
+    comment0_.spam_status,
+    comment0_.text
+from
+    comment comment0_
+where
+    comment0_.spam_status='UNKNOWN'
+----
+
+[source,sql]
+----
+update
+    comment
+set
+    author_name=?,
+    post_id=?,
+    publication_date=?,
+    spam_status=?,
+    text=?
+where
+    id=?
+
+update
+...
+----
+
+[%notitle]
+== `@OneToOne` Surprises
+
+[source,java]
+----
+@Transactional
+public List<Post> getAllWithAuthors() {
+
+  return Post.find(
+      """
+      SELECT p FROM Post p
+        LEFT JOIN FETCH p.author
+      """)
+      .list();
+}
+----
+
+
+[%notitle]
+[.columns]
+=== Bad SQL
+
+[source,sql]
+[.column]
+----
+select
+    post0_.id,
+    author1_.id,
+    post0_.author_id,
+    post0_.body,
+    post0_.publication_date,
+    post0_.title,
+    author1_.name
+from
+    post post0_
+left outer join
+    author author1_
+        on post0_.author_id=author1_.id
+
+select
+    basiccrede0_.author_id,
+    basiccrede0_.password,
+    basiccrede0_.username
+from
+    basic_credentials basiccrede0_
+where
+    basiccrede0_.author_id=?
+----
+
+[source,sql]
+[.column]
+----
+select
+    basiccrede0_.author_id,
+    basiccrede0_.password,
+    basiccrede0_.username
+from
+    basic_credentials basiccrede0_
+where
+    basiccrede0_.author_id=?
+
+select
+    basiccrede0_.author_id,
+    basiccrede0_.password,
+    basiccrede0_.username
+from
+    basic_credentials basiccrede0_
+where
+    basiccrede0_.author_id=?
+----
+
+
+[%notitle]
+=== Another Look at the Author
+
+[source,java]
+----
+@Entity
+public class Author extends PanacheEntity {
+
+  public String name;
+
+  @OneToOne(fetch = FetchType.LAZY,
+            mappedBy = "author")
+  public BasicCredentials basicCredentials;  //<1>
+}
+----
+
+[source,java]
+----
+@Entity
+public class BasicCredentials extends PanacheEntity {
+
+  @OneToOne(fetch = FetchType.LAZY)
+  @MapsId
+  public Author author;                      //<2>
+
+  public String username;
+  public String password;
+}
+----
+
+
+=== Why?
+
+Hibernate has no way of knowing whether `Author#basicCredentials` is `null`!
+
+
+[%notitle]
+[.columns]
+=== Fix: `@LazyToOne(NO_PROXY)`
+
+[source,java,data-line-numbers=8]
+[.column]
+[.is-two-thirds]
+----
+@Entity
+public class Author extends PanacheEntity {
+
+  public String name;
+
+  @OneToOne(fetch = FetchType.LAZY,
+            mappedBy = "author")
+  @LazyToOne(LazyToOneOption.NO_PROXY)  //<1>
+  public BasicCredentials basicCredentials;
+}
+----
+
+[.column]
+<1> `NO_PROXY` + bytecode enhancement = profit
+
+
+[%notitle]
+=== Good SQL
+
+[source,sql]
+----
+select
+    post0_.id,
+    author1_.id,
+    post0_.author_id,
+    post0_.body,
+    post0_.publication_date,
+    post0_.title,
+    author1_.name
+from
+    post post0_
+left outer join
+    author author1_
+        on post0_.author_id=author1_.id
+----
+
+[%notitle]
+== Projection Queries
+
+[source,java]
+----
+@Transactional
+public List<PostSummary> overview() {
+
+  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();
+}
+----
+
+
+[%notitle]
+=== Projection DTOs
+
+[source,java]
+----
+public final class PostSummary {
+
+  public final String authorName;
+  public final String title;
+  public final Instant publicationDate;
+  public final int commentCount;
+
+  public PostSummary(                        //<1>
+      String authorName,
+      String title,
+      Instant publicationDate,
+      int commentCount) {
+    this.authorName = authorName;
+    this.title = title;
+    this.publicationDate = publicationDate;
+    this.commentCount = commentCount;
+  }
+}
+----
+
+
+[%notitle]
+=== Good SQL
+
+[source,sql]
+----
+select
+    author1_.name,
+    post0_.title,
+    post0_.publication_date,
+    (select
+        count(comments2_.post_id)
+     from
+        comment comments2_
+     where
+        post0_.id = comments2_.post_id)
+from
+    post post0_
+cross join
+    author author1_
+where
+    post0_.author_id=author1_.id
+----
+
+
+[%notitle]
+== Conclusion
+
+- Lazy is good
+- Lazier is better
+- Lazy with bytecode enhancement is best
+- Sequential queries avoid cartesian blowup
+- The 1st-level cache is your friend
+- Use DTO projections everywhere all the time
diff --git a/blog/src/main/asciidoc/SLIDES.css b/blog/src/main/asciidoc/SLIDES.css
index b3bed7d..b3e362e 100644
--- a/blog/src/main/asciidoc/SLIDES.css
+++ b/blog/src/main/asciidoc/SLIDES.css
@@ -1,3 +1,7 @@
 .reveal pre code {
     max-height: 100% !important;
 }
+
+.supersmall {
+    font-size: x-small;
+}
diff --git a/blog/src/main/asciidoc/UML.uml b/blog/src/main/asciidoc/UML.uml
index 120c56a..1498101 100644
--- a/blog/src/main/asciidoc/UML.uml
+++ b/blog/src/main/asciidoc/UML.uml
@@ -5,7 +5,7 @@
   <nodes>
     <node x="-20.5" y="369.5">Comment#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/comments/Comment.java</node>
     <node x="205.0" y="-8.5">BasicCredentials#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/authors/BasicCredentials.java</node>
-    <node x="-17.5" y="60.5">Post#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/posts/Post.java</node>
+    <node x="211.5" y="111.5">Post#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/posts/Post.java</node>
     <node x="259.5" y="202.5">Category#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/posts/Category.java</node>
     <node x="19.5" y="2.5">Author#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/authors/Author.java</node>
   </nodes>
@@ -19,16 +19,16 @@
       <point x="97.5" y="0.0" />
       <point x="-60.5" y="0.0" />
     </edge>
-    <edge source="Comment#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/comments/Comment.java" target="Post#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/posts/Post.java">
-      <point x="0.0" y="-70.5" />
-      <point x="0.0" y="59.5" />
-    </edge>
     <edge source="Author#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/authors/Author.java" target="BasicCredentials#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/authors/BasicCredentials.java">
       <point x="60.5" y="0.0" />
       <point x="-75.0" y="0.0" />
     </edge>
+    <edge source="Comment#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/comments/Comment.java" target="Post#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/posts/Post.java">
+      <point x="0.0" y="-70.5" />
+      <point x="0.0" y="59.5" />
+    </edge>
   </edges>
-  <settings layout="Orthogonal" zoom="1.0" x="362.5" y="257.0" />
+  <settings layout="Orthogonal" zoom="1.0" x="399.5" y="272.5" />
   <SelectedNodes>
     <node>Post#file:///Users/mulk/Arbeitskasten/demos/blog/src/main/java/eu/mulk/demos/blog/posts/Post.java</node>
   </SelectedNodes>