Single-table strategy for mapping inheritance with JPA

By | August 10, 2016

1. Stock Market Example

Get started on our real market example with investors who want to buy or sell shares on the marketplace of which they are participants. Actually, stockbrokers (brokers) will do trading on behalf of their clients. But, there are different financial products: shares, loan, option, bond, hibrid, future, and so on. So the easiest way is to use inheritance to keep properties common to many products in a FinancialProduct object, and the Shares, Loan,…objects have properties unique to those product types.

Financial Product Inheritance
Figure 1: Financial Product Inheritance

2. Solution with single-table strategy for JPA

All classes in the domain model hierarchy are mapped to one table, which means that a single table will store all data from all the objects in the domain module. For the Financial Product schema, assume that all product types such as Shares, Option, Bond, Future, are mapped into FinancialProduct table.

single-table-strategy-financial-product
Figure 2: Store all Financial Products in a single table.

3. UML class diagram

UML-Financial-Product
Figure 3: UML class diagram for the financial product.

4. Implementation

4.1 JPA configuration

JPA maps each product type to the table by storing persistent data into relevant mapped column. In order identify different objects in domain model, a discriminator keyword used to contain a value unique to the object type in a given row.

src/main/java/mapping/inheritance/domain/FinancialProduct.java

package mapping.inheritance.domain;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

import org.dozer.Mapping;

@Entity
@Table(name="product")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="type", discriminatorType=DiscriminatorType.INTEGER)
public abstract class FinancialProduct{

	@Transient
    @Mapping("type")
    public FinancialProductType getFinancialProductType() {
        return FinancialProductType.get(this.getClass());
    }
	@Id
	@Column(name = "product_code")
	protected String productCode;
	
	@Column(name = "description")
    private String description;
	
	@Version
    protected long version;
	
	@OneToMany(mappedBy = "productCode", fetch = FetchType.EAGER,
			cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Risk> risks;

	public void setProductCode(String productCode) {
		this.productCode = productCode;
	}
	public void setDescription(String description) {
		this.description = description;
	}
	
	public void setVersion(long version) {
		this.version = version;
	}
	public Set<Risk> getRisks() {
		return risks;
	}
	public void setRisks(Set<Risk> risks) {
		this.risks = risks;
	}
	@Override
	public String toString() {
		return this.productCode + " " + this.getFinancialProductType() +
				" " + this.description + " " + this.version;
	}
	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;

		FinancialProduct financialProduct = (FinancialProduct) o;

		if (version != financialProduct.version)
			return false;
		if (!productCode.equals(financialProduct.productCode))
			return false;
		return true;
	}
	@Override
    public int hashCode() {
        int result = productCode.hashCode();
        result = 31 * result + (int) (version ^ (version >>> 32));
        return result;
    }
}

src/main/java/mapping/inheritance/domain/FinancialProductType.java

package mapping.inheritance.domain;
import static java.util.Arrays.asList;

public enum FinancialProductType {
	 	BONDS(Bonds.class),
	    SHARES(Shares.class),
	    OPTIONS(Options.class),
	    FUTURES(Futures.class);
	   
	    private final Class<? extends FinancialProduct> relatedClass;

	    FinancialProductType(Class<? extends FinancialProduct> relatedClass) {
	        this.relatedClass = relatedClass;
	    }

	    public Class<? extends FinancialProduct> getRelatedClass() {
	        return relatedClass;
	    }

	    public static FinancialProductType get(Class<? extends FinancialProduct> relatedClass) {
	        return asList(values()).stream()
	                .filter(type -> type.relatedClass.equals(relatedClass))
	                .findFirst().orElseThrow(() -> new IllegalArgumentException(
                                                "Invalid Financial Product class"));
	    }

	    public String getExternalName() {
	        return relatedClass.getSimpleName();
	    }
}

src/main/java/mapping/inheritance/domain/Shares.java

package mapping.inheritance.domain;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("1")
public class Shares extends FinancialProduct{}

src/main/java/mapping/inheritance/domain/Bonds.java

package mapping.inheritance.domain;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("2")
public class Bonds extends FinancialProduct{}

Do similar configuration for Futures, and Options.
src/main/java/mapping/inheritance/domain/Risk.java

package mapping.inheritance.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="risk")
public class Risk{
	@Column(name = "product_code")
	private String productCode;
	
	@Id
	@Column(name = "risk_code")
	private String riskCode;
	
	@Column(name = "risk_weight")
	private double riskWeight;
	
	public String getProductCode() {
		return productCode;
	}
	protected Risk(){};
	public Risk(String sharesCode, String riskCode, double riskWeight) {
		super();
		this.productCode = sharesCode;
		this.riskCode = riskCode;
		this.riskWeight = riskWeight;
	}

	public void setSharesCode(String sharesCode) {
		this.productCode = sharesCode;
	}
	@Override
	public String toString() {
		return "|_ _ _ " + this.riskCode + " " + this.productCode + " " + this.riskWeight;
	}
	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;

		Risk that = (Risk) o;

		if (riskCode != null ? !riskCode.equals(that.riskCode) : that.riskCode != null)
			return false;

		return true;
	}

	@Override
	public int hashCode() {
		return riskCode != null ? riskCode.hashCode() : 0;
	}
}

src/main/java/mapping/inheritance/repository/FinancialProductRepository.java

package mapping.inheritance.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import mapping.inheritance.domain.FinancialProduct;

public interface FinancialProductRepository extends JpaRepository<FinancialProduct, String>{
	@Query(value = "Select distinct s from Shares s where s.productCode = :productCode")
	FinancialProduct findSharesByCode(@Param(value ="productCode") String productCode);
	
}

4.2 Services configuration

src/main/java/mapping/inheritance/service/FinancialProductService.java

package mapping.inheritance.service;

import java.util.List;
import mapping.inheritance.domain.FinancialProduct;
import mapping.inheritance.mapper.FinancialProductDTO;

public interface FinancialProductService {
	FinancialProduct findSharesByCode(String sharesCode);
	void add(FinancialProductDTO sharesDTO);
	List<FinancialProduct> findAll();
}

src/main/java/mapping/inheritance/service/FinancialProductServiceImpl.java

package mapping.inheritance.service;

import java.util.List;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import mapping.inheritance.domain.FinancialProduct;
import mapping.inheritance.mapper.FinancialProductDTO;
import mapping.inheritance.mapper.FinancialProductMapper;
import mapping.inheritance.repository.FinancialProductRepository;

@Service ("financialProductService")
@Transactional
public class FinancialProductServiceImpl implements FinancialProductService{
	@Autowired
	private final FinancialProductRepository sharesRepository;
	@Autowired
	private FinancialProductMapper sharesMapper;
	@Autowired
	public FinancialProductServiceImpl(FinancialProductRepository sharesRepository,
                                                FinancialProductMapper sharesMapper) {
		this.sharesRepository = sharesRepository;
		this.sharesMapper = sharesMapper;
	}
	@Override
	public FinancialProduct findSharesByCode(String sharesCode) {
		return sharesRepository.findSharesByCode(sharesCode);
	}
	public void add(FinancialProductDTO sharesDTO) {
		if(!sharesRepository.exists(sharesDTO.getProductCode())){
			FinancialProduct share = sharesMapper.doMapper(sharesDTO);
			sharesRepository.save(share);
		}
	}
	@Override
	public List<FinancialProduct> findAll() {
		return sharesRepository.findAll();
	}
	
}

src/main/java/mapping/inheritance/mapper/FinancialProductMapper.java

package mapping.inheritance.mapper;

import org.springframework.stereotype.Component;
import mapping.inheritance.domain.FinancialProduct;
import mapping.inheritance.domain.FinancialProductType;

@Component
public class FinancialProductMapper {
	public FinancialProduct doMapper(FinancialProductDTO dto) {
		FinancialProduct shares = null;
		try {
			shares = buildShares(dto);
			if(shares != null) {
				shares.setProductCode(dto.getProductCode());
				shares.setDescription(dto.getDescription());
				shares.setVersion(Long.valueOf(dto.getVersion()));
				shares.setRisks(dto.getRisks());
			}
		} catch (InstantiationException | IllegalAccessException e) {
			e.printStackTrace();
		}
		return shares;	
	}

	/**
	 * Extract Financial product type in FinancialProductDTO
	 * Matching with DiscriminatorValue is declared in FinancialProductType relating enumeration
	 * @return the Financial product instance which matched 
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	private FinancialProduct buildShares(FinancialProductDTO dto) 
                      throws InstantiationException, IllegalAccessException {
		FinancialProductType type = dto.getType();
		for (FinancialProductType sharesType : FinancialProductType.values()) {
			if (sharesType.equals(type)) {
				return (FinancialProduct) sharesType.getRelatedClass().newInstance();
			}
		}
		return null;

	}
}

src/main/java/mapping/inheritance/mapper/FinancialProductDTO.java

package mapping.inheritance.mapper;

import java.util.Set;

import mapping.inheritance.domain.Risk;
import mapping.inheritance.domain.FinancialProductType;

public class FinancialProductDTO {
	private String productCode;
	private FinancialProductType type;
	private String description;
	private String version;
	private Set<Risk> risks;
	
	public FinancialProductDTO(String productCode, FinancialProductType type,
			String description, String version, Set<Risk> risks) {
		this.productCode = productCode;
		this.type = type;
		this.description = description;
		this.version = version;
		this.risks = risks;
	}
	public FinancialProductType getType() {
		return type;
	}
	public String getProductCode() {
		return productCode;
	}
	public String getDescription() {
		return description;
	}
	public String getVersion() {
		return version;
	}
	public Set<Risk> getRisks() {
		return risks;
	}
	public void setproductCode(String productCode) {
		this.productCode = productCode;
	}
}

4.3 Resource configuration

src/main/resources/application.yml

spring:

   datasource:
        dataSourceClassName: org.postgresql.ds.PGSimpleDataSource
        test-on-borrow: true
        test-while-idle: true
        validation-query: SELECT 1
        validation-query-timeout: 30
        time-between-eviction-runs-millis: 300000
        url: jdbc:postgresql://localhost:5432/financial_product
        databaseName: financial_product
        serverName:
        username: postgres
        password: admin

   jpa:
           database-platform: org.hibernate.dialect.PostgreSQLDialect
           database: POSTGRESQL
           openInView: false
           show_sql: false
           generate-ddl: false
           hibernate:
               ddl-auto: none
               naming-strategy: org.hibernate.cfg.EJB3NamingStrategy
           properties:
               hibernate.cache.use_second_level_cache: false
               hibernate.cache.use_query_cache: false
               hibernate.generate_statistics: false
               hibernate.cache.region.factory_class: org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory


liquibase:
    change-log: db-changelog.xml

src/main/resources/db-changelog.xml

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd">

	<changeSet id="1" author="Khiem.Truong">
		<createTable tableName="product">
			<column name="product_code" type="varchar(255)">
				<constraints nullable="false" primaryKey="true" />
			</column>
			<column name="type" type="integer">
				<constraints nullable="false" />
			</column>
			<column name="description" type="varchar(255)" />
			<column name="version" type="bigint" />
		</createTable>

		<createTable tableName="risk">
			<column name="risk_code" type="varchar(255)">
				<constraints nullable="false" primaryKey="true" />
			</column>
			<column name="product_code" type="varchar(255)" />
			<column name="risk_weight" type="double" />
		</createTable>


		<loadData tableName="product" encoding="UTF-8"
			file="financial_product.csv" separator=";">
		</loadData>
		<loadData tableName="risk" encoding="UTF-8" file="risk.csv"
			separator=";">
		</loadData>

		<addForeignKeyConstraint baseColumnNames="product_code"
			baseTableName="risk" constraintName="fk_risk_product_code"
			deferrable="true" initiallyDeferred="true" onDelete="CASCADE"
			onUpdate="CASCADE" referencedColumnNames="product_code"
			referencedTableName="product" />

	</changeSet>
</databaseChangeLog>

src/main/resources/financial_product.csv

product_code;type;description;version
SH001;1;New York Stock Exchange;1
SH002;2;New York Stock Exchange;2
SH003;3;London Stock Exchange Group;1
SH004;4;Japan Exchange Group – Tokyo;1

src/main/resources/risk.csv

product_code;risk_code;risk_weight
SH001;RISK001;8.6
SH001;RISK002;7.5

4.4 Make the application executable

src/main/java/mapping/inheritance/service/Application.java

package mapping.inheritance.service;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import mapping.inheritance.domain.FinancialProduct;
import mapping.inheritance.domain.FinancialProductType;
import mapping.inheritance.domain.Risk;
import mapping.inheritance.mapper.FinancialProductDTO;

@SpringBootApplication
@ComponentScan(basePackages = { "mapping.inheritance.domain", 
                               "mapping.inheritance.service",
		               "mapping.inheritance.mapper" })
@EnableJpaRepositories(value = { "mapping.inheritance.repository" })
@EntityScan(basePackageClasses = FinancialProduct.class)
public class Application {

	public static void main(String[] args) {
		ApplicationContext ctx = SpringApplication.run(Application.class, args);

		FinancialProductService shareService = (FinancialProductService)
                                             ctx.getBean("financialProductService");

		List<Risk> risks = Arrays.asList(new Risk("SH005", "RISK003", 5.5), 
                                                new Risk("SH005", "RISK004", 5.0));
		FinancialProductDTO sharesDTO = new FinancialProductDTO("SH005", 
                              FinancialProductType.FUTURES, "NASDAQ Stock Exchange", 
                                                     "1", new HashSet<Risk>(risks));
		shareService.add(sharesDTO);

		List<FinancialProduct> sharesList = shareService.findAll();
		sharesList.forEach(s -> {
			System.out.println(s);
			Set<Risk> risksList = s.getRisks();
			risksList.forEach(r -> System.out.println(r));
		});
	}

}

Output in console:

...
Started Application in 6.912 seconds (JVM running for 7.482)
SH001 SHARES New York Stock Exchange 1
|_ _ _ RISK002 SH001 7.5
|_ _ _ RISK001 SH001 8.6
SH002 BONDS New York Stock Exchange 2
SH003 FUTURES London Stock Exchange Group 1
SH005 FUTURES NASDAQ Stock Exchange 1
|_ _ _ RISK004 SH005 5.0
|_ _ _ RISK003 SH005 5.5
SH004 OPTIONS Japan Exchange Group Tokyo 1

Download Zip here (Build with Maven)

References:

  1. D. Pan, R. Rahman, R. Cuprak, M. Remijan, “Mapping inheritance”, In EJB3 in Action,pp.287-289, Manning, Second Edition, 2014
  2. “Building an Application with Spring Boot”, Spring by Pivotal Software. Accessed on August, 11 2016, https://spring.io/guides/gs/spring-boot/
  3. Liquibase document. Accessed on August, 11 2016, http://www.liquibase.org/documentation/index.html
  4. “Education Center – Australian Securities Exchange”. Accessed on August, 11 2016, http://www.asx.com.au/education/online-courses.htm

One thought on “Single-table strategy for mapping inheritance with JPA

  1. khiem

    Some code changes and what happens
    1. In FinancialProductRepository .class: Add findProductByCode method

    public interface FinancialProductRepository extends JpaRepository{
            // Only for specific Shares Type
    	@Query(value = "Select distinct s from Shares s where s.productCode = :productCode")
    	FinancialProduct findSharesByCode(@Param(value ="productCode") String productCode);
    	// Any product Type
    	@Query(value = "Select distinct p from FinancialProduct p where p.productCode = :productCode")
    	FinancialProduct findProductByCode(@Param(value ="productCode") String productCode);
    }
    

    2. FinancialProductServiceImpl.class: Add a new service

    @Override
    	public FinancialProduct findProductByCode(String productCode) {
    		return sharesRepository.findProductByCode(productCode);
    	}
    

    3. Application.class: Add following codes at bottom

    ...
    System.out.println("Found Shares: " + shareService.findSharesByCode("SH005"));
    System.out.println("Found Financial Product: " + shareService.findProductByCode("SH005"));
    

    Output:

    ...
    SH005 FUTURES NASDAQ Stock Exchange 1
    |_ _ _ RISK004 SH005 5.0
    |_ _ _ RISK003 SH005 5.5
    Found Shares: null
    Found Financial Product: SH005 FUTURES NASDAQ Stock Exchange 1
    
    • When calling the first findSharesByCode, it finds only a Share Type SH005 as in query repository. So result should be NULL. It should be found one if we insert a product with share type.
    • In the second method: findProductByCode, it finds a product type regardless of a particular type that has product code SH005.
    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.