Hibernate Survival Diaries

Posted on February 28, 2023
Tags: java, hibernate

I’ve been developing with Hibernate since approximately 2005. In that time I’ve spoken with many other experienced developers and compared notes on how they use and don’t use Hibernate. This is a brief summary of some key learnings.

Many of the topics discussed here could be considered defects, and in a lot of cases there are tickets, but I haven’t compiled a list here. Some may have actually been fixed (please let me know), but as far as I know this is accurate as of Hibernate 5.4.33.

I’ll continue to update this article as things change.

Reflections

I believe that a lot of these issues come from the API design. This is because:

  1. Maintaining backwards compatibility locks the project into high-edgecase functionality
  2. JPA compatibility is #1 on steroids
  3. The approach of mapping database entities to object is an impedance mismatch
  4. Expressing entity update intent onto standard object manipulation results in
    1. Hard to express mutations (e.g., having to build an object graph that has parent -> child -> parent references)
    2. Unexpected behaviour (e.g., if you setList(…) on a collection you’ll remove the Hibernate managed list and nothing will happen, rather than removing the old entities being removed and the new entities added)

I’ve not found a good Java alternative (I’ve heard jOOQ is worth a look), but have fond memories of a short project using LINQ.

The best approach seems to be: keep everything as trivially simple as possible. This means, only using @OneToMany etc as a way of expressing relationships for JPQL, not for any kind of mutation.

Equals and HashCode

The proper implementation of the equals and hashCode are required for Hibernate to work correctly.

The reference we’ve followed is here.

Envers Troubleshooting

We use Envers to provide database history, which uses the JPA lifecycle hooks to create change entries in the *_aud tables. It does have some peculiar behaviour sometimes.

One example is, if you use an @ElementCollection to have a child entity, and this child entity is a primitive, then Envers will use the entity in the primary key. This is sometime OK, but redundant, except when the value is a too big to be in the key, such as a LONGTEXT or a BLOB.

The only way to overcome this currently is to wrap the element in an @Embeddable and add an @OrderColumn, then the revtype is used instead. e.g.:

@Embeddable
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApprovalCondition implements Serializable {
    @Column(name = "\"condition\"", length = 2000, nullable = false)
    private String condition;
}

public class Approval {
    ...
       
    @ElementCollection
    @OrderColumn
    private List<ApprovalCondition> conditions;
}

ManyToMany with Envers

If you have a many-to-many and you are trying to populate it, and you get an error like the following:

javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [...]

it may be that you are trying to put a reference (getOne) in the list, and not fetching the full object (findById). Ideally you’d be able to use the reference, but it seems to break Envers issue.

Correct use of orphanRemoval

If you naively just use orphanRemoval you’ll end up with a constraint violation when the parent is removed.

    @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
    @OrderBy("crew_index")
    private List<ProjectCrew> projectCrew = new ArrayList<>();

Uniqueness constraints will not work on project_id, crew_index because the add operations occur first, then the orphanRemoval kicks in, but in the mean time there will be duplicates.

This is because the child entity has its own lifecycle, which is not linked to its parent. To link it we need to tell the foreign relationship that it is owned by the other entity

    @ManyToOne(optional = false)
    @JoinColumn(updatable = false)
    private Project project;

In general, don’t cascade persists

Hibernate is quite buggy when it comes to managing anything but the simplest managed relationships. @OneToOne seems to work as expected, but @OneToMany has many issues, particularly when updating a list, where objects don’t get properly populated (causing NULL insert exceptions).

Another failure mode is returning duplicate entries in a list on the parent entity immediately following the save. On clean fetch the correct data is returned.

The only reliable way to manage these related entities it for them to have their own repositories and save/delete them explicitly via their own repository.

Handling deletion of child objects in bi-directional relations

When deleting a child entity using repository.delete(child), the delete operation will not be executed if the child is still referenced in the parent.

    private List<Child> children; // in Parent class

Add a callback method annotated with the JPA lifecycle hook @PreRemove to the child class eg.

    @PreRemove
    private void detachFromParent() {
        getParent().getChildren().remove(this);
        setParent(null);
    }