Saturday, January 31, 2009

Inherited classes in Hibernate

Few days ago I made some refactoring of a Hibernate based JavaEE application. There was a table and a view on that table which included all columns, like this:
CREATE TABLE Person (
  Id NUMBER,
  Name VARCHAR(10),
  BirthDate DATE,
);

CREATE VIEW PersonExtended AS
  SELECT p.*, YearsFromNow(p.BirthDate) AS Age FROM Person p;
Assuming we have a corresponding function this view will include all columns from Person and have an additional column named Age. Before refactoring, there were 2 corresponding entity classes. In the actual code entities have full annotated getters and corresponding setters, but for readability I'll use the most compact and not recommended format here:
//Person.java

@Entity
class Person {
  @Id long id;
  String name;
  Date birthDate;
}

//PersonExtended.java

@Entity
class PersonExtended {
  @Id long id;
  String name;
  Date birthDate;
  int age;
}
Trying to remove the code duplication I changed the entities as following:
//Person.java

@Entity
@Inheritance (strategy=InheritanceType.TABLE_PER_CLASS)
class Person {
  @Id long id;
  String name;
  Date birthDate;
}

//PersonExtended.java

@Entity
class PersonExtended extends Person {
  int age;
}
Alone from the cosmetic improvement, this change also allowed to use the same code for working with both entities. But this polymorphism also created new problems. For example, query fetching all data from Person generated the following SQL:
SELECT p.*, NULL as Age, 1 as discriminator from Person p
  UNION
SELECT pe.*, 2 as discriminator from PersonExtended pe;
For clarity I replaced with * the actual field list from Hibernate generated SQL. So what happened? Hibernate treats PersonExtended as kind of Person, so the result of this query would be all records from Person followed by all records of PersonExtended! They will have correct type in Java, by the way, thanks to discriminator columns generated by Hibernate. Anyway, it's not what we wanted and it's a regression (a new bug) after the refactoring. To fix that bug we must tell Hibernate that PersonExtended is not considered Person. I used a MappedSuperclass for that:
//AbstractPersonBase.java

@MappedSuperclass
abstract class AbstractPersonBase {
  @Id long id;
  String name;
  Date birthDate;
}

//Person.java

@Entity
Person extends AbstractPersonBase {
  //empty, all person data is defined in the superclass
}

//PersonExtended.java

@Entity
PersonExtended extends AbstractPersonBase {
  int age;
}
This code correctly defines the relation between PersonExtended and Person. They have common part but should not be used one instead of the other. This solution with an abstract base has no problem with fetching different entities in the same query. On the other hand, it allows using AbstractPersonBase in cases where both entities are processed in the same way in Java.

No comments: