blob: b2efeb92edcd7d8643eb4be5948d84d04b9f45de [file] [log] [blame]
= Highly Efficient Enterprise Data Access
Matthias Andreas Benkard
// Meta
:experimental:
:data-uri:
:toc:
:stem:
:toclevels: 2
:description: Slides for my Hibernate Patterns and Antipatterns talk
:keywords: mulk
// Settings
:icons: font
:revealjs_theme: simple
:revealjs_width: 1280
:revealjs_height: 800
:customcss: SLIDES.css
:source-highlighter: highlightjs
:highlightjs-theme: grayscale.css
:table-stripes: hover
:stem:
[%notitle]
== Setting the Stage
image:UML.png[]
[%notitle]
[.columns]
=== Posts
[source,java,data-lines=]
[.column]
[.is-two-thirds]
----
@Entity
public class Post extends PanacheEntity {
public String title;
public Instant publicationDate;
public String body;
@ManyToOne(fetch = FetchType.LAZY) //<1>
public Author author;
@ManyToMany(fetch = FetchType.LAZY) //<2>
public Collection<Category> categories;
@OneToMany(fetch = FetchType.LAZY,
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]
=== Authors, Login Credentials
[source,java]
----
@Entity
public class Author extends PanacheEntity {
public String name;
@OneToOne(fetch = FetchType.LAZY, //<1>
mappedBy = "author")
public BasicCredentials basicCredentials;
}
----
[source,java]
----
@Entity
public class BasicCredentials extends PanacheEntity {
@OneToOne(fetch = FetchType.LAZY)
@MapsId
public Author author;
public String username;
public String password;
}
----
[%notitle]
//[.columns]
== Cartesian Blowup
[source,java]
----
@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;
----
[%notitle]
=== Bad Results (1)
--
[source]
----
[2021-02-27 10:32:58] 60 rows retrieved
----
stem:[60 = ubrace|"posts"|_10 * ubrace|("comments")/("post")|_3 * ubrace|"categories"|_2]
Cartesian explosion! 🙀
What gives?
--
[%notitle]
=== Bad Results (2)
[%header%autofit.supersmall,format=tsv,stripes=hover]
|===
post_id com_id cat_id body title comment_author com_pubdate spamp com_text cat_name
4 16 44 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133969 HAM First post Category #1
4 15 44 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133963 SPAM First post! Category #1
4 14 44 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133870 UNKNOWN First post!! Category #1
4 16 45 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133969 HAM First post Category #0
4 15 45 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133963 SPAM First post! Category #0
4 14 45 "foo" Post #0 Anonymous Coward 2021-02-27 10:32:16.133870 UNKNOWN First post!! Category #0
5 19 44 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135200 SPAM Bah Category #1
5 18 44 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135192 SPAM OK Category #1
5 17 44 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135205 UNKNOWN Meh. Category #1
5 19 45 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135200 SPAM Bah Category #0
5 18 45 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135192 SPAM OK Category #0
5 17 45 "bar!" Post #1 Anonymous Coward 2021-02-27 10:32:16.135205 UNKNOWN Meh. Category #0
|===
...
[%notitle]
=== Fix: Sequential Fetch Queries
[source,java,linenums,highlight="|4-9|11-18|15,17"]
----
@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]
== First-Level Caching for the Win
[source,java,linenums,highlight="|4-9|11-12|14-17"]
----
@Transactional
public void updateCommentStatus() {
List<Comment> comments = Comment.find( //<1>
"""
SELECT c FROM Comment c
WHERE c.spamStatus = 'UNKNOWN'
""")
.list();
Map<Long, SpamState> 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,linenums,highlight="|6,7"]
----
@Entity
public class Author extends PanacheEntity {
public String name;
@OneToOne(fetch = FetchType.LAZY,
mappedBy = "author")
public BasicCredentials basicCredentials;
}
----
[source,java,linenums,highlight="0"]
----
@Entity
public class BasicCredentials extends PanacheEntity {
@OneToOne(fetch = FetchType.LAZY)
@MapsId
public Author author;
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,linenums,highlight="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]
=== Fix: Bytecode Enhancement (Gradle)
[source,groovy,linenums,highlight="5|11"]
----
apply plugin: 'org.hibernate.orm'
buildscript {
dependencies {
classpath "org.hibernate:hibernate-gradle-plugin:$hibernateVersion"
}
}
hibernate {
enhance {
enableLazyInitialization = true
enableDirtyTracking = false
enableAssociationManagement = false
}
}
----
[%notitle]
=== Fix: Bytecode Enhancement (Maven)
[source,xml,linenums,highlight="3|10"]
----
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>${hibernate.version}</version>
<executions>
<execution>
<configuration>
<enableLazyInitialization>true</enableLazyInitialization>
<enableDirtyTracking>false</enableDirtyTracking>
<enableAssociationManagement>false</enableAssociationManagement>
<failOnError>true</failOnError>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
----
[%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.columns]
== Projections
[source,java]
[.column.is-one-half]
----
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 pubDate,
int commentCount) {
this.authorName = authorName;
this.title = title;
this.publicationDate = pubDate;
this.commentCount = commentCount;
}
}
----
[source,java]
[.column.is-one-half]
----
@Transactional
public List<PostSummary> overview() {
return Post.<Post>find(
"""
SELECT p FROM Post p
LEFT JOIN FETCH p.author
LEFT JOIN FETCH p.comments
""")
.stream()
.map((Post p) ->
new PostSummary(
p.author.name,
p.title,
p.publicationDate,
p.comments.size()))
.collect(Collectors.toList());
}
----
[%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]
=== 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
----
== Conclusion
[%step]
* Use a DTO projection if you can.
* If not:
[%step]
** Check your ``@OneToOne``s. Use `@LazyToOne` and bytecode enhancement if necessary.
** Don't `LEFT JOIN FETCH` more than one collection at once. Use sequential queries instead.
** The 1st-level cache is your friend. Use it.