Hibernate is not so evil - my personal JPA experience - Exotic Digital Access
  • Kangundo Road, Nairobi, Kenya
  • support@exoticdigitalaccess.co.ke
  • Opening Time : 07 AM - 10 PM
Hibernate is not so evil - my personal JPA experience

Hibernate is not so evil – my personal JPA experience

Let’s go back to our code example. To enable lazy loading on the Author property in the Post entity, we need to add an additional parameter:

@ManyToOne(fetch = FetchType.LAZY)

because the default FetchType for @ManyToOne relationship is FetchType.EAGER.

So now, if we fetch Post from the database using PostRepository, the generated SQL statement should not include any join on the Author table. That’s what we expect from lazy loading, right? But if we refer (accidentally or not) to some fields, not loaded yet, which are meant to be loaded lazily, we should be able to fetch them “on demand”, shouldn’t we? Let’s take a look at the following test:

@SpringBootTest
@ActiveProfiles("test")
class PostAuthorLazyLoadingFailingTest(
private val postRepository: PostRepository,
private val authorRepository: AuthorRepository,
) : BehaviorSpec({

Given("Author - Jan Nowak") {
val janNowak = Author(firstName = "Jan", lastName = "Nowak").let { authorRepository.save(it) }

When("Jan Nowak creates a Post") {
val postId = Post(title = "First Post", content = "Just hanging around", author = janNowak)
.let { postRepository.save(it) }.id

Then("Fetching lazy loaded property should throw LazyInitializationException") {
shouldThrow<LazyInitializationException> {
postRepository.findByIdOrNull(postId)!!.author.firstName shouldBe "Jan"
}
}
}
}
})

In the test above I save the Author , I save the Post, and after that, I get the Post from the database. Later on, I’m trying to assert the author’s first name, but because of the lazy loading, I’m not allowed to do that. The assertion is failing because of LazyInitializationException:

could not initialize proxy [com.pientaa.hibernatedemo.lazyLoading.Author#1] - no Session
org.hibernate.LazyInitializationException: could not initialize proxy [com.pientaa.hibernatedemo.lazyLoading.Author#1] - no Session

The problem is that Hibernate couldn’t initialize the proxy (used for lazy loading), because “there is no Session”. We need to be really careful using lazy loading in Hibernate, because every time we fetch any lazy-loaded data, we need to have Hibernate Session opened. Well, as I already mentioned at the beginning of the article, we shouldn’t need to use EntityManager or HibernateSession directly, because they are just implementation details. But it turns out that if we’d like to use lazy loading, we should be able to somehow open the session. The question is: what is the right place to do this? Let’s try to answer that question considering the onion architecture.

Hibernate is not so evil - my personal JPA experience
Onion architecture

Domain layer? I don’t think so. Domain code should be perfectly clean and framework-agnostic, so there is no place for Hibernate in this layer, especially in a project with a rich domain model, where DDD makes sense.

Presentation layer? Maybe not a good idea. At some point, it will cause performance issues.

Application layer? Well, it’s better than the presentation layer (regarding performance), but HibernateSession doesn’t seem to belong anywhere else except the data access / infrastructure layer, especially because we already use an abstraction (JPA) and Hibernate is just an implementation detail (JPA provider), therefore it shouldn’t be exposed in other parts of the application than aforementioned infrastructure layer.

So it looks like we already know that session boundaries could be set in the application layer, but HibernateSession itself doesn’t fit there. Hmm, I wish there was some kind of an abstraction on top of HibernateSession that could be used in the application layer…

Hibernate is not so evil - my personal JPA experience

Exactly! Transactions! It does make sense, doesn’t it? Even from the business perspective, there is something like a transactional consistency — some business processes need to be synchronized and guaranteed to be done as an atomic transaction, but their boundary is bigger than just one aggregate. Thus, the transaction looks like a perfect abstraction on top of HibernateSession. So, all we need is @Transactional which marks a method to be run in the scope of a transaction, therefore it opens the HibernateSession for us.

Using lazy loading leads to higher complexity, especially when you need to specify transactions to make it work correctly. Don’t use it as a default strategy. Try to consider all alternatives, especially re-designing your aggregates’ boundaries.

However, LazyInitializationException is not the only pitfall of using lazy loading. Actually, the most popular one is n + 1 problem, which can be really painful when it comes to the application performance.

Lazy loading is about cutting the primary SQL query to the “minimum”, not loading the lazy loaded data until it’s needed. So we don’t JOIN on a table, which is a lazy-loaded association. However, when the lazy loaded data is needed, we will execute additional SQL statements anyway. This causes the n + 1 problem because the data access layer needs to execute “n” additional SQL statements to get the same data as we could retrieve by the primary query.

Let’s take a look at the code example again, which I extended by an additional @OneToMany relationship. I won’t go through the details of the best practices regarding this relationship type in Hibernate, because there is already an excellent article about it, which I totally recommend if you’re not familiar with @OneToMany relationship pitfalls:

Let’s create a new entity class PostComment.

@Entity
class PostComment(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
var content: String,

@ManyToOne
val author: Author,

@ManyToOne
val post: Post
)

And let’s extend the current Post class implementation by additional field comments and methods for removing and adding comments to the post.

@Entity
class Post(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
var title: String,
var content: String,

@OneToMany(mappedBy = "post", cascade = [CascadeType.ALL], orphanRemoval = true)
val comments: MutableSet<PostComment> = mutableSetOf(),

@ManyToOne
val author: Author,
) {
fun addComment(content: String, author: Author) {
comments.add(
PostComment(content = content, author = author, post = this)
)
}

fun removeComment(commentId: Long) {
comments.removeIf { it.id == commentId }
}
}

Notice, that I made an author association eagerly loaded because the relationship we’re going to focus on analyzing the n + 1 problem is @OneToMany. This association in Hibernate is lazy-loaded by default.

The n + 1 problem in our case is n + 1 SELECT statements generated for fetching n Post with their PostComments from the database. Let’s say we have a query (simplified for readability’s sake):

select * from post where id < 6

which returns 5 Posts. Fetching the Posts’ comments results in 5 additional SELECT statements:

select * from post_comment where post_id = 1
select * from post_comment where post_id = 2
select * from post_comment where post_id = 3
select * from post_comment where post_id = 4
select * from post_comment where post_id = 5

This problem wouldn’t exist if we didn’t use lazy loading. Because instead of additional n SELECT statements, we would have JOIN on post_comment table. Of course, if n is a small number, it doesn’t hurt us, but the problem escalates when this number gets bigger. Notice, that if we made all the associations lazy loaded, there will be n additional SQL statements generated for each association. As you can see, the problem can escalate very quickly if we overuse lazy loading. And again, if this number is huge, maybe the aggregate is too big and the problem is in design, not in the persistence layer itself.

However, sometimes we need to decide whether JOIN or n additional SELECTs is better for us. Usually, if we’re sure that we’ll need that association to be loaded entirely in the current request, it’s better to fetch it eagerly, using JOIN. But the same entity might be used in another request in which this association is not needed at all. Some kind of hybrid would be perfect, wouldn’t it? And here we’ve got plenty of options, for example:

and the easiest (in my opinion), which is a simple JPQL query using
join fetch explicitly:

@Query("select distinct p from Post p left join fetch p.comments where p.id = :postId")
fun getPostWithComments(@Param("postId") postId: Long): Post

Taking advantage of the aforementioned JPA options to eagerly fetch a custom set of entity’s associations, you can use FetchType.LAZY as the default FetchType and create projections or for a very specific use case. But it’s still easier and safer not to use FetchType.LAZY at all, so don’t do this unless it’s necessary.

As you can see, Hibernate is not as easy as it seems, but for the very basic usage you don’t need to know everything about its tremendous number of features. For any problem you might have, there is always more than one solution. However, there are some very specific cases in which there is the optimal one, which is highly recommended to choose.

Many problems with the data access layer can be just a result of a wrong application design, so don’t be that hard on Hibernate. It’s not as evil as everybody describes it.

Remember to use the proper abstraction in your domain code. In complex projects, with rich domain model, you shouldn’t use JPA entities directly in your domain code.


Source link

Leave a Reply