Keep SOLID in mind working with Hibernate
Whether you like it or not, but Hibernate is the most popular ORM solution for JVM based languages. I have used it in all companies I worked for(starting from big E-commerce sites and ending with small startups). Honestly I don't like having Hibernate or any JPA implementations in projects I am working on because in my humble opinion it brings a huge layer of complexity(read on to know a reason). There are some alternatives such as JOOQ and MyBatis(personally my favorite one). Also I heard about Spring Data JDBC but didn't have a chance to use it. So why do I think that Hibernate is a bad thing, well , one part of a reason is a documentation but the biggest problem is developers
Documentation
The documentation of Hibernate is really obscure. It doesn't cover a great variety of cases and the only way to find out why you have an unexpected behaviour is to refer to Vlad Mihalcea blog posts(or StackOverFlow).
As an example, in one of the companies I worked for we had a
problem
when EntityGraph didn't work for nested collections.
There is no documentation that covers a reason and searching
through the github issues gave us a ticket
with exactly the same problem that has been solved in a
newest
Hibernate release, because project was using Spring Boot, we
had to wait
for Spring Boot team to release a new major version that
bumps
Hibernate version in a parent bom.
But these kind of problems are somewhat resolvable, you
google ,
debug Hibernate's source code(trust me, not a pleasant
experience) or see release notes to see if your problem has
been solved, so documentation is not a biggest pitfall.
Developers
One thing I noticed everytime I start working on a new project. Java developers don't know how to organize an entity layer. As an example, let's say we are building an e-commerce website, of course we have a user,orders,items,comment tables. Order has a foreign key to user and item , and so does a comment table. We can go to Hibernate's quick start page and see how to map these relationships to Entity class.
Straight forward right? Yes it is , but the problem here is
what are responsibilities of this class
(I
in SOLID).To put it another way, in which case Am I allowed
to change this
class ?
Problem 1 - Smart Entities
Let's say we are using Spring Security to add a security
layer for http based endpoints.Spring requires you to
provide an implementation of UserDetails
interface that has methods to get user's name,password and
roles. A User Entity class already has these parameters
right? So why not just use it ?class UserEntity
implements
UserDetails
. If you do so then the whole
security layer will be mixed up with business logic. You
will be able to access user's orders right from
ServletListener.Whenever
you change User Entity apart from checking a business logic
you will have to check that it won't break a security layer
too . Does it follow a single responsibility
principle ? I doubt it does. So what is a solution ?
Hibernate doesn't mention it explicitly but you don't have to have a single Entity that represents a database table. What if we keep a User Entity above and introduce a new one
So now whenever Hibernate fires a select query it won't touch any dependencies apart from the fields listed in UserCredentials class
Problem 2 - DTO layer
I think everybody knows what the fetch modes in Hibernate
are.
In essence it's a way to let Hibernate know when you need to
use joins to fetch class dependencies or just fetch
attributes of a single table. I bet you saw LazyInitException
before which happens
when you try to access a nested object or collection out of
a transaction which
was loaded lazily. Let's have another
use case with the same User Entity.
Imagine when users open their profiles, they only see their
names and some meta information such as birth , city and so
on. Spring
has a pattern called DTO which implies that you can't expose
an Entity layer into a controller layer or put it another
way,
the controller has to return a DTO, not a business Entity.
Imagine you have a layer that transforms an entity into a
dto(if you don't want to write this layer manually then
check out a MapStruct
project that does it for you)
And here is the UserDTO
So how EntityToDto
is supposed to know whether
UserEntity
was loaded with or without
corresponding orders? One solution I see often is to inject
an EntityManager
right
intoEntityToDto
and check every single
dependency
(If you don't care about exposing a JPA implementation , you
can use static method from Hibernate
Hibernate.isInitialized(entity);
but new
problem arises, it's hard to test a class that uses static
methods).
It's just horrible, now Dto layer has an EntityManager, a manager that can easily fire sql queries right from DtoLayer. What is more , Dto layer is becoming too smart, it can work with a case when the user has or doesn't have loaded orders(Interface segregation principle). What is a solution? The one I came up with is to introduce a third UserEntity class that doesn't have a dependency on orders and credentials.
It doesn't have any information about credentials because
they are only known by the security layer and it doesn't
have
orders. So now UserProfileEntity is only responsible for
a user page and we can remove entity manager from Dto
.Moreover, now we can modify
UserEntity
class by declaring all dependencies
as eager.We know for sure that users of this class will work
with orders, so there is no need to declare orders as lazy.
I see your point, but we would end up having too many classes, is it any better than having a single class per table?
Ok, let me convince you with one more example, the problem is not only in DtoLayer but in the repository layer too. Repository layer encapsulates the logic required to access data sources. Having single user entity means we need multiple methods to fetch the same user with/without orders
In the example above we are using Spring Data to generate two methods, using EntityGraph we are telling Hibernate when to load a lazy collection. I think the repository layer shouldn't be mixed up with this logic, it should only be responsible for fetching data without knowing how to do it. Introducing a new entity class that doesn't contain any refereces to other tables precisely solves this problem.
Problem 3 - Spring Data is bloated
I assume nowadays most people are working with Hibernate through Spring Data abstraction, it's great and allows you to get rid of a boilerplate, however, I advise you not to leak Spring Data Repositories into a Service layer.
Here is one example of Spring Repository,
implementation will be created in runtime by spring and
using specific naming convention it understands what kind of
query you are expecting(in example above it's a select of a
single user by id). But because you extend CrudRepository
your interface gets additional methods such as fetching and
deleting
all users. According to Martin Fowler
,Service layer defines a
set of available operations and
coordinates the application's response in each
operation. Having the repository above in UserService
we imply that deleting all users from the database is a part
of
a service operation but it's not. There is a great
article
that proposes an idea of having Spring layers encapsulated
by Domain Objects . But this approach is hard to implement
in
existing applications. What I propose is something similar,
create a sub package where you store Spring data
repositories(ideally make it package friendly so only
classes from the same package can have an access ) and then
introduce
a new repository package that contains your domain specific
datasource interaction.
In the example above we have a repository class which only has 1 method, then instead of injecting Spring Data Repo ,the service layer will use this Repository. Cons ? A new repository clearly defines a datasource operation that can be used by the service layer.
Problem 4 - Transaction management is tricky
I noticed that many people don't really understand how
transaction management works.Having
@Transactional
on top of your method will make
a deal right? Let's make a small test, do you see a problem
in the code below ? We are trying to update a user with data
from html form.
If you don't then read on. All entities managed by Hibernate
have a state, if your entity is inside of a database
transaction it has a managed state, as soon as transactional
method is about to exit, hibernate checks if any fields were
modified , if they were, then Hibernate fires an update
query , put it another way , you don't have to call save
, calling a setter is enough.That is why, even though
birthday was invalid and method save wasn't called, the
user's name
will be updated anyway, if it's a part of a business
requirements then that's totally fine, otherwise good luck
debugging the cause .Some other things to
consider, If you have a transactional method ,same as above,
how to reuse it using a different propagation level(let's
say execute a method in a new transaction even though
method was called from another transactional). I know some
workarounds but don't want to describe them , cause it's
not the best choice. Let me know if you are familiar with a
stable solution
Problem 5 - How Delete Works
Let's take a look at UserEntity for the last time.What if we
want to delete a user by id who has a list of orders, let's
say 5 orders. How many sql queries will be executed ? The
answer is 6, 5 to delete every individual order , and then
one to
delete a user. Couldn't Hibernate just run 2 queries instead
?
DELETE order where user_id=?; DELETE user where
id=?;
. Unfortunately it can't because one of the
orders can be a part of a different transaction and as a
result stored in Hibernate context. If deleting users isn't
a common operation then leaving it as it is would be the
best choice. However, if it's a critical part of an
application then we have to rewrite it. One solution is to
introduce a native query in the Repository layer.
The problem here is that native queries are not tracked by
Hibernate listeners(@EntityListener
) which
means if you have some hooks that
have to be executed right after you delete an order(let's
say
save some statistics and then send an email to
managers) then they won't work.One solution I come up with
is to use Spring's integrated event bus library
In this case when order was deleted by Hibernate, the @EntityListener will be called automatically, otherwise you have to publish an event into the EventBus which will delegate a hook logic to EntityListener.
Conclusion
There is no way JVM based backends(most of them) will get rid of the Hibernate in the near future,so the only thing we can do as developers is to introduce a good level of abstraction and don't forget that SOLID principles could be applied to entity layer too.