воскресенье, 15 мая 2016 г.

Hibernate single-table hierarchy with abstract superclass and referencing it

Suppose you want a hierarchy of some entities that have common superclass, for example, a company's facilities. And there are several requirements:

1. The hierarchy is stored in single table
2. Hierarchy is made of different classes and we want to know their types when go through it.
3. Classes may or may not contain fields that are related to certain level of the hierarchy
4. Reference to the parent entity is stored in "parent_id" column regardless of current hierarchy level, class name, parent, etc.
5. The employee class has a reference to the AbstractFacility class, so "An Employee can work at facility of any level of hierarchy".
6. We can define the actual type of AbstractFacility the employee has reference to.

Let's take a look at the code:
@Entity
@Table(name = "facility")
@DiscriminatorColumn(name = "facility_type")
public abstract class AbstractFacility {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    protected String name;
}

@Entity
public class GlobalHQ extends AbstractFacility {
//The GlobalHQ is the highest in the hierarchy, has no parent
    @OneToMany(mappedBy = "globalHq", cascade = CascadeType.ALL)
    private List<LocalHQ> localHqs;
}

@Entity
public class LocalHQ extends AbstractFacility {
    @NotNull
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private GlobalHQ globalHq;

    @OneToMany(mappedBy = "localHq", cascade = CascadeType.ALL)
    private List<LocalShop> localShops;
}

@Entity
@SecondaryTable(name = "shop_secondary", pkJoinColumns = @PrimaryKeyJoinColumn(name = "shop_id"))
public class LocalShop extends AbstractFacility {
    @NotNull
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private LocalHQ localHq;

    //This field belongs only to LocalShop class
    @Column(table = "well_secondary")
    private String shopData;

}
As you can see, all facilities are descendants of a AbstractFacility, and GlobalHQ has many localHqs, each LocalHQ has many LocalShops.
LocalShop class has a field "shopData" that must not be contained in the main hierarchy table. The example contains only one such field, but in real case there can be a lots of them in any level of the hierarchy, and it's a bad idea to store them in the same common table, it will be a mess of columns. So, we use a secondary table to store additional fields there.

Let's take a look at the employee class, it's first, flawed version:
@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    private AbstractFacility workPlace;
}
At first glance this class seems to be ok, and it almost is: hibernate will map the workpalce field. The problem will occur when we try to determine the subclass of the workPlace: using the instanceof will cause the exception. That is because the hibernate does not know the exact subclass of the AbstractFacility, and debugger will show you that the class of the workplace field is "AbstractFacility", and it can not be cast to any of the subclasses. This can be quite a surprise, because you could expect the hibernate to define the subclass of AbstractFacility by the discriminator column, but it does not.

The solution is to use the hibernate's @Any annotation, which will force the Employee class to store the actual type of workplace field in a separate column:
@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Any(metaColumn = @Column(name = "workplace_type"))
    @AnyMetaDef(idType = "long", metaType = "string",
            metaValues = {
                    @MetaValue(targetEntity = GlobalHQ.class, value = "GlobalHQ"),
                    @MetaValue(targetEntity = LocalHQ.class, value = "LocalHQ"),
                    @MetaValue(targetEntity = LocalShop.class, value = "LocalShop")
            })
    @ManyToOne
    private AbstractFacility workPlace;
}
Using this annotation hibernate will map the actual subclass, so you can use the instanceof and class-casts without exceptions.

The drawback of this solution appears when you add a new class to your hierarchy - you must remember to add a metaValue to all classes that refer to AbstractFacility using @Any.