Hibernate: Ditch or Double Down? When ORM Isn't Enough
Transcript:
Some developers treat Hibernate as heavens. Others blame it for nasty performance problems. Should you ditch Hibernate or double down? Let’s find out. We will explore where Hibernate shines, where plain JDBC or jOOQ are better, and how to take the best of both worlds in one project.
Hibernate is an object-relational mapping framework. In other words, it follows the ORM approach. This approach means that instead of manually writing SQL queries or mapping database rows to application fields, you just manipulate objects, and the ORM tool usually translates that into database operations. So, Hibernate acts as a translator between Java objects and database tables and relations. It handles the SQL and mapping for you.
Hibernate sits on top of the Java Database Connectivity API, or JDBC for short. It provides data access from the Java programming language, so Hibernate calls JDBC under the hood. But as Hibernate is an ORM tool, it implements the Java Persistence API, or JPA. JPA is a standard specification. Basically, it’s a set of interfaces and annotations that define how an ORM tool must work. Hibernate provides an implementation of JPA-defined APIs such as the EntityManagerFactory, EntityManager interfaces, mapping annotations, and so on.
Plus, it includes some additional features that are part of the native Hibernate API. These include, among others, the SessionFactory, which extends the EntityManagerFactory, and the Session, which extends the EntityManager. In addition, it provides extra mapping annotations that can be used with JPA APIs or the native API. Hibernate also supports multiple SQL dialects. These are classes that translate Hibernate Query Language into the native SQL dialect of the target database. Hibernate supports PostgreSQL, Oracle, MySQL dialects, and many others.
You write your domain models, and then you use Hibernate APIs, Hibernate Query Language, or JPQL to perform CRUD operations. Hibernate generates SQL, or you can write native queries, and it talks to the database using JDBC under the hood. It uses JDBC to open connections, execute the generated SQL, and fetch the results.
Hibernate can accelerate development and boost developer productivity. There is no need to bury yourself in writing absolutely all SQL queries. Most—like 95%—of Hibernate queries work just fine, and the others you can tune manually. Plus, there’s no need to waste time on manual mapping of standard relations such as one-to-many, many-to-many, and so on. It is a piece of cake for Hibernate. You can work with different relational databases with minimal changes.
Hibernate is also deeply integrated into major frameworks such as Spring, Quarkus, or Micronaut. Plus, complex tasks such as caching, auditing, and validating are already solved in Hibernate. For example, let’s take caching. Hibernate’s caching feature stores frequently accessed data in memory, reducing the number of database round trips. The application looks into the cache before hitting the database, which improves performance.
Hibernate provides two levels of caching: first-level cache and second-level cache. The first-level cache stores entities that are frequently used during a single Hibernate session and is enabled by default. The second-level cache stores data frequently used across different sessions. It reduces the number of database queries, but it must be enabled explicitly.
So, what are good use cases for Hibernate? For instance, when you implement domain-driven design with domain models that are based on standard relations such as many-to-one, one-to-many, many-to-many, and so on, or when you need to do specific operations such as locking, as Lukas Eder pointed out in his article comparing Hibernate to jOOQ. Also, your project may need to run on different databases, and you don’t want to litter your codebase with database-specific SQL. In exchange, you are ready to deal with issues related to badly implemented lazy loading, cache, equals/hashCode, and N+1 issues.
But there are some situations when you might want to say goodbye to Hibernate. The tool has several drawbacks. First of all, it is an abstraction and a so-called leaky abstraction. This means that it might lead to performance issues when everything was working fine, and then one day you hit an N+1 issue that has been sitting in your code because of some badly written logic.
The N+1 issue arises when you have an initial query that fetches a number of records from the database, and then another query is executed for each record to fetch data on related rows. For example, we have suppliers and products. We fetch all suppliers and then iterate over them to fetch all their products to retrieve the name of each product. This logic may result in a new query for each supplier. Each query has to be sent to the database, executed, and the results sent back to the application, which may take a lot of time and resources.
Another drawback of Hibernate comes from its power. Hibernate is a very complex tool with powerful features, but misuse of features such as cache or lazy loading may lead to performance issues. Also, writing equals, hashCode, and toString methods with Hibernate is not trivial. You need to ensure the consistency of equals and hashCode and make sure that your toString method doesn’t trigger lazy loading on all fields. In the description, links are attached to articles by Vlad Mihalcea and Thorben Janssen that explore how to better implement these methods with Hibernate.
So, what are the use cases that are better suited for a SQL-first approach? You may have complex read queries that implement window functions, complex joins, and database-specific features. Hibernate struggles with that. Your code might be more data-oriented than object-oriented, which is also a call for plain SQL. You may also have complex data models, for example referencing a part of a composite key, or cases where it is impossible to map JSON to an actual data structure in Hibernate.
On the screen, you can see an example of a query that Hibernate might choke on. Here we have a window function that looks at all rows in the result set, sorts them by the expression given in the ORDER BY clause, and assigns a rank number based on the order. We also have an anti-join, which only keeps rows that do not have a match in some other set.
In this case, we include suppliers but only count non-recalled products. As a result, we rank suppliers by revenue from products that weren’t recalled. Imagine we want to generate a report where we count supplier revenue, rank suppliers by that revenue, and calculate the average price for each supplier. The query shown is better written in plain SQL, and maybe you shouldn’t trust Hibernate to generate such a query.
However, the question of using ORM or SQL has a more surprising answer than you might think, because you can use both in one project. Hibernate and JPA were designed to work together with handwritten SQL, not to replace it completely. Even the creator of Hibernate states that you don’t have to use it everywhere. ORM and JDBC solve different problems, which means you can use both approaches in one codebase.
There’s a great book by Vlad Mihalcea that describes how to do that. Essentially, you can use Hibernate for CRUD operations and plain SQL such as JDBC or jOOQ for cases where ORM struggles. I hope you found this useful. Drop a comment on whether you are totally satisfied with Hibernate or would like to say goodbye to it. And as usual, don’t forget to like, subscribe, and until next time.





