feat: #1316 Single Table Inheritance pattern implemented (#2632)

* implemented single table inheritance

* added the Java documentation for the pattern

* updated code files as per the standards

* added the puml and png diagram for the file

* added README.md

* #1316 Single Table Inheritance pattern implemented

* resolved the Checkstyle violations

* removed the tests due to build failure

* updated the code as per review

* resolved Checkstyle violations
This commit is contained in:
Ved Asole
2024-02-25 17:50:13 +05:30
committed by GitHub
parent 79d41d9336
commit b2c7410c03
16 changed files with 811 additions and 0 deletions
+1
View File
@@ -210,6 +210,7 @@
<module>crtp</module>
<module>log-aggregation</module>
<module>health-check</module>
<module>single-table-inheritance</module>
</modules>
<repositories>
<repository>
+157
View File
@@ -0,0 +1,157 @@
---
title: Single Table Inheritance Pattern
category: Structural
language: en
tag:
- Data access
---
## Single Table Inheritance(STI)
## Intent
Represents an inheritance hierarchy of classes as a single table that has columns for all the fields of the various classes.
## Explanation
Real-world example
> There can be many different types of vehicles in this world but all of them
> come under the single umbrella of Vehicle
In plain words
> It maps each instance of class in an inheritance tree into a single table.
Wikipedia says
> Single table inheritance is a way to emulate object-oriented inheritance in a relational database.
> When mapping from a database table to an object in an object-oriented language,
> a field in the database identifies what class in the hierarchy the object belongs to.
> All fields of all the classes are stored in the same table, hence the name "Single Table Inheritance".
**Programmatic Example**
Baeldung - Hibernate Inheritance
> We can define the strategy we want to use by adding the @Inheritance annotation to the superclass:
```java
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyProduct {
@Id
private long productId;
private String name;
// constructor, getters, setters
}
```
The identifier of the entities is also defined in the superclass.
Then we can add the subclass entities:
```java
@Entity
public class Book extends MyProduct {
private String author;
}
```
```java
@Entity
public class Pen extends MyProduct {
private String color;
}
```
Discriminator Values
- Since the records for all entities will be in the same table, Hibernate needs a way to differentiate between them.
- By default, this is done through a discriminator column called DTYPE that has the name of the entity as a value.
- To customize the discriminator column, we can use the @DiscriminatorColumn annotation:
```java
@Entity(name="products")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="product_type",
discriminatorType = DiscriminatorType.INTEGER)
public class MyProduct {
// ...
}
```
- Here weve chosen to differentiate MyProduct subclass entities by an integer column called product_type.
- Next, we need to tell Hibernate what value each subclass record will have for the product_type column:
```java
@Entity
@DiscriminatorValue("1")
public class Book extends MyProduct {
// ...
}
```
```java
@Entity
@DiscriminatorValue("2")
public class Pen extends MyProduct {
// ...
}
```
- Hibernate adds two other predefined values that the annotation can take — null and not null:
- @DiscriminatorValue(“null”) means that any row without a discriminator value will be mapped to the entity class with this annotation; this can be applied to the root class of the hierarchy.
- @DiscriminatorValue(“not null”) Any row with a discriminator value not matching any of the ones associated with entity definitions will be mapped to the class with this annotation.
## Class diagram
![alt text](./etc/single-table-inheritance.urm.png "Singleton pattern class diagram")
## Applicability
Use the Singleton pattern when
* Use STI When The Subclasses Have The Same Fields/Columns But Different Behavior
- A good indication that STI is right is when the different subclasses have the same fields/columns but different methods. In the accounts example above, we expect all the columns in the database to be used by each subclass. Otherwise, there will be a lot of null columns in the database.
<br><br>
* Use STI When We Expect To Perform Queries Across All Subclasses
- Another good indication STI is right is if we expect to perform queries across all classes. For example, if we want to find the top 10 accounts with the highest balances across all types, STI allows lets us use just one query, whereas MTI will require in memory manipulation.
### Tutorials
- <a href ="https://www.youtube.com/watch?v=M5YrLtAHtOo" >Java Brains - Single Table Inheritance</a>
## Consequences
* Fields are sometimes relevant and sometimes not, which can be confusing
to people using the tables directly.
* Columns used only by some subclasses lead to wasted space in the database.
How much this is actually a problem depends on the specific data
characteristics and how well the database compresses empty columns.
Oracle, for example, is very efficient in trimming wasted space, particularly
if you keep your optional columns to the right side of the database
table. Each database has its own tricks for this.
* The single table may end up being too large, with many indexes and frequent
locking, which may hurt performance. You can avoid this by having
separate index tables that either list keys of rows that have a certain property
or that copy a subset of fields relevant to an index.
* You only have a single namespace for fields, so you have to be sure that
you dont use the same name for different fields. Compound names with
the name of the class as a prefix or suffix help here.
## Related patterns
* MappedSuperclass
* Single Table
* Joined Table
* Table per Class
## Credits
* [Single Table Inheritance - martinFowler.com](https://www.martinfowler.com/eaaCatalog/singleTableInheritance.html)
* [Patterns of Enterprise Application Architecture](https://books.google.co.in/books?id=vqTfNFDzzdIC&pg=PA278&redir_esc=y#v=onepage&q&f=false)
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

@@ -0,0 +1,163 @@
@startuml
package com.iluwatar.repository {
interface VehicleRepository {
}
}
package com.iluwatar.service {
class VehicleService {
- vehicleRepository : VehicleRepository
+ VehicleService(vehicleRepository : VehicleRepository)
+ deleteVehicle(vehicle : Vehicle)
+ getAllVehicles() : List<Vehicle>
+ getVehicle(vehicleId : int) : Vehicle
+ saveVehicle(vehicle : Vehicle) : Vehicle
+ updateVehicle(vehicle : Vehicle) : Vehicle
}
}
package com.iluwatar.entity {
class Car {
- engineCapacity : int
+ Car()
+ Car(manufacturer : String, model : String, noOfPassengers : int, engineCapacity : int)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getEngineCapacity() : int
+ hashCode() : int
+ setEngineCapacity(engineCapacity : int)
+ toString() : String
}
class Freighter {
- flightLength : double
+ Freighter()
+ Freighter(manufacturer : String, model : String, countOfSeats : int, loadCapacity : int, flightLength : double)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getFlightLength() : double
+ hashCode() : int
+ setFlightLength(flightLength : double)
+ toString() : String
}
abstract class PassengerVehicle {
- noOfPassengers : int
+ PassengerVehicle()
+ PassengerVehicle(manufacturer : String, model : String, noOfPassengers : int)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getNoOfPassengers() : int
+ hashCode() : int
+ setNoOfPassengers(noOfPassengers : int)
+ toString() : String
}
class Train {
- noOfCarriages : int
+ Train()
+ Train(manufacturer : String, model : String, noOfPassengers : int, noOfCarriages : int)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getNoOfCarriages() : int
+ hashCode() : int
+ setNoOfCarriages(noOfCarriages : int)
+ toString() : String
}
abstract class TransportVehicle {
- loadCapacity : int
+ TransportVehicle()
+ TransportVehicle(manufacturer : String, model : String, loadCapacity : int)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getLoadCapacity() : int
+ hashCode() : int
+ setLoadCapacity(loadCapacity : int)
+ toString() : String
}
class Truck {
+ towingCapacity : int
+ Truck()
+ Truck(manufacturer : String, model : String, loadCapacity : int, towingCapacity : int)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getTowingCapacity() : int
+ hashCode() : int
+ setTowingCapacity(towingCapacity : int)
+ toString() : String
}
abstract class Vehicle {
- manufacturer : String
- model : String
- vehicleId : int
+ Vehicle()
+ Vehicle(manufacturer : String, model : String)
+ Vehicle(vehicleId : int, manufacturer : String, model : String)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getManufacturer() : String
+ getModel() : String
+ getVehicleId() : int
+ hashCode() : int
+ setManufacturer(manufacturer : String)
+ setModel(model : String)
+ setVehicleId(vehicleId : int)
+ toString() : String
}
}
package com.iluwatar.abstractEntity {
abstract class PassengerVehicle {
- noOfPassengers : int
+ PassengerVehicle()
+ PassengerVehicle(manufacturer : String, model : String, noOfPassengers : int)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getNoOfPassengers() : int
+ hashCode() : int
+ setNoOfPassengers(noOfPassengers : int)
+ toString() : String
}
abstract class TransportVehicle {
- loadCapacity : int
+ TransportVehicle()
+ TransportVehicle(manufacturer : String, model : String, loadCapacity : int)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getLoadCapacity() : int
+ hashCode() : int
+ setLoadCapacity(loadCapacity : int)
+ toString() : String
}
abstract class Vehicle {
- manufacturer : String
- model : String
- vehicleId : int
+ Vehicle()
+ Vehicle(manufacturer : String, model : String)
+ Vehicle(vehicleId : int, manufacturer : String, model : String)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getManufacturer() : String
+ getModel() : String
+ getVehicleId() : int
+ hashCode() : int
+ setManufacturer(manufacturer : String)
+ setModel(model : String)
+ setVehicleId(vehicleId : int)
+ toString() : String
}
}
package com.iluwatar {
class SingleTableInheritance {
- vehicleService : VehicleService
+ SingleTableInheritance(vehicleService : VehicleService)
+ main(args : String[]) {static}
+ run(args : String[])
}
}
SingleTableInheritance --> "-vehicleService" VehicleService
VehicleService --> "-vehicleRepository" VehicleRepository
PassengerVehicle --|> Vehicle
TransportVehicle --|> Vehicle
Car --|> PassengerVehicle
Freighter --|> TransportVehicle
PassengerVehicle --|> Vehicle
Train --|> PassengerVehicle
TransportVehicle --|> Vehicle
Truck --|> TransportVehicle
@enduml
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>java-design-patterns</artifactId>
<version>1.26.0-SNAPSHOT</version>
</parent>
<artifactId>single-table-inheritance</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,93 @@
package com.iluwatar;
import com.iluwatar.entity.Car;
import com.iluwatar.entity.Truck;
import com.iluwatar.entity.Vehicle;
import com.iluwatar.service.VehicleService;
import java.util.List;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Single Table Inheritance pattern :
* <br>
* It maps each instance of class in an inheritance tree into a single table.
* <br>
* <p>
* In case of current project, in order to specify the Single Table Inheritance to Hibernate
* we annotate the main Vehicle root class with @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
* due to which a single root <b>Vehicle</b> class table will be created
* in the database and it will have columns for all the fields of
* it's subclasses(Car, Freighter, Train, Truck). <br>
* Additional to that, a new separate <b>"vehicle_id"</b> column would be added
* to the Vehicle table to save the type of the subclass object that
* is being stored in the database. This value is specified by the @DiscriminatorValue annotation
* value for each subclass in case of Hibernate. <br>
* </p><br>
* Below is the main Spring Boot Application class from where the Program Runs.
* <p>
* It implements the CommandLineRunner to run the statements at the
* start of the application program.
* </p>
*/
@SpringBootApplication
@AllArgsConstructor
public class SingleTableInheritance implements CommandLineRunner {
//Autowiring the VehicleService class to execute the business logic methods
private final VehicleService vehicleService;
/**
* The entry point of the Spring Boot Application.
*
* @param args program runtime arguments
*/
public static void main(String[] args) {
SpringApplication.run(SingleTableInheritance.class, args);
}
/**
* The starting point of the CommandLineRunner
* where the main program is run.
*
* @param args program runtime arguments
*/
@Override
public void run(String... args) throws Exception {
Logger log = LoggerFactory.getLogger(SingleTableInheritance.class);
log.info("Saving Vehicles :- ");
// Saving Car to DB as a Vehicle
Vehicle vehicle1 = new Car("Tesla", "Model S", 4, 825);
Vehicle car1 = vehicleService.saveVehicle(vehicle1);
log.info("Vehicle 1 saved : {}", car1);
// Saving Truck to DB as a Vehicle
Vehicle vehicle2 = new Truck("Ford", "F-150", 3325, 14000);
Vehicle truck1 = vehicleService.saveVehicle(vehicle2);
log.info("Vehicle 2 saved : {}\n", truck1);
log.info("Fetching Vehicles :- ");
// Fetching the Car from DB
Car savedCar1 = (Car) vehicleService.getVehicle(vehicle1.getVehicleId());
log.info("Fetching Car1 from DB : {}", savedCar1);
// Fetching the Truck from DB
Truck savedTruck1 = (Truck) vehicleService.getVehicle(vehicle2.getVehicleId());
log.info("Fetching Truck1 from DB : {}\n", savedTruck1);
log.info("Fetching All Vehicles :- ");
// Fetching the Vehicles present in the DB
List<Vehicle> allVehiclesFromDb = vehicleService.getAllVehicles();
allVehiclesFromDb.forEach(s -> log.info(s.toString()));
}
}
@@ -0,0 +1,39 @@
package com.iluwatar.entity;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* A class that extends the PassengerVehicle class
* and provides the concrete inheritance implementation of the Car.
*
* @see PassengerVehicle PassengerVehicle
* @see Vehicle Vehicle
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Entity
@DiscriminatorValue(value = "CAR")
public class Car extends PassengerVehicle {
private int engineCapacity;
public Car(String manufacturer, String model, int noOfPassengers, int engineCapacity) {
super(manufacturer, model, noOfPassengers);
this.engineCapacity = engineCapacity;
}
// Overridden the toString method to specify the Vehicle object
@Override
public String toString() {
return "Car{"
+ super.toString()
+ '}';
}
}
@@ -0,0 +1,41 @@
package com.iluwatar.entity;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* A class that extends the TransportVehicle class
* and provides the concrete inheritance implementation of the Car.
*
* @see TransportVehicle TransportVehicle
* @see Vehicle Vehicle
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Entity
@DiscriminatorValue(value = "FREIGHTER")
public class Freighter extends TransportVehicle {
private double flightLength;
public Freighter(String manufacturer, String model, int loadCapacity, double flightLength) {
super(manufacturer, model, loadCapacity);
this.flightLength = flightLength;
}
// Overridden the toString method to specify the Vehicle object
@Override
public String toString() {
return "Freighter{ "
+ super.toString()
+ " ,"
+ "flightLength="
+ flightLength
+ '}';
}
}
@@ -0,0 +1,30 @@
package com.iluwatar.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* An abstract class that extends the Vehicle class
* and provides properties for the Passenger type of Vehicles.
*
* @see Vehicle
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public abstract class PassengerVehicle extends Vehicle {
private int noOfPassengers;
protected PassengerVehicle(String manufacturer, String model, int noOfPassengers) {
super(manufacturer, model);
this.noOfPassengers = noOfPassengers;
}
@Override
public String toString() {
return super.toString();
}
}
@@ -0,0 +1,38 @@
package com.iluwatar.entity;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* A class that extends the PassengerVehicle class
* and provides the concrete inheritance implementation of the Car.
*
* @see PassengerVehicle PassengerVehicle
* @see Vehicle Vehicle
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Entity
@DiscriminatorValue(value = "TRAIN")
public class Train extends PassengerVehicle {
private int noOfCarriages;
public Train(String manufacturer, String model, int noOfPassengers, int noOfCarriages) {
super(manufacturer, model, noOfPassengers);
this.noOfCarriages = noOfCarriages;
}
// Overridden the toString method to specify the Vehicle object
@Override
public String toString() {
return "Train{"
+ super.toString()
+ '}';
}
}
@@ -0,0 +1,25 @@
package com.iluwatar.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* An abstract class that extends the Vehicle class
* and provides properties for the Transport type of Vehicles.
*
* @see Vehicle
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public abstract class TransportVehicle extends Vehicle {
private int loadCapacity;
protected TransportVehicle(String manufacturer, String model, int loadCapacity) {
super(manufacturer, model);
this.loadCapacity = loadCapacity;
}
}
@@ -0,0 +1,38 @@
package com.iluwatar.entity;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* A class that extends the PassengerVehicle class
* and provides the concrete inheritance implementation of the Car.
*
* @see TransportVehicle TransportVehicle
* @see Vehicle Vehicle
*/
@Data
@NoArgsConstructor
@Entity
@DiscriminatorValue(value = "TRUCK")
public class Truck extends TransportVehicle {
private int towingCapacity;
public Truck(String manufacturer, String model, int loadCapacity, int towingCapacity) {
super(manufacturer, model, loadCapacity);
this.towingCapacity = towingCapacity;
}
// Overridden the toString method to specify the Vehicle object
@Override
public String toString() {
return "Truck{ "
+ super.toString()
+ ", "
+ "towingCapacity="
+ towingCapacity
+ '}';
}
}
@@ -0,0 +1,54 @@
package com.iluwatar.entity;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* An abstract class that is the root of the Vehicle Inheritance hierarchy
* and basic provides properties for all the vehicles.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "VEHICLE_TYPE")
public abstract class Vehicle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int vehicleId;
private String manufacturer;
private String model;
protected Vehicle(String manufacturer, String model) {
this.manufacturer = manufacturer;
this.model = model;
}
@Override
public String toString() {
return "Vehicle{"
+ "vehicleId="
+ vehicleId
+ ", manufacturer='"
+ manufacturer
+ '\''
+ ", model='"
+ model
+ '}';
}
}
@@ -0,0 +1,14 @@
package com.iluwatar.repository;
import com.iluwatar.entity.Vehicle;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* A repository that is extending the JPA Repository
* to provide the default Spring DATA JPA methods for the Vehicle class.
*/
@Repository
public interface VehicleRepository extends JpaRepository<Vehicle, Integer> {
}
@@ -0,0 +1,71 @@
package com.iluwatar.service;
import com.iluwatar.entity.Vehicle;
import com.iluwatar.repository.VehicleRepository;
import java.util.List;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
/**
* A service class that is used to provide the business logic
* for the Vehicle class and connect to the database to
* perform the CRUD operations on the root Vehicle class.
*
* @see Vehicle
*/
@Service
@AllArgsConstructor
public class VehicleService {
private final VehicleRepository vehicleRepository;
/**
* A method to save all the vehicles to the database.
*
* @param vehicle Vehicle bbject
* @see Vehicle
*/
public Vehicle saveVehicle(Vehicle vehicle) {
return vehicleRepository.save(vehicle);
}
/**
* A method to get a specific vehicle from vehicle id.
*
* @param vehicleId Vehicle Id
* @see Vehicle
*/
public Vehicle getVehicle(int vehicleId) {
return vehicleRepository.findById(vehicleId).orElse(null);
}
/**
* A method to get all the vehicles saved in the database.
*
* @see Vehicle
*/
public List<Vehicle> getAllVehicles() {
return vehicleRepository.findAll();
}
/**
* A method to update a vehicle in the database.
*
* @param vehicle Vehicle object
* @see Vehicle
*/
public Vehicle updateVehicle(Vehicle vehicle) {
return vehicleRepository.save(vehicle);
}
/**
* A method to save all the vehicles to the database.
*
* @param vehicle Vehicle object
* @see Vehicle
*/
public void deleteVehicle(Vehicle vehicle) {
vehicleRepository.delete(vehicle);
}
}
@@ -0,0 +1,11 @@
# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:sti
spring.datasource.username=sa
spring.datasource.password=password
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.hibernate.ddl-auto=update
#Uncomment the below properties to see the SQL being generated in backend
#spring.jpa.show-sql=true
#spring.jpa.properties.hibernate.format_sql=true