Enterprise Java Development@TOPIC@
Built on: 2014-03-07 00:07 EST
Copyright © 2014 jim stafford (jcstaff@apl.jhu.edu)
Abstract
This presentation provides information for mapping Java entity class inheritance to the database using JPA.
At the completion of this topic, the student shall
have an understanding of:
Inheritance strategies
Single Table
Table per Concrete Class
Joined
Mapped Superclass for non-entity parent classes
be able to:
Be able to map a Java inheritance relationship using a Single Table strategy
Be able to map a Java inheritance relationship using a Table per Concrete Class strategy
Be able to map a Java inheritance relationship using a Table per Subclass (Join) strategy
Advantages
Simplest to implement
Single table to administer
Performs better than other inheritance strategies
No complex joins
Disadvantages
Unused fields when sub-types have unique properties
Sub-type columns must be nullable
Harder to enforce constraints within database
SQL "check" constraint can help
check(TYPE != 'BREAD_TYPE' or (BAKEDON is not null and SLICES is not null)) check(TYPE != 'Soup' or (SOUPTYPE is not null and EXPIRATION is not null)
Not normalized
More suitable for hierarchies with sub-types that...
Differ primarily in behavior only
Do not have unique data requirements
Figure 1.2. Single Table Example Database Schema
create table ORMINH_PRODUCT (
PTYPE varchar(32) not null,
id bigint generated by default as identity,
cost double not null,
expiration date,
SOUPTYPE varchar(16),
bakedOn date,
slices integer,
primary key (id)
)Single table
No joins
Unused columns
Figure 1.3. Single Table Example Java Mapping (Parent Class)
@Entity @Table(name="ORMINH_PRODUCT")
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="PTYPE", //column in root table indicating type
discriminatorType=DiscriminatorType.STRING,//data type of column
length=32) //length of discriminator string
public abstract class Product {
@Id @GeneratedValue
private long id;
private double cost;
...
@Transient
public abstract String getName();
Parent defines default mapping for all derived types
Figure 1.4. Single Table Example Java Mapping (Annotated Derived)
@Entity
@DiscriminatorValue("BREAD_TYPE") //value placed in root table to indicate type
public class Bread extends Product {
private int slices;
@Temporal(TemporalType.DATE)
private Date bakedOn;
...
@Transient
public String getName() { return "Bread"; }
Supplies type-specific column value
Figure 1.5. Single Table Example Java Mapping (Default Derived)
@Entity
public class Soup extends Product {
public enum SoupType {
UNKNOWN("Unknown"),
CHICKEN_NOODLE("Chicken Noodle"),
NEW_ENGLAND_CLAM_CHOWDER("New England Clam Chowder"),
TOMATO("Tomato");
private String text;
private SoupType(String text) { this.text = text; }
public String text() { return text; }
};
@Enumerated(EnumType.STRING)
@Column(name="SOUPTYPE", length=16)
private SoupType type = SoupType.UNKNOWN;
@Temporal(TemporalType.DATE)
private Date expiration;
...
@Transient
public String getName() { return type.text() + "Soup"; }
Accepts default type-specific column value
Figure 1.6. Single Table Example Usage (Persist)
ejava.examples.orm.inheritance.annotated.Soup soup = new Soup();
soup.setCost(2.12);
final long lifetime = 365L*24*60*60*1000;
soup.setExpiration(new Date(System.currentTimeMillis() + lifetime));
soup.setSoupType(Soup.SoupType.CHICKEN_NOODLE);
em.persist(soup);
ejava.examples.orm.inheritance.annotated.Bread bread = new Bread();
bread.setBakedOn(new Date());
bread.setCost(2.25);
bread.setSlices(24);
em.persist(bread);
Hibernate:
insert into ORMINH_PRODUCT (id, cost, expiration, SOUPTYPE, PTYPE)
values (null, ?, ?, ?, 'Soup')
Hibernate:
insert into ORMINH_PRODUCT(id, cost, bakedOn, slices, PTYPE)
values (null, ?, ?, ?, 'BREAD_TYPE')Two rows inserted into single base table with discriminator type supplied
Figure 1.7. Single Table Example Usage (Get Entities)
List<Product> products = em.createQuery(
"select p from Product p", Product.class)
.getResultList();
assertTrue("unexpected number of products:" + products.size(),
products.size() == 2);
for(Product p: products) {
log.info("product found:" + p);
}
Hibernate:
select
product0_.id as id2_9_,
product0_.cost as cost3_9_,
product0_.expiration as expirati4_9_,
product0_.SOUPTYPE as SOUPTYPE5_9_,
product0_.bakedOn as bakedOn6_9_,
product0_.slices as slices7_9_,
product0_.PTYPE as PTYPE1_9_
from
ORMINH_PRODUCT product0_
-product found:Soup, id=1, cost=2.12, type=CHICKEN_NOODLE, expiration=2014-10-05
-product found:Bread, id=2, cost=2.25, slices=24, baked=2013-10-05Single table queried for objects
Figure 1.8. Single Table Example Usage (Verify DB Schema)
//query specific tables for columns
int rows = em.createNativeQuery(
"select ID, TYPE, COST, SOUPTYPE, EXPIRATION, BAKEDON, SLICES " +
" from ORMINH_PRODUCT")
.getResultList().size();
assertEquals("unexpected number of product rows:" + rows, 2, rows);
select * from ORMINH_PRODUCT PTYPE ID COST BAKEDON SLICES EXPIRATION SOUPTYPE ---------- -- ---- ---------- ------ ---------- -------------- Soup 1 2.12 (null) (null) 2007-10-08 CHICKEN_NOODLE BREAD_TYPE 2 2.25 2006-10-08 24 (null) (null)
Single table "sparsely" filled based on type
@DiscriminatorColumn defines column to hold type-specific value
name (default="DTYPE")Column name
discriminatorTypeSTRING (defaults to Entity.name)Only portable technique when accepting default
CHARVendor-specific value when accepting default
INTEGERVendor-specific value when accepting default
columnDefinitionDatabase-specific definition for when generating schema
lengthSize of STRING
Advantages
May have constrained columns
No joins when accessing a single concrete type
Disadvantages
Not normalized
Redundant columns in each concrete child table
More work required to query across tables
Requires use of SQL "UNION"
Least desirable from a performance/portability standpoint
More suitable for ...
Sub-types not needed to be manipulated with sibling other sub-types
Figure 2.2. Table per Concrete Class Example Database Schema
create table ORMINH_CHECKING (
id bigint not null,
balance double not null,
fee double not null,
primary key (id)
)
create table ORMINH_INTERESTACCT (
id bigint not null,
balance double not null,
rate double not null,
primary key (id)
)
create sequence ORMINH_SEQTable for each concrete class
No separate table for parent class
Parent columns repeated in concrete sub-class tables
* his particular example uses SEQUENCE for primary key generation
Figure 2.3. Table per Concrete Class Example Java Mapping (Parent Class)
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
@SequenceGenerator(
name="orminhSeq", //required logical name
sequenceName="ORMINH_SEQ" //name in database
)
public abstract class Account {
@Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="orminhSeq")
private long id;
private double balance;
...
public void deposit(double amount) throws AccountException {
setBalance(getBalance() + amount);
}
public abstract void withdraw(double amount) throws AccountException;
...
Parent defines default mapping for all derived types, including PK generation
Using a common SEQUENCE allows sub-classes to have unique PKs across all tables
Figure 2.4. Table per Concrete Class Example Java Mapping (Subclasses)
@Entity
@Table(name="ORMINH_CHECKING")
public class CheckingAccount extends Account {
private double fee;
public void withdraw(double amount) throws AccountException {
super.setBalance(super.getBalance() - fee);
}
@Entity
@Table(name="ORMINH_INTERESTACCT")
public class InterestAccount extends Account {
private double rate;
public void withdraw(double amount) throws AccountException {
super.setBalance(super.getBalance() - amount);
}
Subclasses name their specific entity class table
Figure 2.5. Table per Concrete Class Example Usage (Persist)
ejava.examples.orm.inheritance.annotated.CheckingAccount checking = new CheckingAccount();
checking.setFee(0.50);
em.persist(checking);
ejava.examples.orm.inheritance.annotated.InterestAccount savings = new InterestAccount();
savings.setRate(0.25);
em.persist(savings);
Hibernate:
call next value for ORMINH_SEQ
Hibernate:
insert into ORMINH_CHECKING (balance, fee, id)
values (?, ?, ?)
Hibernate:
insert into ORMINH_INTERESTACCT (balance, rate, id)
values (?, ?, ?)Rows for entities placed into separate tables
Figure 2.6. Table per Concrete Class Example Usage (Get Entities)
List<Account> accounts =em.createQuery("select a from Account a").getResultList();
assertTrue("unexpected number of accounts:" + accounts.size(), accounts.size() == 2);
for(Account a: accounts) {
log.info("account found:" + a);
}
select
account0_.id as id1_0_,
account0_.balance as balance2_0_,
account0_.rate as rate1_7_,
account0_.fee as fee1_2_,
account0_.clazz_ as clazz_
from (
select id, balance, rate, null as fee, 1 as clazz_
from ORMINH_INTERESTACCT
union all
select id, balance, null as rate, fee, 2 as clazz_
from ORMINH_CHECKING
) account0_
-account found:InterestAccount, id=51, balance=0.0, rate=0.25
-account found:CheckingAccount, id=50, balance=0.0, fee=0.5
Query through parent type causes SQL "UNION ALL" of each concrete sub-class table
Figure 2.7. Table per Concrete Class Example Usage (Verify DB Schema)
//query specific tables for columns
int rows = em.createNativeQuery(
"select ID, BALANCE, FEE from ORMINH_CHECKING")
.getResultList().size();
assertEquals("unexpected number of checking rows:" + rows, 1, rows);
rows = em.createNativeQuery(
"select ID, BALANCE, RATE from ORMINH_INTERESTACCT")
.getResultList().size();
assertEquals("unexpected number of interestacct rows:" + rows, 1, rows);
select * from ORMINH_CHECKING ID BALANCE FEE -- ------- --- 50 0.0 0.5 select * from ORMINH_INTERESTACCT ID BALANCE RATE -- ------- ---- 51 0.0 0.25
Table per concrete class
No unused columns
Parent columns repeated in each sub-class table
Advantages
Can be normalized
Permits constraints to be defined
Disadvantages
Requires access to multiple tables when using (insert, select, update, and delete) an entity
More suitable for sub-classes that...
Have many unique properties
Require database constraints
Are queried for across sibling sub-types
Figure 3.2. Join Example Database Schema
create table ORMINH_PERSON (
id bigint generated by default as identity,
firstName varchar(255),
lastName varchar(255),
primary key (id)
)
create table ORMINH_EMPLOYEE (
hireDate date,
payrate double not null,
id bigint not null,
primary key (id)
)
alter table ORMINH_CUSTOMER
add constraint FK6D5464A42122B7AC
foreign key (id)
references ORMINH_PERSON
alter table ORMINH_EMPLOYEE
add constraint FK9055CB742122B7AC
foreign key (id)
references ORMINH_PERSONTable for each entity class is created
Each table has a primary key
Sub-class tables use a primary key join with parent class
Figure 3.3. Join Example Java Mapping (Parent Class)
@Entity @Table(name="ORMINH_PERSON")
@Inheritance(strategy=InheritanceType.JOINED)
public class Person {
@Id @GeneratedValue
private long id;
private String firstName;
private String lastName;
Parent defines default mapping for all derived types
Parent entity class table also defined
Figure 3.4. Join Example Java Mapping (Subclasses)
@Entity
@Table(name="ORMINH_CUSTOMER") //joined with Person table to form Customer
public class Customer extends Person {
public enum Rating { GOLD, SILVER, BRONZE }
@Enumerated(EnumType.STRING)
private Rating rating;
@Entity
@Table(name="ORMINH_EMPLOYEE") //joined with Person table to form Employee
public class Employee extends Person {
private double payrate;
@Temporal(TemporalType.DATE)
private Date hireDate;
Sub-classes define their entity-specific tables
Figure 3.5. Join Example Usage
ejava.examples.orm.inheritance.annotated.Employee employee = new Employee();
employee.setFirstName("john");
employee.setLastName("doe");
employee.setHireDate(new Date());
employee.setPayrate(10.00);
em.persist(employee);
ejava.examples.orm.inheritance.annotated.Customer customer = new Customer();
customer.setFirstName("jane");
customer.setLastName("johnson");
customer.setRating(Customer.Rating.SILVER);
em.persist(customer);
Hibernate:
insert into ORMINH_PERSON (id, firstName, lastName)
values (null, ?, ?)
Hibernate:
insert into ORMINH_EMPLOYEE (hireDate, payrate, id)
values (?, ?, ?)
Hibernate:
insert into ORMINH_PERSON (id, firstName, lastName)
values (null, ?, ?)
Hibernate:
insert into ORMINH_CUSTOMER (rating, id)
values (?, ?)Each persist() must insert into concrete class table and parent class table(s)
Figure 3.6. Join Example Usage (Get Entities)
List<Person> people = em.createQuery("select p from Person p", Person.class).getResultList();
assertTrue("unexpected number of people:" + people.size(),
people.size() == 2);
for(Person p: people) {
log.info("person found:" + p);
}
select
person0_.id as id1_8_,
person0_.firstName as firstNam2_8_,
person0_.lastName as lastName3_8_,
person0_1_.hireDate as hireDate1_6_,
person0_1_.payrate as payrate2_6_,
person0_2_.rating as rating1_5_,
case
when person0_1_.id is not null then 1
when person0_2_.id is not null then 2
when person0_.id is not null then 0
end as clazz_
from ORMINH_PERSON person0_
left outer join ORMINH_EMPLOYEE person0_1_
on person0_.id=person0_1_.id
left outer join ORMINH_CUSTOMER person0_2_
on person0_.id=person0_2_.id
-person found:Employee, id=1, firstName=john, lastName=doe, payrate=10.0
-person found:Customer, id=2, firstName=jane, lastName=johnson, rating=SILVERParent class table joined with each sub-class table during query of parent type
Figure 3.7. Join Example Usage (Verify DB Schema)
int rows = em.createNativeQuery(
"select ID, FIRSTNAME, LASTNAME from ORMINH_PERSON")
.getResultList().size();
assertEquals("unexpected number of person rows:" + rows, 2, rows);
rows = em.createNativeQuery(
"select ID, RATING from ORMINH_CUSTOMER")
.getResultList().size();
assertEquals("unexpected number of customer rows:" + rows, 1, rows);
rows = em.createNativeQuery(
"select ID, PAYRATE, HIREDATE from ORMINH_EMPLOYEE")
.getResultList().size();
assertEquals("unexpected number of employee rows:" + rows, 1, rows);
select * from ORMINH_PERSON ID FIRSTNAME LASTNAME -- --------- -------- 1 john doe 2 jane johnson select * from ORMINH_EMPLOYEE ID HIREDATE PAYRATE -- ---------- ------- 1 2006-10-08 10.0 select * from ORMINH_CUSTOMER ID RATING -- ------ 2 SILVER
Entities span multiple tables
Advantages
Allows inheritance of non-entity classes
Disadvantages
No base entity to form queries across hierarchy (unlike TABLE_PER_CLASS)
Tables not normalized (like TABLE_PER_CLASS)
Parent columns repeated in each subclass table
More suitable for ...
Independent subclasses
Figure 4.2. Non-Entity Example Database Schema
create table ORMINH_ALBUM (
ALBUM_ID bigint generated by default as identity,
ALBUM_VERSION bigint,
artist varchar(255),
title varchar(255),
primary key (ALBUM_ID)
)
create table ORMINH_TOOTHPASTE (
id bigint generated by default as identity,
version bigint not null,
size integer not null,
primary key (id)
)Non-entity base class properties appear in subclass tables
Figure 4.3. Non-Entity Example Java Mapping (Parent Class)
@MappedSuperclass
public abstract class BaseObject {
private long id;
@Access(AccessType.FIELD)
private long version;
@Transient
public long getId() { return id; }
protected void setId(long id) {
this.id = id;
}
Parent class is not a legal entity -- has no @Id
In this example, the implementation of BaseObject actually has an id attribute that the derived classes make use of. However, it is marked as @Transient in the base class and @Id in the derived Entity classes since MappedSuperClasses do not have primary keys. This specific example could have also used TABLE_PER_CLASS because of the availability of an id property in the base class.
Figure 4.4. Non-Entity Example Java Mapping (Override Defaults)
@Entity
@Table(name="ORMINH_ALBUM") //this table holds both this entity and parent class
@AttributeOverrides({
@AttributeOverride(name="version", column=@Column(name="ALBUM_VERSION"))
})
public class Album extends BaseObject {
@Access(AccessType.FIELD)
private String artist;
@Access(AccessType.FIELD)
private String title;
@Id @GeneratedValue //id is being generated independent of other siblings
@Column(name="ALBUM_ID")
public long getId() { return super.getId(); }
protected void setId(long id) {
super.setId(id);
}
"version" column name from parent being renamed
"id" property defined in this class given custom column name
Transient attribute in parent being reused to hold @Id for subclass using PROPERTY access
Figure 4.5. Non-Entity Example Java Mapping (Default Derived)
@Entity
@Table(name="ORMINH_TOOTHPASTE") //table holds this entity and parent class
public class ToothPaste extends BaseObject {
@Access(AccessType.FIELD)
private int size;
@Id @GeneratedValue //id is being generated independent of other siblings
public long getId() { return super.getId(); }
protected void setId(long id) {
super.setId(id);
}
Entity accepts mapping defaults
Figure 4.6. Non-Entity Example Usage
ejava.examples.orm.inheritance.annotated.Album album = new Album();
album.setArtist("Lynyrd Skynyrd");
album.setTitle("One More for the Road");
em.persist(album);
ejava.examples.orm.inheritance.annotated.ToothPaste toothpaste = new ToothPaste();
toothpaste.setSize(10);
em.persist(toothpaste);
Hibernate:
insert into ORMINH_ALBUM (ALBUM_ID, ALBUM_VERSION, artist, title)
values (null, ?, ?, ?)
Hibernate:
insert into ORMINH_TOOTHPASTE (id, version, size)
values (null, ?, ?)Rows are inserted into type-specific entity class tables
Figure 4.7. Non-Entity Example Usage (Get Entities)
List<BaseObject> objects = em.createQuery("select a from Album a").getResultList();
objects.addAll( em.createQuery("select tp from ToothPaste tp").getResultList());
assertTrue("unexpected number of objects:" + objects.size(), objects.size() == 2);
for(BaseObject o: objects) {
log.info("object found:" + o);
}
Hibernate:
select
album0_.ALBUM_ID as ALBUM1_1_,
album0_.ALBUM_VERSION as ALBUM2_1_,
album0_.artist as artist3_1_,
album0_.title as title4_1_
from ORMINH_ALBUM album0_
Hibernate:
select
toothpaste0_.id as id1_12_,
toothpaste0_.version as version2_12_,
toothpaste0_.size as size3_12_
from ORMINH_TOOTHPASTE toothpaste0_
-object found:ejava.examples.orm.inheritance.annotated.Album@3822f407, id=1, name=Lynyrd Skynyrd:One More for the Road
-object found:ejava.examples.orm.inheritance.annotated.ToothPaste@22a79bc, id=1, name=10oz toothpaste
Separate tables are accessed when obtaining each type
Figure 4.8. Non-Entity Example Usage (Verify DB Schema)
int rows = em.createNativeQuery(
"select ALBUM_ID, ALBUM_VERSION, ARTIST, TITLE " +
" from ORMINH_ALBUM")
.getResultList().size();
assertEquals("unexpected number of album rows:" + rows, 1, rows);
rows = em.createNativeQuery(
"select ID, VERSION, SIZE " +
" from ORMINH_TOOTHPASTE")
.getResultList().size();
assertEquals("unexpected number of toothpaste rows:" + rows, 1, rows);
select * from ORMINH_ALBUM ALBUM_ID ALBUM_VERSION ARTIST TITLE -------- ------------- -------------- --------------------- 1 0 Lynyrd Skynyrd One More for the Road select * from ORMINHTOOTHPASTE ID VERSION SIZE -- ------- ---- 1 0 10
Separate tables per concrete class (like TABLE_PER_CLASS)
No unused columns (like TABLE_PER_CLASS)
Figure 5.2. Generated Database Schema
create table ORMINH_SHAPE (
id bigint generated by default as identity,
version bigint not null,
posx integer not null,
posy integer not null,
primary key (id)
)
create table ORMINH_CIRCLE (
radius integer not null,
id bigint not null,
primary key (id)
)
create table ORMINH_RECTANGLE (
height integer not null,
width integer not null,
id bigint not null,
primary key (id)
)
alter table ORMINH_CIRCLE
add constraint FKFF2F1F1632C97600
foreign key (id)
references ORMINH_SHAPE
alter table ORMINH_RECTANGLE
add constraint FK1FFF614932C97600
foreign key (id)
references ORMINH_SHAPEcreate table ORMINH_CUBE (
depth integer not null,
id bigint not null,
primary key (id)
)
alter table ORMINH_CUBE
add constraint FK84203FB112391CE
foreign key (id)
references ORMINH_RECTANGLEProvider used parent's JOIN strategy over child's TABLE_PER_CLASS specification