Search code examples
hibernateinheritancejpaeclipselink

How does the JPA handle partial, non-disjoint inheritance (using InheritanceType.JOINED in class-per-table scenario) along with EntityManager.find()?


I have a problem modeling/understanding partial, non-disjoint inheritance with JPA annotations.

Here are the four tables:

CREATE TABLE Persons (
  id INTEGER NOT NULL,
  first_name VARCHAR(50) NOT NULL,
  last_name VARCHAR(50) NOT NULL,
  PRIMARY KEY (id));

CREATE TABLE Referees (
  id INTEGER NOT NULL,
  license_nbr VARCHAR(10) DEFAULT NULL NULL,
  PRIMARY KEY (id),
  CONSTRAINT referees_persons_fk
    FOREIGN KEY (id)
    REFERENCES Persons (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE);

CREATE TABLE Coaches (
  id INTEGER NOT NULL,
  license_nbr VARCHAR(10) DEFAULT NULL NULL,
  PRIMARY KEY (id),
  CONSTRAINT coaches_persons_fk
    FOREIGN KEY (id)
    REFERENCES Persons (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE);

CREATE TABLE Players (
  id INTEGER NOT NULL,
  registration_nbr VARCHAR(20) DEFAULT NULL NULL,
  PRIMARY KEY (id),
  CONSTRAINT players_persons_fk
    FOREIGN KEY (id)
    REFERENCES Persons (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE);

My shortened entity classes are:

@Entity
@Table(name = "Persons")
@Inheritance(strategy = InheritanceType.JOINED)
public class Person implements Serializable
{
    @Id
    @Column(name = "id")
    private Integer id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    ...
}

@Entity
@Table(name = "Referees")
public class Referee extends Person implements Serializable
{
    @Column(name = "license_nbr")
    private String licenseNbr = null;

    ...
}

@Entity
@Table(name = "Coaches")
public class Coach extends Person implements Serializable
{
    @Column(name = "license_nbr")
    private String licenseNbr = null;

    ...
}

@Entity
@Table(name = "Players")
public class Player extends Person implements Serializable
{
    @Column(name = "registration_nbr")
    private String registrationNbr = null;

    ...
}

Partial inheritance: person entities can exist without any corresponding referee, coach, or player entities

Non-disjoint inheritance: there can be any number of corresponding sub entities, that is a person can be a referee, coach, and player at the same time, possibly any combination of the three. Each sub table can only have exactly none or one entity, but not multiple.

Note that because the inheritance is partial, Person isn't abstract. Furthermore, there's no discriminator on Person/s because the inheritance is non-disjoint.

Is such a model possible in Java ORMs? How would the annotations look like?

I have problems finding instances the way I annotated the classes using:

// Michael Jordan: the person who's only a player, too

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (-1): wrong Player instance
Person pe1 = em.find(Person.class, 1);
System.out.println("Loaded person = " + pe1);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (+1): correct Player instance
Player pl1 = em.find(Player.class, 1);
System.out.println("Loaded player = " + pl1);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (+1): correct null
Referee re1 = em.find(Referee.class, 1);
System.out.println("Loaded referee = " + re1);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (+1): correct null
Coach ch1 = em.find(Coach.class, 1);
System.out.println("Loaded coach = " + ch1);

System.out.println();
System.out.println();

// Dirk Nowitzki: the person who's also a player *and* a referee

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (-1): wrong Player instance
Person pe2 = em.find(Person.class, 2);
System.out.println("Loaded person = " + pe2);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (+1): correct Player instance
Player pl2 = em.find(Player.class, 2);
System.out.println("Loaded player = " + pl2);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (-1): wrong null
Referee re2 = em.find(Referee.class, 2);
System.out.println("Loaded referee = " + re2);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (+1): correct null
Coach ch2 = em.find(Coach.class, 2);
System.out.println("Loaded coach = " + ch2);

System.out.println();
System.out.println();

// Blake Griffin: the person who's also a player, referee, *and* coach

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (-1): wrong Player instance
Person pe3 = em.find(Person.class, 3);
System.out.println("Loaded person = " + pe3);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (+1): correct Player instance
Player pl3 = em.find(Player.class, 3);
System.out.println("Loaded player = " + pl3);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (-1): wrong null
Referee re3 = em.find(Referee.class, 3);
System.out.println("Loaded referee = " + re3);

// EclipseLink 2.1.1 ( 1): ?
// EclipseLink 2.2.0 ( 1): ?
// Hibernate 3.6     (-1): wrong null
Coach ch3 = em.find(Coach.class, 3);
System.out.println("Loaded coach = " + ch3);

EclipseLink 2.1.1 and 2.2.0 nightly build fail completely on EntityManager.find(...), while Hibernate at least returns some of the expected instances. Note the word "expected" here. It's my understanding of what a JPA ORM ought to do, but I might be mistaken.

As you can see, Hibernate always returns sub class instances of Player, but is unable to find Coach and Referee instances if a Player instance is also available.

So, how does JPA best handle this model?


Solution

  • Furthermore, there's no discriminator on Person/s because the inheritance is non-disjoint.

    To my knowledge, the inheritance with JPA is strict i.e. an entity belongs to a single hierarchy (a Person is either a Player or a Referee, not both, so there is only one corresponding record in the sub tables). In other words, I don't think non-disjoint inheritance is supported (nothing in the JPA specification says it is by the way) or even possible (more on this later).

    And whether you use a discriminator with a JOINED strategy or not doesn't mean that inheritance is non-disjoint or not, that's just used as an hint by some persistence provider.

    Actually, while the JPA specification suggests (in section 11.1.10 "DiscriminatorColumn Annotation") that one should be able to use a Discriminator with a JOINED strategy, Hibernate doesn't even support it, although this was possible using XML mappings, as reported in ANN-140. The JPA wiki book sum it up like this:

    Some JPA providers support joined inheritance with or without a discriminator column, some require the discriminator column, and some do not support the discriminator column. So joined inheritance does not seem to be fully standardized yet.

    If you look at the SQL generated by Hibernate you'll see that it performs an outer join mixed with a case to detect and instantiate the "right" type. For example, given a Project with a SmallProject and a LargeProject as subclasses, a from Project would give:

    select
      project0_.id as id66_,
      project0_.name as name66_,
      project0_1_.budget as budget67_,
      case 
       when project0_1_.id is not null then 1 
       when project0_2_.id is not null then 2 
       when project0_.id is not null then 0 
       else -1 
      end as clazz_ 
     from
      Project project0_ 
     left outer join
      LARGEPROJECT project0_1_ 
       on project0_.id=project0_1_.id 
     left outer join
      SMALLPROJECT project0_2_ 
       on project0_.id=project0_2_.id
    

    Obviously, the above would be problematic with non-disjoint inheritance i.e. rows in both child tables for a same ID.

    But regardless of how Hibernate implemented the JOINED strategy, I think non-disjoint inheritance is more generally just not possible given how the persistence context works, you can't have a Player instance and a Coach instance with the same id in the persistence context, you can't have multiple objects representing the same row of the database, this would break JPA's state management model.

    PS: I'm surprised the first test Person pe1 = em.find(Person.class, 1); failed with Hibernate. It definitely works with earlier versions. I wonder "what" Player instance you get.

    References

    • JPA 2.0 specification
      • Section 2.12 "Inheritance Mapping Strategies"
      • Section 11.1.10 "DiscriminatorColumn Annotation"
      • Section 11.1.20 "Inheritance Annotation"
    • JPA Wiki Book