Published: 2014-11-30
Updated: 2014-12-06
ORM (object-relational mapping) has been around for quite a long time now. It started with the Sun JDO specification and Hibernate , and was merged with the work of various persistence providers to the JPA (Java Persistence API) specification, which is the valid standard now. Currently JPA is implemented by Hibernate, EclipseLink, OpenJPA, and Toplink.
The object-relational mapping is kind of an unsolvable problem , but JDO and JPA made the best of it. When you want to persist instances of classes that inherit from other classes you will get in touch with it.
The following is try-out code that shows how inheritance hierarchies can be saved and queried using different ORM-options, and what are the resulting database structures and query results.
Generally you have 4 mapping options when storing inheritance classes to a database:
All of these options result in different database structures.
An entity is a record from a database table. In Java it would be the instance of a class marked by an @Entity annotation.
For the pending ORM inheritance test I will use Event as base-class.
An event is for example a party, a concert, or a festival. Basically it defines a location and a start day and time.
1 | @Entity |
All annotations used here are imported from javax.persistence
. Do not use vendor-specific annotations unless you can not get around it.
Certainly there are different kinds of Event
. To have at least three levels of inheritance I derive a OneDayEvent
(party, concert) from Event
, and a MultiDayEvent
(festival) from OneDayEvent
.
1 | @Entity |
Mind that you don't need to define the primary key @Id
in these derived classes.
1 | @Entity |
These are my test entities, currently annotated for a Single-Table mapping.
An Event defines a location and a start day and time, the OneDayEvent adds an end time (on same day), and a MultiDayEvent adds an end day (at same end time).
Mind that the annotations used in this first source code here lead to the default single-table mapping, as they are simply annotated by @Entity.
I decided for Hibernate as JPA provider of my tests.
There you can can register your persistence classes programmatically, or by a configuration property hibernate.archive.autodetection
in the standardized JPA configuration file persistence.xml.
Programmatical (here overriding the setUp()
of an unit-test):
@Override
protected void setUp() throws Exception {
final AnnotationConfiguration configuration = new AnnotationConfiguration();
for (Class<?> persistenceClass : getPersistenceClasses())
configuration.addAnnotatedClass(persistenceClass);
configuration.configure();
this.sessionFactory = configuration.buildSessionFactory();
}
protected Class<?>[] getPersistenceClasses() {
return new Class<?> [] {
Event.class,
OneDayEvent.class,
MultiDayEvent.class,
};
}
Configurative in META-INF/persistence.xml:
<property name="hibernate.archive.autodetection" value="class" />
In an abstract super-class I provided methods to begin and commit a transaction, using Hibernate's SessionFactory
(see Hibernate docs how to do that).
The following source code will add one entity for each inheritance level (Event, OneDayEvent, MultiDayEvent). Then it will query each level.
1 | public void testOrmInheritance() { |
I will have to change that test code for @MappedSuperclass case, but for the three InheritanceType mappings it can be the same.
What does this output?
Event Event: location=Home, startDateTime=2014-11-14 22:26:14.412
Event OneDayEvent: location=Hilton, startDateTime=2014-11-14 22:26:14.453, endTime=12:05:00
Event MultiDayEvent: location=Paris, startDateTime=2014-11-14 22:26:14.459, endTime=13:10:00, endDay=2014-11-14 22:26:14.459
OneDayEvent OneDayEvent: location=Hilton, startDateTime=2014-11-14 22:26:14.453, endTime=12:05:00
OneDayEvent MultiDayEvent: location=Paris, startDateTime=2014-11-14 22:26:14.459, endTime=13:10:00, endDay=2014-11-14 22:26:14.459
MultiDayEvent MultiDayEvent: location=Paris, startDateTime=2014-11-14 22:26:14.459, endTime=13:10:00, endDay=2014-11-14 22:26:14.459
When querying Event (the base class), I get all three different events that I've inserted. As expected, when querying OneDayEvent I get two different events, and from MultiDayEvent I get just one result.
As expected? Wouldn't I have expected just one event for OneDayEvent, not two?
From a logical point of view, every multi-day event is also a single-day event, thus you get all single-day and all multi-day events when querying single-day events.
This was the first gotcha I had with mappings.
The
SELECT * FROM EVENT;
DTYPE ID LOCATION STARTDATETIME ENDTIME ENDDAY
-------------------------------------------------------
Event 1 Home 2014-11-14 22:26:14.412 null null
OneDayEvent 2 Hilton 2014-11-14 22:26:14.453 12:05:00 null
MultiDayEvent 3 Paris 2014-11-14 22:26:14.459 13:10:00 2014-11-14 22:26:14.459
DTYPE
column is the "discriminator" column, this denotes the concrete type of the table row.
With a Table-Per-Concrete-Class mapping the output is the same as with single-table mapping, because ORM-mapping does not influence query results. But the database structure is different, as there are several tables now. They contain column set duplications, this is not a normalized database.
SELECT * FROM EVENT;
ID LOCATION STARTDATETIME
-------------------------------------------------------
1 Home 2014-11-14 22:57:22.687
SELECT * FROM ONEDAYEVENT;
ID LOCATION STARTDATETIME ENDTIME
-------------------------------------------------------
2 Hilton 2014-11-14 22:57:22.789 12:05:00
SELECT * FROM MULTIDAYEVENT;
ID LOCATION STARTDATETIME ENDTIME ENDDAY
-------------------------------------------------------
3 Paris 2014-11-14 22:57:22.79 13:10:00 2014-11-14 22:57:22.791
Why are there three tables?
Because I did not declare Event to be an abstract
class.
Would I have done this, there would have been only two tables (no Event table).
Would I have done this, I couldn't have created and stored Java Event
objects.
Because I created three concrete classes, I got three database tables.
For this kind of mapping I had to apply following annotations:
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class Event
{
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class OneDayEvent extends Event
{
@Entity
public class MultiDayEvent extends OneDayEvent
{
This is the resulting database structure:
SELECT * FROM EVENT;
ID LOCATION STARTDATETIME
-------------------------------------------------------
1 Home 2014-11-14 23:17:42.408
2 Hilton 2014-11-14 23:17:42.454
3 Paris 2014-11-14 23:17:42.481
SELECT * FROM ONEDAYEVENT;
ENDTIME ID
-------------------------------------------------------
12:05:00 2
13:10:00 3
SELECT * FROM MULTIDAYEVENT;
ENDDAY ID
-------------------------------------------------------
2014-11-14 23:17:42.481 3
This is a clean normalized database structure with no column-set duplications. But of course any query for MultiDayEvent has to read three tables now!
Following are the annotations for this kind of mapping:
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
public class Event
{
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
public class OneDayEvent extends Event
{
@Entity
public class MultiDayEvent extends OneDayEvent
{
MappedSuperclass is not really a mapping but more a way to use class-inheritance without having to care for ORM mapping. Mind that any super-class that is NOT annotated with @MappedSuperclass will NOT be persisted!
Following are the annotations now:
@MappedSuperclass
public class Event
{
@MappedSuperclass
public class OneDayEvent extends Event
{
@Entity
public class MultiDayEvent extends OneDayEvent
{
For this to work I had to change the test application source code. It is not possible to save or query Event or OneDayEvent now, Hibernate throws exceptions when trying such. So just the last insert and query of MultiDayEvent remained in the code.
Following is the output:
MultiDayEvent MultiDayEvent: location=Paris, startDateTime=2014-11-14 23:44:46.032, endTime=13:10:00, endDay=2014-11-14 23:44:46.032
This is the resulting database structure:
SELECT * from MULTIDAYEVENT;
ID LOCATION STARTDATETIME ENDTIME ENDDAY
-------------------------------------------------------
1 Paris 2014-11-14 23:44:46.032 13:10:00 2014-11-14 23:44:46.032
In fact this is the same as a Table-Per-Concrete-Class mapping, the only difference being that the super-classes Eventand OneDayEvent are not abstract
.
It is also possible to create three tables with this kind of mapping when I use following annotations (simply adding @Entity):
@Entity
@MappedSuperclass
public class Event
{
@Entity
@MappedSuperclass
public class OneDayEvent extends Event
{
@Entity
public class MultiDayEvent extends OneDayEvent
{
SELECT * FROM EVENT;
ID LOCATION STARTDATETIME
-------------------------------------------------------
1 Home 2014-11-30 20:29:53.263
SELECT * FROM ONEDAYEVENT;
ID LOCATION STARTDATETIME ENDTIME
-------------------------------------------------------
1 Hilton 2014-11-30 20:29:53.33 12:05:00
SELECT * FROM MULTIDAYEVENT;
ID LOCATION STARTDATETIME ENDTIME ENDDAY
-------------------------------------------------------
1 Paris 2014-11-30 20:29:53.334 13:10:00 2014-11-30 20:29:53.334
As I said, this is very near to Table-Per-Concrete-Class, and the test output of this is exactly the same.
As soon as there are options, the question rises: which is the best?