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.
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.
Figure 2: Store all Financial Products in a single table.
3. UML class diagram
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:
- D. Pan, R. Rahman, R. Cuprak, M. Remijan, “Mapping inheritance”, In EJB3 in Action,pp.287-289, Manning, Second Edition, 2014
- “Building an Application with Spring Boot”, Spring by Pivotal Software. Accessed on August, 11 2016, https://spring.io/guides/gs/spring-boot/
- Liquibase document. Accessed on August, 11 2016, http://www.liquibase.org/documentation/index.html
- “Education Center – Australian Securities Exchange”. Accessed on August, 11 2016, http://www.asx.com.au/education/online-courses.htm
Some code changes and what happens
1. In FinancialProductRepository .class: Add findProductByCode method
2. FinancialProductServiceImpl.class: Add a new service
3. Application.class: Add following codes at bottom
Output: