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.
Mapping Options
Generally you have 4 mapping options when storing inheritance classes to a database:
- Single-Table mapping
- Table-Per-Class (should have been named Table-Per-Concrete-Class)
- Joined (should have been named Table-Per-Class)
- Mapped Superclasses
All of these options result in different database structures.
- Single-Table
maps all derived classes to just one database table.
The table's default name is that of the base class of the hierarchy.
Rows are marked as certain types using a dedicated database table column.
Disadvantage of this mapping is that you can not declare not-null properties in just one derived class (because all classes are in just one table).
- Table-Per-Concrete-Class
maps every concrete class (not a super-class)
to exactly one table. All super-class properties are duplicated in these tables.
No dedicated column is needed to mark types.
- Joined mapping
(the actual table-per-class) creates one table per class.
Complex and slow query joins can result from this.
But properties are not duplicated, this gives a clean normalized database design. Joined is the only mapping that provides such.
- Mapped Superclasses are a way to mix-in super-classes
for which no separate table will be generated. You can use this with all mappings.
These properties will be duplicated in database tables for any mapping (except for Single-Table of course).
Entities
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | @Entity public class Event { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String location; private Date startDateTime; public Long getId() { return id; } public Date getStartDateTime() { return startDateTime; } public void setStartDateTime(Date startDateTime) { this.startDateTime = startDateTime; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } @Override public String toString() { return getClass().getSimpleName()+":\tlocation="+location+",\tstartDateTime="+startDateTime; } } |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Entity public class OneDayEvent extends Event { private Time endTime; public Time getEndTime() { return endTime; } public void setEndTime(Time endTime) { this.endTime = endTime; } @Override public String toString() { return super.toString()+", endTime="+endTime; } } |
Mind that you don't need to define the primary key @Id
in these derived classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Entity public class MultiDayEvent extends OneDayEvent { private Date endDay; public Date getEndDay() { return endDay; } public void setEndDay(Date endDay) { this.endDay = endDay; } @Override public String toString() { return super.toString()+", endDay="+endDay; } } |
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.
Application
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public void testOrmInheritance() { // create events final Session writeSession = beginTransaction("Event insert"); final Event event = new Event(); event.setLocation("Home"); event.setStartDateTime(new Date()); writeSession.save(event); final OneDayEvent oneDayEvent = new OneDayEvent(); oneDayEvent.setLocation("Hilton"); oneDayEvent.setStartDateTime(new Date()); oneDayEvent.setEndTime(Time.valueOf("12:05:00")); writeSession.save(oneDayEvent); final MultiDayEvent multiDayEvent = new MultiDayEvent(); multiDayEvent.setLocation("Paris"); multiDayEvent.setStartDateTime(new Date()); multiDayEvent.setEndTime(Time.valueOf("13:10:00")); multiDayEvent.setEndDay(new Date()); writeSession.save(multiDayEvent); commitTransaction(writeSession, "Event insert"); // read events from database final Session readSession = beginTransaction("Event read"); final String [] entityNames = new String [] { Event.class.getSimpleName(), OneDayEvent.class.getSimpleName(), MultiDayEvent.class.getSimpleName(), }; for (String entityName : entityNames) { final List<Event> result = readSession.createQuery("select e from "+entityName+" e").list(); for (Event e : result) { System.out.println(entityName+"\t"+e); } } commitTransaction(readSession, "Event read"); } |
I will have to change that test code for @MappedSuperclass case, but for the three InheritanceType mappings it can be the same.
Single-Table
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.
- When using one of the three InheritanceType mappings, watch out when querying any super-class, because you might receive more records than you expect!
TheSELECT * 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.
Table-Per-Concrete-Class
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 {
Joined
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 {
Mapped Superclass
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 Event
and 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.
What To Take?
As soon as there are options, the question rises: which is the best?
- Single Table: good when having not so many sub-classes, or all of them are very similar, and not having required properties in just one sub-class,
- Table Per Concrete Class: good when having many sub-classes, or they are very different,
- Joined (Table Per Class): good when a normalized database is required, and not so many records are in tables (slow join performance).