The Hibernate.initialize(proxy)
is useful only if you are using the second-level cache. Otherwise, you are going to issue a second query which is less efficient than just initializing the proxy with the initial query.
Risking N+1 query issues
So, when executing the following test case:
LOGGER.info("Clear the second-level cache");
entityManager.getEntityManagerFactory().getCache().evictAll();
LOGGER.info("Loading a PostComment");
PostComment comment = entityManager.find(
PostComment.class,
1L
);
assertEquals(
"A must read!",
comment.getReview()
);
Post post = comment.getPost();
LOGGER.info("Post entity class: {}", post.getClass().getName());
Hibernate.initialize(post);
assertEquals(
"High-Performance Java Persistence",
post.getTitle()
);
First, we are going to clear the second-level cache since, unless you explicitly enable the second-level cache and configure a provider, Hibernate is not going to use the second-level cache.
When running this test case, Hibernate executes the following SQL statements:
-- Clear the second-level cache
-- Evicting entity cache: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post
-- Evicting entity cache: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment
-- Loading a PostComment
SELECT pc.id AS id1_1_0_,
pc.post_id AS post_id3_1_0_,
pc.review AS review2_1_0_
FROM post_comment pc
WHERE pc.id=1
-- Post entity class: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post$HibernateProxy$5LVxadxF
SELECT p.id AS id1_0_0_,
p.title AS title2_0_0_
FROM post p
WHERE p.id=1
We can see that the second-level cache was properly evicted and that, after fetching the PostComment
entity, the post entity is represented by a HibernateProxy
instance which only contains the Post
entity identifier that was retrieved from the post_id
column of the post_comment database table row.
Now, due to the call to the Hibernate.initialize
method, a secondary SQL query is executed to fetch the Post
entity, and that’s not very efficient and can lead to N+1 query issues.
Using JOIN FETCH with JPQL
In the previous case, the PostComment
should be fetched along with its post association using the JOIN FETCH JPQL directive.
LOGGER.info("Clear the second-level cache");
entityManager.getEntityManagerFactory().getCache().evictAll();
LOGGER.info("Loading a PostComment");
PostComment comment = entityManager.createQuery(
"select pc " +
"from PostComment pc " +
"join fetch pc.post " +
"where pc.id = :id", PostComment.class)
.setParameter("id", 1L)
.getSingleResult();
assertEquals(
"A must read!",
comment.getReview()
);
Post post = comment.getPost();
LOGGER.info("Post entity class: {}", post.getClass().getName());
assertEquals(
"High-Performance Java Persistence",
post.getTitle()
);
This time, Hibernate execute a single SQL statement, and we no longer risk to bump into N+1 query issues:
-- Clear the second-level cache
-- Evicting entity cache: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post
-- Evicting entity cache: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment
-- Loading a PostComment
SELECT pc.id AS id1_1_0_,
p.id AS id1_0_1_,
pc.post_id AS post_id3_1_0_,
pc.review AS review2_1_0_,
p.title AS title2_0_1_
FROM post_comment pc
INNER JOIN post p ON pc.post_id=p.id
WHERE pc.id=1
-- Post entity class: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post
Using the 2nd-level cache with Hibernate.initialize
So, to see when the Hibernate.initialize
is really worth using, you need to use the second-level cache:
LOGGER.info("Loading a PostComment");
PostComment comment = entityManager.find(
PostComment.class,
1L
);
assertEquals(
"A must read!",
comment.getReview()
);
Post post = comment.getPost();
LOGGER.info("Post entity class: {}", post.getClass().getName());
Hibernate.initialize(post);
assertEquals(
"High-Performance Java Persistence",
post.getTitle()
);
This time, we are no longer evicting the second-level cache regions, and, since we are using the READ_WRITE cache concurrency strategy, the entities are cached right after they get persisted, hence no SQL query is needed to be executed when running the test case above:
-- Loading a PostComment
-- Cache hit :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment#1`
-- Proxy class: com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post$HibernateProxy$rnxGtvMK
-- Cache hit :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post#1`
Both the PostComment
and the post
association are fetched from the second-level cache as illustrated by the Cache hit log messages.
So, if you are using the second-level cache, it’s fine to use the Hibernate.initiaize
to fetch extra associations that you need to fulfill your business use case. In this case, even if you have N+1 cache calls, each call should run very quickly since the second-level cache is configured properly and data is returned from the memory.
Hibernate.initialize
and proxy collection
The Hibernate.initialize
can be used for collections as well. Now, because second-level cache collections are read-through, meaning that they are stored in the cache the first time they get loaded when running the following test case:
LOGGER.info("Loading a Post");
Post post = entityManager.find(
Post.class,
1L
);
List<PostComment> comments = post.getComments();
LOGGER.info("Collection class: {}", comments.getClass().getName());
Hibernate.initialize(comments);
LOGGER.info("Post comments: {}", comments);
Hibernate executes an SQL query to load the PostComment
collection:
-- Loading a Post
-- Cache hit :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post#1`
-- Collection class: org.hibernate.collection.internal.PersistentBag
- Cache hit, but item is unreadable/invalid :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post.comments`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post.comments#1`
SELECT pc.post_id AS post_id3_1_0_,
pc.id AS id1_1_0_,
pc.id AS id1_1_1_,
pc.post_id AS post_id3_1_1_,
pc.review AS review2_1_1_
FROM post_comment pc
WHERE pc.post_id=1
-- Post comments: [
PostComment{id=1, review='A must read!'},
PostComment{id=2, review='Awesome!'},
PostComment{id=3, review='5 stars'}
]
However, if the PostComment
collection is already cached:
doInJPA(entityManager -> {
Post post = entityManager.find(Post.class, 1L);
assertEquals(3, post.getComments().size());
});
When running the previous test case, Hibernate can fetch all data from the cache only:
-- Loading a Post
-- Cache hit :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post#1`
-- Collection class: org.hibernate.collection.internal.PersistentBag
-- Cache hit :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post.comments`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$Post.comments#1`
-- Cache hit :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment#1`
-- Cache hit :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment#2`
-- Cache hit :
region = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment`,
key = `com.vladmihalcea.book.hpjp.hibernate.fetching.HibernateInitializeTest$PostComment#3`