March 28, 2013

Hibernate strong versus weak relationship

The problem

When you want to map two entities together using a many-to-many connection, Hibernate needs you to provide a @ManyToMany JPA annotation. When I wanted to connect two entities I initially set up the connection in the following way:

@Table(name = "person")
public class Person extends AbstractDocumentEntity {

    @ManyToMany(mappedBy = "persons")
    private List<PersonGroup> groups;

And the corresponding group entity looks like this:

@Table(name = "person_group")
public class PersonGroup extends AbstractTrackedEntity {

    @ManyToMany
    @JoinTable(name = "person_person_group",
            joinColumns = { @JoinColumn(name = "group_id") },
            inverseJoinColumns = { @JoinColumn(name = "person_id") })
    private List<Person> persons;

This setup works perfectly when you try to read data from the database. Whenever you fetch the base entity, you can also read its connected groups or persons. So far, so good.

The problem lies however in the situation where you want to update the list of connected entities. In its current state, no changes are saved no matter if I add new entities to either of the collections.

Cascading changes

The first change that needed to be done, was to define that changes to the collection should be updated when the main enity gets persisted. One defines this by saying that the collection changes should be cascaded from the main entity. This is defined by adding a cascade property to the connection definition:

    @ManyToMany(mappedBy = "persons", cascade = CascadeType.ALL)
    @ManyToMany(cascade = CascadeType.ALL)

This changed the situation somewhat when I tried to update the collections. When I added entities to the groups collection from the person entity, nothing seemed to happen when I told Hibernate to persist the changes. When I added persons from the group entity, changes were saved. This shows that the cascade property is ignored when you are also using the mappedBy property.

Weak versus strong connection

The problem lies in how Hibernate defines this connection between these two entities. The group entity owns the connection in this case, as it defines what table and fields to use when looking up data. The person entity simply connects to this connection without defining what the connection entails. In this case this means that the connection from the group entity is the strong side, while the connection from the person side is the weak side. What this means in practice, is that no changes will be saved from the weak side, no matter if you define the connection to cascade its changes or not.

The solution to all of this is to define both sides as the strong side, by adding full definition on both ends, and drop the mappedBy property:

@Table(name = "person")
public class Person extends AbstractDocumentEntity {

    @OrderBy("title")
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "person_person_group",
              joinColumns = { @JoinColumn(name = "person_id") },
              inverseJoinColumns = { @JoinColumn(name = "group_id") })
    private List<PersonGroup> groups;

And the group entity ended up looking like this:

@Table(name = "person_group")
public class PersonGroup extends AbstractTrackedEntity {

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "person_person_group",
               joinColumns = { @JoinColumn(name = "group_id") },
               inverseJoinColumns = { @JoinColumn(name = "person_id") })
    private List<Person> persons;

As a bonus I also added a sort definition from the person entity to make the returned result look better in the application interface.

Final thoughts

The end result is a slightly more convoluted configuration. For me there is no logical reason why the original solution with a cascade property added should not work. I would rather that Hibernate read the cascade property, and from there decide what should be done with the collection, no matter if you use mappedBy or not.

No comments:

Post a Comment