Blog-Archiv

Sonntag, 30. November 2014

Object Relational Mapping with Inheritance in Java

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.

  • 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).
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.

  • When using one of the three InheritanceType mappings, watch out when querying any super-class, because you might receive more records than you expect!
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?

  • 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).
Here is a JPA tutorial, and here you can view the JPA specification.




Keine Kommentare: