Object Relational Mapping with Inheritance in Java


Published: 2014-11-30
Updated: 2016-06-09
Web: https://fritzthecat-blog.blogspot.com/2014/11/object-relational-mapping-with.html


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:

  1. Single-Table mapping
  2. Table-Per-Class (should have been named Table-Per-Concrete-Class)
  3. Joined (should have been named Table-Per-Class)
  4. Mapped Superclasses

All of these options result in different database structures.

For a good summary you could also read this java.dzone article.

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.

Here is the database structure (as generated by Hibernate):

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
The 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?

Here is a JPA tutorial, and here you can view the JPA specification.





ɔ⃝ Fritz Ritzberger, 2014-11-30