I think that a lot of developers tend to think that because they are using an object-relational mapping tool like Hibernate, it means that they no longer have to think about persistence; that magically Hibernate is going to take care of everything. I find the case to be quite the opposite. When using a tool like Hibernate, you need to arguably know more about your domain objects and the way they are fetched. Failure to do so could result in your database taking a beating. While Hibernate may volunteer a large amount of assistance to transparently hide the mismatch between objects and relational structures, if you ask it to do something dumb, it is going to do it for you. That includes the dreaded n+1 problem. Hibernate can take care of a lot of things, but it cannot write your application code.

This page summarizes some of my approaches to working with Hibernate and a number of techniques and facts I have learned along the way.

Defining Entites

The most important part of the persistence strategy (and likely the application as a whole) is the definition of the entities. Everything cascades from this core set of objects, so it is imperative to get them right. Below is my guide for setting up Hibernate/JPA entities.

  1. Create a new class, implementing the java.io.Serializable interface (also include the serialVersionUID).

  2. Add class variables @Id Serializable id and @Version Date lastUpdated. These variables are the primary key and the version number for optimistic locking, respectively.

  3. Add any properties you need.

  4. Write a default constructor. All variables of type Collection, List, Set, …​ should be initialized here.

  5. Generate getters and setters for all properties:

    • id and lastUpdated should have a public getter, and no setter. (jury is still out, perhaps a protected setter)

    • variables of type Collection, List, Set, …​ have a public getter and no setter. The public getter preferably return an unmodifiable list.

  6. Add JPA/Hibernate annotations. Add annotations to the getter methods. (should I just assign them to the variables instead?)

    • add Hibernate Validator annotations such as @NotNull, @Length, @Range, …​

    • add @Column(name=xxx), even when the same because you don’t want to forget what the original column name was that your data depends on

    • add @OneToOne, @OneToMany, @ManyToOne or @ManyToMany in the case of relations.

  7. When using relations, make sure that the relation is bidirectional. For the sake of simplicity, I create utility methods that handle this on 1 side of the relation: properties of type Collection have utility methodes addXXX and removeXXX. (perhaps the corresponding setter on the other side of the relation should be protected, and called from the utility methods.)

  8. Implement equals and hashcode methods. Soooo important!

    • Shortcircuit the comparison by considering the id

    • Only consider associations that are not proxies or are initialized proxies by using Hibernate.isInitialized()

    • Only consider fields that actually distinguish on object from the next…​don’t just use every field

  9. Build a toString() method using ToStringBuilder from jakarta commons for ease of debugging

  10. Add a mapping element to the Spring configuration file

get() vs load()

There is an important difference between the Session#get() and Session#load() operations that have a crucial impact on database usage. The get() operation will always fetch an instance from the database. On the other hand, the load() method always tries to return a proxy, only returning the instance if it is already managed in the current session. When using load(), the database is never accessed. Instead, the object’s properties (other than id) must be accessed in order to trigger initialization. The idea is to use load() when assigning one instance to another. You are telling Hibernate that you are not interested in the object’s data, but rather its identifier for purposes of establishing an association. An important consideration to keep in mind, though, is that load() will throw an ObjectNotFoundException if the corresponding row does not exist in the database.

Unit Testing Assertions

Use Session#flush() and Session#evict() in a test to write data to the database table and clear from first level (in memory) cache, respectively, so that the remainder of the test can verify that it made it into the database. The evict() call ensures that when retrieving the instance the next time, it won’t be found in first level cache and will be forced to consult the database.

Use Session#close() to end the current session and check the states of lazy associations, either forcing a LazyInitializationException, or verifying that one is not thrown when the data is expected to have been fetched.