Mapping Java Entities for Persistence With Hibernate (Part 3)
Learn more about mapping Java entities for persistence using Hibernate.
Join the DZone community and get the full member experience.
Join For FreeIn part 2, we went over mapping collections such as a simple set of strings, as well as basic associations between entities. The Book
entity was enriched with many-to-one associations to Author
and Publisher
. In this part, we'll explore how to map subclasses (inheritance) to tables using the various available strategies.
Inheritance is a common aspect of object-oriented languages like Java, but has no support in standard relational databases or SQL. So, Hibernate (and JPA) offers several ways to bridge this in its ORM implementation. Going back to our sample domain model, let's say that the class Book
inherits from an abstract class Publication
, along with another subclass Magazine
:
public abstract class Publication {
protected Long id;
protected String title;
protected Publisher publisher;
protected Date publishingDate;
...
}
public class Book extends Publication {
protected int volumes;
protected Set<String> contentTags = new HashSet<>();
protected Author author;
...
}
public class Magazine extends Publication {
protected int issueNumber;
protected Schedule schedule;
...
}
public enum Schedule {
WEEKLY, MONTHLY, YEARLY
}
Using @MappedSuperclass
The first strategy to map this inheritance is to have two tables each for the subclasses Book
and Magazine
, where each table contains columns for attributes in the concrete class as well as those inherited from the parent class Publication
. A simple way to do this is to add @MappedSuperclass
to Publication
and map the subclasses using @Entity
just as before:
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public abstract class Publication {
@Id
@GeneratedValue(generator = "idGenerator")
... // declare idGenerator here or in a shared package-info.java
protected Long id;
@Column(nullable = false)
protected String title;
@ManyToOne
@JoinColumn(name = "PUBLISHER_ID", nullable = false)
protected Publisher publisher;
@Temporal(TemporalType.DATE)
protected Date publishingDate;
...
}
@Entity
public class Book extends Publication {
protected int volumes;
@ElementCollection
@CollectionTable(name = "CONTENT_TAG",
joinColumns = @JoinColumn(name = "BOOK_ID"))
@Column(name = "TAG", nullable = false)
protected Set<String> contentTags = new HashSet<>();
@ManyToOne
@JoinColumn(name = "AUTHOR_ID", nullable = false)
protected Author author;
...
}
@Entity
public class Magazine extends Publication {
protected int issueNumber;
@Enumerated(EnumType.STRING) // store the enum value as string
protected Schedule schedule;
...
}
This results in two tables (ignoring those for Author
and Publisher
):
create table Book (
id bigint not null,
publishingDate date,
title varchar(255) not null,
volumes integer not null,
PUBLISHER_ID bigint,
AUTHOR_ID bigint not null,
primary key (id)
)
create table Magazine (
id bigint not null,
publishingDate date,
title varchar(255) not null,
issueNumber integer not null,
schedule varchar(255),
PUBLISHER_ID bigint not null,
primary key (id)
)
The main problem with this mapping strategy is that it doesn't easily support polymorphism in Java code. For example, we cannot execute a query on the superclass Publication
so that it covers both tables of its subclasses. If the application does not really need such queries, and will explicitly query the specific subclasses, then this approach may be suitable.
Table Per Concrete Class Mapping Using Union for Polymorphic Queries
If we instead map the parent class as an entity and specify InheritanceType.TABLE_PER_CLASS
, then we'd end up with the same schema as before, but Hibernate would use UNION
to run polymorphic queries:
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Publication {
@Id
@GeneratedValue(generator = "idGenerator")
... // declare idGenerator here or in a shared package-info.java
protected Long id;
@Column(nullable = false)
protected String title;
@ManyToOne
@JoinColumn(name = "PUBLISHER_ID", nullable = false)
protected Publisher publisher;
@Temporal(TemporalType.DATE)
protected Date publishingDate;
...
}
Again, we have only two tables for the concrete classes Book
and Magazine
, as shown below. Note that if the parent class was not abstract, we would have a third table (remember that this a table-per-concrete-class strategy).
create table Book (
id bigint not null,
publishingDate date,
title varchar(255) not null,
PUBLISHER_ID bigint not null,
volumes integer not null,
AUTHOR_ID bigint not null,
primary key (id)
)
create table Magazine (
id bigint not null,
publishingDate date,
title varchar(255) not null,
PUBLISHER_ID bigint not null,
issueNumber integer not null,
schedule varchar(255),
primary key (id)
)
To demonstrate how a polymorphic query is executed by Hibernate, let's query a publication by its title, knowing that it's a book:
Query<Publication> query = session.createQuery("from Publication where title = :title", Publication.class);
query.setParameter("title", "The Lord of the Rings");
Book book = (Book) query.getSingleResult();
assertEquals("The Lord of the Rings", book.getTitle());
assertEquals("1954-07-29", book.getPublishingDate());
assertEquals("J. R. R. Tolkien", book.getAuthor().getName());
assertEquals("Allen & Unwin", book.getPublisher().getName());
If we turn on SQL logging, we see that it used the following query:
select publicatio0_.id as id1_4_, publicatio0_.PUBLISHER_ID as PUBLISHE4_4_,
publicatio0_.publishingDate as publishi2_4_,
publicatio0_.title as title3_4_, publicatio0_.AUTHOR_ID as AUTHOR_I2_1_,
publicatio0_.volumes as volumes1_1_, publicatio0_.issueNumber as issueNum1_3_,
publicatio0_.schedule as schedule2_3_, publicatio0_.clazz_ as clazz_
from ( select id, publishingDate, title, PUBLISHER_ID, volumes, AUTHOR_ID,
null as issueNumber, null as schedule, 1 as clazz_
from Book
union
all select id, publishingDate, title, PUBLISHER_ID, null as volumes,
null as AUTHOR_ID, issueNumber, schedule, 2 as clazz_
from Magazine ) publicatio0_
where publicatio0_.title=?
Single Table for Class Hierarchy
Another way to map an inheritance hierarchy is to use only one table for all classes. This table would include columns for all attributes of all classes. An additional column, called the discriminator column, tells Hibernate which type each row corresponds to. The advantage is performance since no joins or unions are needed and polymorphism is therefore fast at the level of the DB. The major disadvantage is that subclasses cannot have fields that are nullable=false
, since the columns of these fields are also shared by other subclasses that do not have these fields. This leaves the responsibility of enforcing the data integrity to the programmer, using data validation logic in the business code.
import javax.persistence.DiscriminatorColumn;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "PUBLICATION_TYPE")
public abstract class Publication {
@Id
@GeneratedValue(generator = "idGenerator")
... // declare idGenerator here or in a shared package-info.java
protected Long id;
@Column(nullable = false)
protected String title;
@ManyToOne
@JoinColumn(name = "PUBLISHER_ID", nullable = false)
protected Publisher publisher;
@Temporal(TemporalType.DATE)
protected Date publishingDate;
...
}
@Entity
public class Book extends Publication {
@ManyToOne
@JoinColumn(name = "AUTHOR_ID") // must be nullable!
protected Author author;
...
}
@Entity
public class Magazine extends Publication {
...
}
The resulting schema is a single table for all classes in the hierarchy:
create table Publication (
PUBLICATION_TYPE varchar(31) not null,
id bigint not null,
publishingDate date,
title varchar(255) not null,
volumes integer,
issueNumber integer,
schedule varchar(255),
PUBLISHER_ID bigint not null,
AUTHOR_ID bigint,
primary key (id)
)
Queries against the parent type are straightforward — Hibernate queries the single table and uses the specified criteria as the WHERE
clause. If the application queries against a specific subclass, Hibernate further uses the discriminator column PUBLICATION_TYPE
to filter only the rows for that particular subclass. For example, the JPA query from Book where title = :title
would generate the following SQL:
select book0_.id as id2_2_, book0_.PUBLISHER_ID as PUBLISHE8_2_,
book0_.publishingDate as publishi3_2_, book0_.title as title4_2_,
book0_.AUTHOR_ID as AUTHOR_I9_2_, book0_.volumes as volumes5_2_
from Publication book0_
where
book0_.PUBLICATION_TYPE='Book'
and book0_.title=?
Joined Tables
The fourth mapping strategy is to map each subclass to a table containing columns mapped only to the properties declared in the subclass. The tables are joined via foreign key constraints to the table of their superclass, which contains columns for the inherited properties. Polymorphic queries would use JOIN
in the SQL statements using the foreign key columns.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Publication {
@Id
@GeneratedValue(generator = "idGenerator")
... // declare idGenerator here or in a shared package-info.java
protected Long id;
@Column(nullable = false)
protected String title;
@ManyToOne
@JoinColumn(name = "PUBLISHER_ID", nullable = false)
protected Publisher publisher;
@Temporal(TemporalType.DATE)
protected Date publishingDate;
...
}
@Entity
public class Book extends Publication {
protected int volumes;
@ElementCollection
@CollectionTable(name = "CONTENT_TAG",
joinColumns = @JoinColumn(name = "BOOK_ID"))
@Column(name = "TAG", nullable = false)
protected Set<String> contentTags = new HashSet<>();
@ManyToOne
@JoinColumn(name = "AUTHOR_ID", nullable = false)
protected Author author;
...
}
@Entity
public class Magazine extends Publication {
protected int issueNumber;
@Enumerated(EnumType.STRING)
protected Schedule schedule;
...
}
The resulting schema is two tables for the subclasses Book
and Magazine
, joined with a table for the parent class Publication
:
create table Book (
volumes integer not null,
id bigint not null,
AUTHOR_ID bigint not null,
primary key (id)
)
create table Magazine (
issueNumber integer not null,
schedule varchar(255),
id bigint not null,
primary key (id)
)
create table Publication (
id bigint not null,
publishingDate date,
title varchar(255) not null,
PUBLISHER_ID bigint not null,
primary key (id)
)
alter table Book
add constraint FKh7kfm44rlyes4hoxg70sw2v7k
foreign key (id)
references Publication
alter table Magazine
add constraint FKg27disaxrv1fevl8118yt1ejr
foreign key (id)
references Publication
Queries would use JOIN
in order to retrieve all columns for an entity (both those for the subclass fields and those inherited). As an example, the query from Publication where title = :title
would result in the following SQL:
select publicatio0_.id as id1_4_, publicatio0_.PUBLISHER_ID as PUBLISHE4_4_,
publicatio0_.publishingDate as publishi2_4_, publicatio0_.title as title3_4_,
publicatio0_1_.AUTHOR_ID as AUTHOR_I3_1_, publicatio0_1_.volumes as volumes1_1_,
publicatio0_2_.issueNumber as issueNum1_3_, publicatio0_2_.schedule as schedule2_3_,
case
when publicatio0_1_.id is not null then 1
when publicatio0_2_.id is not null then 2
when publicatio0_.id is not null then 0
end as clazz_
from Publication publicatio0_
left outer join
Book publicatio0_1_
on publicatio0_.id=publicatio0_1_.id
left outer join
Magazine publicatio0_2_
on publicatio0_.id=publicatio0_2_.id
where publicatio0_.title=?
Hibernate used a case
clause to determine the correct class type based on the presence of the id column.
Stay tuned for part 4...
Further Reading
Published at DZone with permission of Mahmoud Anouti, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments