When I first learned about Hibernate about 12 years ago (I guess just about when it was at its peak) I found it to be one of the coolest libraries ever created. Hibernate made it possible to use OOD in practice! Build complex domain model graphs that were not only data structures but also contained some actual domain logic. And then you could just persist all of that.
Unfortunately my views on Hibernate/JPA have changed since then. Where I once saw it as an enabler I now see it more as a constraint - a source of accidental complexity. So much so that I would not consider Hibernate (or any other ORM tool) as the first choice for persistence anymore.
I feel that nowadays it is popular to criticize Hibernate and JPA in general. Unfortunately it seems that very often these opinions are based on some superficial experience and emotions. I do believe that emotional aspect and overall popularity is important when evaluating some technology but it is also important to try to understand the underlying rational reasoning.
I do not consider myself a Hibernate expert but I hope my experience is good enough to represent the opinion of someone who has given it a good try. I have been part of a dozen or so projects using JPA/Hibernate. From small 4 dev 4 month ones to big enterprise systems built by many different teams.
As I said the main reason why I immediately liked Hibernate was its support for building complex object graphs. I used to believe that it is the OO way to always use references to other objects instead of holding only IDs of associations or limiting model traversability in general.
This all changed when thanks to Vaughn Vernon I finally understood what was the idea behind Aggregates in DDD. I realized that referencing objects outside of your aggregate by ID actually makes your design much better and helps to avoid this huge tangled mess where everything is somehow linked to everything. Also I understood that if you are relying on lazy-loading for something in your aggregate then you have probably modeled it incorrectly. Aggregate is all about keeping invariants - something that you don't seem to need if you can do lazy-loading.
So in short Hibernate's support for complex object graphs and lazy loading is not something we inevitably need for implementing rich domain models.
Hibernate supports persisting quite complex models. However, there are some cases where it is hard to find any good solution.
It gets hard to use Hibernate without making any sacrifices to the design as soon as we do not want to model our domain classes 1:1 based on our database schema. Once we start thinking how to make our domain entities smaller, introduce interfaces and multiple implementations for value objects, use unidirectional associations where it makes sense it will require a lot of effort to find the right blog post or stackoverflow answer. Yes, it is possible to use things like
PostLoad hooks or force Hibernate to use
AccessType.PROPERTY but these are all tricks that are only needed for the persistence framework and will not add any other value.
Violation of Single Responsibility Principle
The simplest way how this becomes obvious is when we start writing tests for the ORM managed classes.
Even though Hibernate takes care of all DB interaction we still need to verify that it understands our mapping the way we intended. If we have an entity
MyEntity which uses annotations for mapping information we will need to write a test like
MyEntityPersistenceSpec. However, we also need to test business logic so we will have something like
MyEntityDomainLogicSpec as well. Now we have 2 specs for testing 2 different aspects of one production class. This kind of split in tests is often good sign that we should split the production code as well. One way how to achieve that is by moving all state into
MyEntityState object as suggested by Vernon but this is again an example of particular tooling forcing design decisions on us.
Even if we go old-school and externalize mappings into XML we still have the problem that we cannot be fully sure if the way how we modify our entities does not somehow affect persistence. All the collections in
MyEntity are not under our full control. They are implementations provided by Hibernate. For example, we must be careful when we create new collections vs when we use
The reality is that often we as developers use tools that we don't fully understand. As long as we are willing to make compromises in our design - use bidirectional associations, avoid interfaces, keep model relatively simple - all is fine. There is a lot of good documentation available for JPA and Hibernate. However, if we want to build something more advanced it gets much harder to find help. There are simply too many variations for building domain models.
I still believe that Hibernate is an amazing technology. What better proof can there be when something is so widely used for more than a decade. It's just that when I used to believe that Hibernate is the enabler for building good domain models then now I believe it is more suitable when you are OK to keep your domain model relatively close to your DB schema.
I think quite many systems are built using anemic domain model where full power of OOD is not utilized. Depending on the essential complexity of given problem domain this can have little or significant effect on the overall cost of maintenance. So in that sense I don't believe that complex domain model is always a "must have" in which case using Hibernate might still be ok. At least when none of the reasons for not using Hibernate are applicable.
Used image from Ridley Scott's Alien