Polymorphic association, supported by Hibernate, is the solution of such mapping requirement as an instance of entity needs to be associated with one of multiple possible entities. This can be implemented with Any relationship. In this relationship, the table holding the relationship would have two columns for relationship management. One to identify the primary key value of record being associated and another to identify the table to which the associated record belongs.
As the relationship is between one table to multiple other tables, foreign key relationship would not exist in database.One such real life example is sale scenario, where a vehicle seller sells truck and bus both. The requirement is to design sales system to manage the data of vehicles being sold. As bus and truck are completely different vehicles, a developer might design the system as mentioned in following class and ER diagram.
In this case, during each sale transaction, one vehicle will be sold. This vehicle can either be bus or truck. On the database level, SaleTransaction table would have two attributes vehicleType and vehicleId to manage the relationship. Vehicle type would represent whether corresponding vehicleId belongs to bus or truck.
As the relationship is between one table to multiple tables, foreign key constraint wouldn’t exist in the database layer.
Let us see the implementation of this scenario.
Class maintaining the relationship between SaleTransaction to Bus and Truck entity
SaleTransaction.java
package com.anytest; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import lombok.Data; import org.hibernate.annotations.Any; import org.hibernate.annotations.AnyMetaDef; import org.hibernate.annotations.Cascade; import org.hibernate.annotations.MetaValue; @Data @Entity public class SaleTransaction { @Id @GeneratedValue private Long Id; private Date saleDate; private Long salePrice; @Any (metaColumn = @Column(name = "vehicleType")) @AnyMetaDef(idType = "long", metaType = "string", metaValues = { @MetaValue(targetEntity = Bus.class, value = "bus"), @MetaValue(targetEntity = Truck.class, value = "truck") }) @Cascade( { org.hibernate.annotations.CascadeType.ALL}) @JoinColumn(name = "vehicleId") private Vehicle vehicle; public SaleTransaction() { } public SaleTransaction(final Date saleDate, final Vehicle vehicle,final Long salePrice) { this.saleDate = saleDate; this.vehicle = vehicle; this.salePrice=salePrice; } }
In this case, the developer has defined that SaleTransaction would be managing the relationship using vehicleType and vehicleId columns in database. If the transaction is of bus sale then vehicleType column in SaleTransaction table would be ‘bus’ and vehicleId would be the value of primary key of corresponding bus. In the same way, if the transaction is of Truck sale then the vehicleType column in SaleTransaction would be ‘truck’ and vehicleId would be the value of primary key of corresponding truck.
Query to create table structure in H2 database
create table Bus ( id bigint generated by default as identity, acAvailable boolean not null, buyingPrice bigint, engineCapacity bigint, modelNo varchar(255), seatCount integer not null, primary key (id) ); create table SaleTransaction ( Id bigint generated by default as identity, saleDate timestamp, salePrice bigint, vehicleType varchar(255), vehicleId bigint, primary key (Id) ); create table Truck ( id bigint generated by default as identity, buyingPrice bigint, engineCapacity bigint, modelNo varchar(255), trunkVolumeCapacity bigint, primary key (id) );
Vehicle Interface
package com.anytest; public interface Vehicle { Long getBuyingPrice(); String getModelNo(); Long getEngineCapacity(); }
Bus Class
package com.anytest; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import lombok.Data; @Data @Entity public class Bus implements Vehicle { @Id @GeneratedValue Long id; Long buyingPrice; String modelNo; Long engineCapacity; int seatCount; boolean acAvailable; public Bus(){} public Bus(final Long buyingPrice, final String modelNo, final Long engineCapacity, final int seatCount, final boolean acAvailable) { this.buyingPrice=buyingPrice; this.modelNo=modelNo; this.engineCapacity=engineCapacity; this.seatCount=seatCount; this.acAvailable=acAvailable; } }
Truck Class
package com.anytest; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import lombok.Data; @Data @Entity public class Truck implements Vehicle { @Id @GeneratedValue Long id; Long buyingPrice; String modelNo; Long engineCapacity; Long trunkVolumeCapacity; public Truck(){} public Truck(final Long buyingPrice, final String modelNo, final Long engineCapacity, final Long trunkVolumeCapacity) { this.buyingPrice=buyingPrice; this.modelNo=modelNo; this.engineCapacity=engineCapacity; this.trunkVolumeCapacity=trunkVolumeCapacity; } }
hibernate.cfg.xml – hibernate configuration file
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.driver_class">org.h2.Driver</property> <property name="hibernate.connection.url">jdbc:h2:~/anyMappingDemo</property> <property name="hibernate.connection.username">sa</property> <property name="hibernate.connection.password"></property> <property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property> <!-- JDBC connection pool (use the built-in) --> <!--<property name="hibernate.hbm2dll.auto">create-drop</property> --> <property name="connection.pool_size">1</property> <property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property> <mapping class="com.anytest.Bus"></mapping> <mapping class="com.anytest.Truck"></mapping> <mapping class="com.anytest.SaleTransaction"></mapping> </session-factory> </hibernate-configuration>
HibernateInterface – Class to deal with Hibernte interaction
package com.anytest; import java.util.List; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; @SuppressWarnings("deprecation") public class HibernateInterface { final static Configuration CONFIGURATION = new Configuration().configure(); final static SessionFactory SESSION_FACTORY = CONFIGURATION.buildSessionFactory(); static { Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { if(SESSION_FACTORY!=null && !SESSION_FACTORY.isClosed()) { SESSION_FACTORY.close(); } } })); } public static void saveEntities(final Object entity) { final Session session = SESSION_FACTORY.openSession(); session.beginTransaction(); try { session.saveOrUpdate(entity); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally{ session.close(); } } @SuppressWarnings("unchecked") public static List<Object> getRecordsOfType(final Class<?> classObject) { final Session session = SESSION_FACTORY.openSession(); try { List<Object> resultList = session.createCriteria(classObject).list(); resultList.toString(); return resultList; } finally{ session.close(); } } public static int exectueQuery(final String hqlQuery) { int effectedRecords=0; final Session session = SESSION_FACTORY.openSession(); session.beginTransaction(); try { effectedRecords=session.createQuery(hqlQuery).executeUpdate(); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally{ session.close(); } return effectedRecords; } }
PaymentActoinPerfomer – Class to perform Implicit Polymorphism test
package com.anytest; import java.util.Date; public class ActionPerformer { public static void main(String[] args) { final Bus bus = new Bus(600000L, "ABCDBusModel", 100L, 35, true); final Truck truck = new Truck(700000L, "ABCDTruckModel", 300L, 30000L); final SaleTransaction busSaleTransaction = new SaleTransaction(new Date(), bus,650000L); final SaleTransaction truckSaleTransaction = new SaleTransaction(new Date(), truck,780000L); HibernateInterface.exectueQuery("delete from com.anytest.SaleTransaction"); HibernateInterface.exectueQuery("delete from com.anytest.Vehicle"); HibernateInterface.saveEntities(busSaleTransaction); HibernateInterface.saveEntities(truckSaleTransaction); System.out.println("SaleTransaction records =" + HibernateInterface.getRecordsOfType(SaleTransaction.class)); } }
In this scenario, the developer has set cascade type as ALL. So while saving the transient entry of busSaleTransaction and truckSaleTransaction, hibernate at first stores the transient records of Bus into BUS table or Truck into TRUCK table and then corresponding transaction entry in SaleTransaction table.
This behaviour can be seen in the query fired hibernate. While saving bus transaction hibernate fires following query –
Hibernate: insert into Bus (id, acAvailable, buyingPrice, engineCapacity, modelNo, seatCount) values (null, ?, ?, ?, ?, ?) Hibernate: insert into SaleTransaction (Id, saleDate, salePrice, vehicleType, vehicleId) values (null, ?, ?, ?, ?)
While retrieving the records from SaleTransaction table, hibernate fires the query on SaleTransaction table. Then depending on the value of vehicleType hibernate fires the query in Bus or Truck table to get the associated records. In this Sales scenario hibernate fires following queries to get SaleTransaction records –
Hibernate: select this_.Id as Id1_1_0_, this_.saleDate as saleDate2_1_0_, this_.salePrice as salePric3_1_0_, this_.vehicleType as vehicleT4_1_0_, this_.vehicleId as vehicleI5_1_0_ from SaleTransaction this_ Hibernate: select bus0_.id as id1_0_0_, bus0_.acAvailable as acAvaila2_0_0_, bus0_.buyingPrice as buyingPr3_0_0_, bus0_.engineCapacity as engineCa4_0_0_, bus0_.modelNo as modelNo5_0_0_, bus0_.seatCount as seatCoun6_0_0_ from Bus bus0_ where bus0_.id=? Hibernate: select truck0_.id as id1_2_0_, truck0_.buyingPrice as buyingPr2_2_0_, truck0_.engineCapacity as engineCa3_2_0_, truck0_.modelNo as modelNo4_2_0_, truck0_.trunkVolumeCapacity as trunkVol5_2_0_ from Truck truck0_ where truck0_.id=?
The result of retrieving all sales transactions by firing query on SaleTransaction.class is as following –
SaleTransaction records =[SaleTransaction(Id=5, saleDate=2013-10-27 18:35:09.088, vehicle=Bus(id=3, buyingPrice=600000, modelNo=ABCDBusModel, engineCapacity=100, seatCount=35, acAvailable=true), salePrice=650000), SaleTransaction(Id=6, saleDate=2013-10-27 18:35:09.088, vehicle=Truck(id=3, buyingPrice=700000, modelNo=ABCDTruckModel, engineCapacity=300, trunkVolumeCapacity=30000), salePrice=780000)]
Limitation of Any Hibernate Mapping
Any relationship cannot be implemented as bidirectional association. It will be unidirectional relationship managed by entity holding the relationship data.
The second limitation of Any relationship is that one can not set foreign key relationship on database layer to manage integrity.
The working project can be accessed from here in github.
I hope this article helped you to understand polymorphic association implementation in hibernate in a better way. Do share your experience about the article and way you are planning to use polymorphic association in your application.
I will come with the different new topics in future for sure. If you have any questions, please write it in the comments section.