blob: b2efeb92edcd7d8643eb4be5948d84d04b9f45de [file] [log] [blame]
Matthias Andreas Benkard36b0f042021-02-27 10:46:04 +01001= Highly Efficient Enterprise Data Access
2Matthias Andreas Benkard
3// Meta
4:experimental:
5:data-uri:
Matthias Andreas Benkard36b0f042021-02-27 10:46:04 +01006:toc:
7:stem:
8:toclevels: 2
9:description: Slides for my Hibernate Patterns and Antipatterns talk
10:keywords: mulk
11// Settings
12:icons: font
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010013:revealjs_theme: simple
14:revealjs_width: 1280
15:revealjs_height: 800
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010016:customcss: SLIDES.css
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010017:source-highlighter: highlightjs
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010018:highlightjs-theme: grayscale.css
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +010019:table-stripes: hover
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010020:stem:
Matthias Andreas Benkard36b0f042021-02-27 10:46:04 +010021
22
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010023[%notitle]
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010024== Setting the Stage
25
26image:UML.png[]
27
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010028[%notitle]
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010029[.columns]
30=== Posts
31
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010032[source,java,data-lines=]
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010033[.column]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010034[.is-two-thirds]
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010035----
36@Entity
37public class Post extends PanacheEntity {
38
39 public String title;
40 public Instant publicationDate;
41 public String body;
42
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010043 @ManyToOne(fetch = FetchType.LAZY) //<1>
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010044 public Author author;
45
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010046 @ManyToMany(fetch = FetchType.LAZY) //<2>
47 public Collection<Category> categories;
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010048
49 @OneToMany(fetch = FetchType.LAZY,
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010050 mappedBy = "post") //<3>
51 public Collection<Comment> comments;
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010052}
53----
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010054
55[.column]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010056<1> lazy is good (can always `JOIN FETCH` later)
57<2> `@ManyToMany` always implies a join table
58<3> mapping without a join table, non-owning side
59
60
61[%notitle]
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010062=== Authors, Login Credentials
63
64[source,java]
65----
66@Entity
67public class Author extends PanacheEntity {
68
69 public String name;
70
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010071 @OneToOne(fetch = FetchType.LAZY, //<1>
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010072 mappedBy = "author")
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010073 public BasicCredentials basicCredentials;
74}
75----
76
77[source,java]
78----
79@Entity
80public class BasicCredentials extends PanacheEntity {
81
82 @OneToOne(fetch = FetchType.LAZY)
83 @MapsId
84 public Author author;
85
86 public String username;
87 public String password;
88}
89----
90
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010091
92[%notitle]
93//[.columns]
Matthias Andreas Benkard08492672021-02-27 13:57:48 +010094== Cartesian Blowup
95
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010096[source,java]
Matthias Andreas Benkard36b0f042021-02-27 10:46:04 +010097----
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +010098@Transactional
99public List<Post> getAllWithCommentsAndCategories() {
100
101 return Post.find(
102 """
103 SELECT p FROM Post p
104 LEFT JOIN FETCH p.comments
105 LEFT JOIN FETCH p.categories
106 """)
107 .list();
108}
Matthias Andreas Benkard36b0f042021-02-27 10:46:04 +0100109----
110
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100111[%notitle]
112=== Bad SQL
113
114[source,sql]
115//[.column]
116//[.is-two-thirds]
117----
118select
119 post0_.id,
120 comments1_.id,
121 category3_.id,
122 post0_.author_id,
123 post0_.body,
124 post0_.publication_date,
125 post0_.title,
126 comments1_.author_name,
127 comments1_.post_id,
128 comments1_.publication_date,
129 comments1_.spam_status,
130 comments1_.text,
131 comments1_.post_id,
132 comments1_.id,
133 category3_.name,
134 categories2_.post_id,
135 categories2_.categories_id
136from
137 post post0_
138left outer join
139 comment comments1_
140 on post0_.id=comments1_.post_id
141left outer join
142 post_category categories2_
143 on post0_.id=categories2_.post_id
144left outer join
145 category category3_
146 on categories2_.categories_id=category3_.id;
147----
148
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100149[%notitle]
150=== Bad Results (1)
151
152--
Matthias Andreas Benkard36b0f042021-02-27 10:46:04 +0100153[source]
154----
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100155[2021-02-27 10:32:58] 60 rows retrieved
Matthias Andreas Benkard36b0f042021-02-27 10:46:04 +0100156----
157
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100158stem:[60 = ubrace|"posts"|_10 * ubrace|("comments")/("post")|_3 * ubrace|"categories"|_2]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100159
160Cartesian explosion! 🙀
161
162What gives?
163--
164
165[%notitle]
166=== Bad Results (2)
167
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100168[%header%autofit.supersmall,format=tsv,stripes=hover]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100169|===
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100170post_id com_id cat_id body title comment_author com_pubdate spamp com_text cat_name
1714 16 44 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133969 HAM First post Category #1
1724 15 44 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133963 SPAM First post! Category #1
1734 14 44 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133870 UNKNOWN First post!! Category #1
1744 16 45 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133969 HAM First post Category #0
1754 15 45 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133963 SPAM First post! Category #0
1764 14 45 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133870 UNKNOWN First post!! Category #0
1775 19 44 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135200 SPAM Bah Category #1
1785 18 44 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135192 SPAM OK Category #1
1795 17 44 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135205 UNKNOWN Meh. Category #1
1805 19 45 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135200 SPAM Bah Category #0
1815 18 45 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135192 SPAM OK Category #0
1825 17 45 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135205 UNKNOWN Meh. Category #0
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100183|===
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100184...
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100185
186[%notitle]
187=== Fix: Sequential Fetch Queries
188
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100189[source,java,linenums,highlight="|4-9|11-18|15,17"]
Matthias Andreas Benkard36b0f042021-02-27 10:46:04 +0100190----
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100191@Transactional
192public List<Post> getAllWithCommentsAndCategories2() {
193
194 List<Post> posts = Post.find(
195 """
196 SELECT p FROM Post p
197 LEFT JOIN FETCH p.comments
198 """)
199 .list();
200
201 posts = Post.find(
202 """
203 SELECT DISTINCT p FROM Post p
204 LEFT JOIN FETCH p.categories
205 WHERE p IN (?1)
206 """,
207 posts)
208 .list();
209
210 return posts;
211}
212----
213
214
215[%notitle]
216[.columns]
217=== Good SQL
218
219[source,sql]
220[.column]
221----
222select
223 post0_.id,
224 comments1_.id,
225 post0_.author_id,
226 post0_.body,
227 post0_.publication_date,
228 post0_.title,
229 comments1_.author_name,
230 comments1_.post_id,
231 comments1_.publication_date,
232 comments1_.spam_status,
233 comments1_.text,
234 comments1_.post_id,
235 comments1_.id
236from
237 post post0_
238left outer join
239 comment comments1_
240 on post0_.id=comments1_.post_id
241----
242
243[source,sql]
244[.column]
245----
246select
247 distinct post0_.id,
248 cat2_.id,
249 post0_.author_id,
250 post0_.body,
251 post0_.publication_date,
252 post0_.title,
253 cat2_.name,
254 cats1_.post_id,
255 cats1_.categories_id
256from
257 post post0_
258left outer join
259 post_category cats1_
260 on post0_.id=cats1_.post_id
261left outer join
262 category cat2_
263 on cats1_.categories_id=cat2_.id
264where
265 post0_.id in (
266 ? , ? , ? , ? , ? , ? , ? , ? ,
267 ? , ? , ? , ? , ? , ? , ? , ? ,
268 ? , ? , ? , ? , ? , ? , ? , ? ,
269 ? , ? , ? , ? , ? , ?
270 )
271----
272
273
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100274[%notitle]
275== First-Level Caching for the Win
276
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100277[source,java,linenums,highlight="|4-9|11-12|14-17"]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100278----
279@Transactional
280public void updateCommentStatus() {
281
282 List<Comment> comments = Comment.find( //<1>
283 """
284 SELECT c FROM Comment c
285 WHERE c.spamStatus = 'UNKNOWN'
286 """)
287 .list();
288
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100289 Map<Long, SpamState> assessments =
290 spamAssessmentService.assess(comments); //<2>
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100291
292 for (var assessment : assessments.entrySet()) {
293 Comment comment = Comment.findById(assessment.getKey()); //<3>
294 comment.spamStatus = assessment.getValue();
295 }
296}
297----
298
299
300[%notitle]
301=== Good SQL
302
303[source,sql]
304----
305select
306 comment0_.id,
307 comment0_.author_name,
308 comment0_.post_id,
309 comment0_.publication_date,
310 comment0_.spam_status,
311 comment0_.text
312from
313 comment comment0_
314where
315 comment0_.spam_status='UNKNOWN'
316----
317
318[source,sql]
319----
320update
321 comment
322set
323 author_name=?,
324 post_id=?,
325 publication_date=?,
326 spam_status=?,
327 text=?
328where
329 id=?
330
331update
332...
333----
334
335[%notitle]
336== `@OneToOne` Surprises
337
338[source,java]
339----
340@Transactional
341public List<Post> getAllWithAuthors() {
342
343 return Post.find(
344 """
345 SELECT p FROM Post p
346 LEFT JOIN FETCH p.author
347 """)
348 .list();
349}
350----
351
352
353[%notitle]
354[.columns]
355=== Bad SQL
356
357[source,sql]
358[.column]
359----
360select
361 post0_.id,
362 author1_.id,
363 post0_.author_id,
364 post0_.body,
365 post0_.publication_date,
366 post0_.title,
367 author1_.name
368from
369 post post0_
370left outer join
371 author author1_
372 on post0_.author_id=author1_.id
373
374select
375 basiccrede0_.author_id,
376 basiccrede0_.password,
377 basiccrede0_.username
378from
379 basic_credentials basiccrede0_
380where
381 basiccrede0_.author_id=?
382----
383
384[source,sql]
385[.column]
386----
387select
388 basiccrede0_.author_id,
389 basiccrede0_.password,
390 basiccrede0_.username
391from
392 basic_credentials basiccrede0_
393where
394 basiccrede0_.author_id=?
395
396select
397 basiccrede0_.author_id,
398 basiccrede0_.password,
399 basiccrede0_.username
400from
401 basic_credentials basiccrede0_
402where
403 basiccrede0_.author_id=?
404----
405
406
407[%notitle]
408=== Another Look at the Author
409
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100410[source,java,linenums,highlight="|6,7"]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100411----
412@Entity
413public class Author extends PanacheEntity {
414
415 public String name;
416
417 @OneToOne(fetch = FetchType.LAZY,
418 mappedBy = "author")
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100419 public BasicCredentials basicCredentials;
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100420}
421----
422
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100423[source,java,linenums,highlight="0"]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100424----
425@Entity
426public class BasicCredentials extends PanacheEntity {
427
428 @OneToOne(fetch = FetchType.LAZY)
429 @MapsId
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100430 public Author author;
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100431
432 public String username;
433 public String password;
434}
435----
436
437
438=== Why?
439
440Hibernate has no way of knowing whether `Author#basicCredentials` is `null`!
441
442
443[%notitle]
444[.columns]
445=== Fix: `@LazyToOne(NO_PROXY)`
446
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100447[source,java,linenums,highlight="8"]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100448[.column]
449[.is-two-thirds]
450----
451@Entity
452public class Author extends PanacheEntity {
453
454 public String name;
455
456 @OneToOne(fetch = FetchType.LAZY,
457 mappedBy = "author")
458 @LazyToOne(LazyToOneOption.NO_PROXY) //<1>
459 public BasicCredentials basicCredentials;
460}
461----
462
463[.column]
464<1> `NO_PROXY` + bytecode enhancement = profit
465
466
467[%notitle]
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100468=== Fix: Bytecode Enhancement (Gradle)
469
470[source,groovy,linenums,highlight="5|11"]
471----
472apply plugin: 'org.hibernate.orm'
473
474buildscript {
475 dependencies {
476 classpath "org.hibernate:hibernate-gradle-plugin:$hibernateVersion"
477 }
478}
479
480hibernate {
481 enhance {
482 enableLazyInitialization = true
483 enableDirtyTracking = false
484 enableAssociationManagement = false
485 }
486}
487----
488
489[%notitle]
490=== Fix: Bytecode Enhancement (Maven)
491
492[source,xml,linenums,highlight="3|10"]
493----
494<plugin>
495 <groupId>org.hibernate.orm.tooling</groupId>
496 <artifactId>hibernate-enhance-maven-plugin</artifactId>
497 <version>${hibernate.version}</version>
498
499 <executions>
500 <execution>
501
502 <configuration>
503 <enableLazyInitialization>true</enableLazyInitialization>
504 <enableDirtyTracking>false</enableDirtyTracking>
505 <enableAssociationManagement>false</enableAssociationManagement>
506 <failOnError>true</failOnError>
507 </configuration>
508
509 <goals>
510 <goal>enhance</goal>
511 </goals>
512
513 </execution>
514 </executions>
515</plugin>
516----
517
518[%notitle]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100519=== Good SQL
520
521[source,sql]
522----
523select
524 post0_.id,
525 author1_.id,
526 post0_.author_id,
527 post0_.body,
528 post0_.publication_date,
529 post0_.title,
530 author1_.name
531from
532 post post0_
533left outer join
534 author author1_
535 on post0_.author_id=author1_.id
536----
537
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100538[%notitle.columns]
539== Projections
540
541[source,java]
542[.column.is-one-half]
543----
544public final class PostSummary {
545
546 public final String authorName;
547 public final String title;
548 public final Instant publicationDate;
549 public final int commentCount;
550
551 public PostSummary(
552 String authorName,
553 String title,
554 Instant pubDate,
555 int commentCount) {
556 this.authorName = authorName;
557 this.title = title;
558 this.publicationDate = pubDate;
559 this.commentCount = commentCount;
560 }
561}
562----
563
564[source,java]
565[.column.is-one-half]
566----
567@Transactional
568public List<PostSummary> overview() {
569
570 return Post.<Post>find(
571 """
572 SELECT p FROM Post p
573 LEFT JOIN FETCH p.author
574 LEFT JOIN FETCH p.comments
575 """)
576 .stream()
577 .map((Post p) ->
578 new PostSummary(
579 p.author.name,
580 p.title,
581 p.publicationDate,
582 p.comments.size()))
583 .collect(Collectors.toList());
584}
585----
586
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100587[%notitle]
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100588=== Projection Queries
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100589
590[source,java]
591----
592@Transactional
593public List<PostSummary> overview() {
594
595 return entityManager.createQuery(
596 """
597 SELECT NEW eu.mulk.demos.blog.posts.PostSummary(
598 p.author.name, p.title, p.publicationDate, size(p.comments))
599 FROM Post p
600 """,
601 PostSummary.class)
602 .getResultList();
603}
604----
605
606
607[%notitle]
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100608=== Good SQL
609
610[source,sql]
611----
612select
613 author1_.name,
614 post0_.title,
615 post0_.publication_date,
616 (select
617 count(comments2_.post_id)
618 from
619 comment comments2_
620 where
621 post0_.id = comments2_.post_id)
622from
623 post post0_
624cross join
625 author author1_
626where
627 post0_.author_id=author1_.id
628----
629
630
Matthias Andreas Benkard9d00b2e2021-02-27 21:05:29 +0100631== Conclusion
632
Matthias Andreas Benkard10d53fb2021-03-02 16:34:58 +0100633[%step]
634* Use a DTO projection if you can.
635* If not:
636[%step]
637** Check your ``@OneToOne``s. Use `@LazyToOne` and bytecode enhancement if necessary.
638** Don't `LEFT JOIN FETCH` more than one collection at once. Use sequential queries instead.
639** The 1st-level cache is your friend. Use it.