JPA Inheritance vs Composition Spring Boot Example
1. Introduction
Inheritance is an “IS-A” type of relationship in object-oriented programming (OOP). Inheritance is tightly coupled since child classes extend from the parent class. JPA @MappedSuperclass can be annotated to the super/parent class. Composition is a “HAS-A” type of relationship in OOP, in which an object is composed of smaller associated objects. JPA @Embeddable and @Embedded can be annotated to these associating objects. In this example, I will create a spring boot project that explains JPA inheritance vs composition with the following annotations.
- @MappedSuperclass: marks a class whose mapping information is applied to the entities that inherit from it. A mapped superclass has no separate table defined for it.
- @Embeddable: specifies a class whose instances are stored as a part of an owning entity. Each of the persistent properties or fields of the embedded object is mapped to the database table for the owning entity.
- @Embedded: specifies a persistent field or property of an entity whose value is an instance of an embeddable class. The embeddable class must be annotated as @Embeddable.
- @OneToMany: specifies a many-valued association with one-to-many multiplicity.
- @ManyToOne: specifies a single-valued association to another entity class that has many-to-one multiplicity.
2. Setup
In this step, I will create a gradle project for a Spring boot data JPA.
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.3.2' id 'io.spring.dependency-management' version '1.1.6' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
Also configure application.properties
as the following:
application.properties
spring.application.name=jpa-inheritance-comp-demo spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto= update spring.h2.console.enabled=true
3. JPA Inheritance vs Composition Diagram
As Figure 1 shows, The common fields “CREATED_BY
and CREATED_DATE
” are mapped by a base class annotated with @MappedSuperclass
. This is a JPA inheritance example. The home address and work address are mapped by the address objects annotated with @Embedded
annotation for a JPA composition example.
Figure 2 Java class diagram shows the inheritance hierarchy among the classes.
4. JPA Inheritance vs Composition Data Models
In this step, I will create six data model classes.
4.1 SuperBaseClass
In this step, I will create a super base class – SuperBaseClass
that annotates with @MappedSuperclass
.
SuperBaseClass.java
package com.zheng.demo.jpa.model; import java.io.Serializable; import java.time.ZonedDateTime; import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; import lombok.Data; import lombok.EqualsAndHashCode; @Data @MappedSuperclass public class SuperBaseClass implements Serializable { private static final long serialVersionUID = -8418746719009088954L; @Column(name = "CREATED_BY") @EqualsAndHashCode.Include private String createdBy; @Column(name = "CREATED_DATE") @EqualsAndHashCode.Include private ZonedDateTime createdDate; }
- Line 12:
@MappedSuperclass
maps its properties to child entities’ columns.
4.2 Employee
In this step, I will create an Employee
class extending from the SuperBaseClass
defined in step 4.1.
Employee.java
package com.zheng.demo.jpa.model; import jakarta.persistence.AttributeOverride; import jakarta.persistence.AttributeOverrides; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @Entity(name = "T_EMPLOYEE") @EqualsAndHashCode(callSuper = false) public class Employee extends SuperBaseClass { private static final long serialVersionUID = -198345917492306405L; @Column(name = "FIRST_NAME") private String firstName; @Embedded @AttributeOverrides({ @AttributeOverride(name = "line1", column = @Column(name = "HOME_ADDR_LINE1")), @AttributeOverride(name = "line2", column = @Column(name = "HOME_ADDR_LINE2")), @AttributeOverride(name = "city", column = @Column(name = "HOME_ADDR_CITY")), @AttributeOverride(name = "state", column = @Column(name = "HOME_ADDR_STATE")) }) private Address homeAddress; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "EMPLOYEE_ID") private Integer id; @Column(name = "LAST_NAME") private String lastName; @Embedded @AttributeOverrides({ @AttributeOverride(name = "line1", column = @Column(name = "WORK_ADDR_LINE1")), @AttributeOverride(name = "line2", column = @Column(name = "WORK_ADDR_LINE2")), @AttributeOverride(name = "city", column = @Column(name = "WORK_ADDR_CITY")), @AttributeOverride(name = "state", column = @Column(name = "WORK_ADDR_STATE")) }) private Address workAddress; }
- Line 26,41:
@Embedded
annotation maps the home and work addresses to its columns.
4.3 Item
In this step, I will create an Item
class extending from the SuperBaseClass
defined in step 4.1.
Item.java
package com.zheng.demo.jpa.model; import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @Entity @Table(name = "T_ITEM") @EqualsAndHashCode(callSuper = false) public class Item extends SuperBaseClass { private static final long serialVersionUID = 2328545108774264879L; private String description; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "ITEM_ID") private Integer id; @JsonBackReference @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ORDER_ID") private Order order; public Item(Order order, String description) { this.setDescription(description); this.setOrder(order); } }
- Line 34:
@ManyToOne
annotation marks the many-to-one relationship betweenItem
andOrder
entities.
4.4 Order
In this step, I will create an Order
class extending from the SuperBaseClass
defined in step 4.1.
Order.java
package com.zheng.demo.jpa.model; import java.time.ZonedDateTime; import java.util.HashSet; import java.util.Set; import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @Entity @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) @Table(name = "T_ORDER") public class Order extends SuperBaseClass { private static final long serialVersionUID = -520933370803886172L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "ORDER_ID") @EqualsAndHashCode.Include private Integer id; @JsonManagedReference @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "order") private Set items = new HashSet<>(); @EqualsAndHashCode.Include private String name; public void addItem(String detail) { Item m = new Item(); m.setCreatedBy("system"); m.setCreatedDate(ZonedDateTime.now()); m.setDescription(detail); m.setOrder(this); items.add(m); } }
- Line 38:
@OneToMany
marks one-to-many relationship betweenOrder
andItem
entities.
4.5 Task
In this step, I will create a Task
class extending from the SuperBaseClass
defined in step 4.1.
Task.java
package com.zheng.demo.jpa.model; import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @Entity @Table(name = "T_TASK") @EqualsAndHashCode(callSuper = false) public class Task extends SuperBaseClass { private static final long serialVersionUID = 7871984492489240292L; private String description; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "EMPLOYEE_ID") @JsonBackReference private Employee employee; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "TASK_ID") private Integer id; }
- Line 27:
@ManyToOne
annotation marks the M-1 relationship betweenTask
andEmployee
entities.
4.6 Address
In this step, I will create an embeddable Address
class.
Address.java
package com.zheng.demo.jpa.model; import jakarta.persistence.Embeddable; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @Embeddable public class Address { private String city; private String line1; private String line2; private String state; }
- Line 9:
@Embeddable
marks this class which can be used as an embedded component for other entities.
5. JPA Repositories
5.1 Employee Repository
In this step, I will create an EmployeeRepo
interface which extends from the JpaRepository
.
EmployeeRepo.java
package com.zheng.demo.jpa.repo; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.zheng.demo.jpa.model.Employee; @Repository public interface EmployeeRepo extends JpaRepository<Employee, Integer> { }
5.2 Item Repository
In this step, I will create an ItemRepo
interface which extends from the JpaRepository
.
ItemRepo.java
package com.zheng.demo.jpa.repo; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.zheng.demo.jpa.model.Item; @Repository public interface ItemRepo extends JpaRepository<Item, Integer> { List<Item> findItemsByOrderId(Integer orderId); }
5.3 Order Repository
In this step, I will create an OrderRepo
interface which extends from the JpaRepository
.
OrderRepo.java
package com.zheng.demo.jpa.repo; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.zheng.demo.jpa.model.Order; @Repository public interface OrderRepo extends JpaRepository<Order, Integer> { }
5.4 Task Repository
In this step, I will create a TaskRepo
interface which extends from the JpaRepository
.
TaskRepo.java
package com.zheng.demo.jpa.repo; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.zheng.demo.jpa.model.Task; @Repository public interface TaskRepo extends JpaRepository<Task, Integer> { List<Task> findTasksByEmployeeId(Integer employeeId); }
6. Services
6.1 Employee Service
In this step, I will create an EmployeeService
class that can save, read, and add tasks to an employee.
EmployeeService.java
package com.zheng.demo.jpa.service; import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.zheng.demo.jpa.model.Address; import com.zheng.demo.jpa.model.Employee; import com.zheng.demo.jpa.model.Task; import com.zheng.demo.jpa.repo.EmployeeRepo; import com.zheng.demo.jpa.repo.TaskRepo; @Service public class EmployeeService { @Autowired private EmployeeRepo empRepo; @Autowired private TaskRepo taskRepo; @Transactional public void addTask(Integer empId, String taskDetail) { Optional<Employee> foundP = get(empId); if (foundP.isPresent()) { Task note = new Task(); note.setCreatedBy("system"); note.setCreatedDate(ZonedDateTime.now()); note.setDescription(taskDetail); note.setEmployee(foundP.get()); taskRepo.save(note); } } @Transactional(readOnly = true) public Optional<Employee> get(Integer empId) { return empRepo.findById(empId); } @Transactional public List<Task> getTasks(Integer empId) { return taskRepo.findTasksByEmployeeId(empId); } @Transactional public Integer save(final String firstName, final String lastName, final String state, final String city, final String line1) { Address address = new Address(); address.setCity(city); address.setLine1(line1); address.setState(state); Employee user = new Employee(); user.setFirstName(firstName); user.setLastName(lastName); user.setHomeAddress(address); user.setCreatedBy("System"); user.setCreatedDate(ZonedDateTime.now()); user = empRepo.save(user); return user.getId(); } }
6.2 Order Service
In this step, I will create an OrderServce
class that can create an order, read orders, and add items to an order.
OrderService.java
package com.zheng.demo.jpa.service; import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.zheng.demo.jpa.model.Item; import com.zheng.demo.jpa.model.Order; import com.zheng.demo.jpa.repo.ItemRepo; import com.zheng.demo.jpa.repo.OrderRepo; @Service public class OrderService { @Autowired private ItemRepo itemRepo; @Autowired private OrderRepo orderRepo; @Transactional public Order addItem(Integer orderId, String itemDesc) { Order itemOrder = null; Optional<Order> foundOrd = getOrder(orderId); if (foundOrd.isPresent()) { itemOrder = foundOrd.get(); } else { itemOrder = new Order(); itemOrder.setName("NA"); } Item newItem = new Item(); newItem.setCreatedBy("system"); newItem.setCreatedDate(ZonedDateTime.now()); newItem.setDescription(itemDesc); newItem.setOrder(itemOrder); itemRepo.save(newItem); return itemOrder; } @Transactional public List<Item> getItems(Integer orderId) { return itemRepo.findItemsByOrderId(orderId); } @Transactional(readOnly = true) public Optional<Order> getOrder(Integer orderId) { return orderRepo.findById(orderId); } @Transactional public Order save(final String name, final String orderItemDe) { Order order = new Order(); order.setCreatedBy("System"); order.setCreatedDate(ZonedDateTime.now()); order.setName(name); order.addItem(orderItemDe); order = orderRepo.save(order); return order; } }
6.3 Test Employee Service
In this step, I will create an EmployeeServiceTest
to test the methods.
EmployeeServiceTest.java
package com.zheng.demo.jpa.service; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import com.zheng.demo.jpa.model.Employee; import com.zheng.demo.jpa.model.Task; @SpringBootTest class EmployeeServiceTest { @Autowired private EmployeeService testService; @Test void test_save_get() { Integer maryId = testService.save("Mary", "Zheng", "MO", "St. Louis", "100 street"); Optional<Employee> foundMary = testService.get(maryId); assertFalse(foundMary.isEmpty()); assertEquals("Mary", foundMary.get().getFirstName()); } @Test void test_addTask_getTasks() { Integer alexId = testService.save("Alex", "Zheng", "MO", "St. Louis", "200 street"); testService.addTask(alexId, "Alex note 1"); testService.addTask(alexId, "Alex note 2"); List<Task> notes = testService.getTasks(alexId); assertEquals(2, notes.size()); assertEquals("Alex note", notes.get(0).getDescription().substring(0, 9)); } }
Run the EmployeeServiceTest
and capture the output here.
EmployeeServiceTest Output
11:01:33.337 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.zheng.demo.jpa.service.EmployeeServiceTest]: EmployeeServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 11:01:33.514 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.zheng.demo.jpa.JpaInheritanceCompDemoApplication for test class com.zheng.demo.jpa.service.EmployeeServiceTest . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.3.2) 2024-09-07T11:01:34.055-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] c.z.d.jpa.service.EmployeeServiceTest : Starting EmployeeServiceTest using Java 17.0.11 with PID 38824 (started by azpm0 in C:\MaryTools\workspace\jpa-inheritance-comp-demo) 2024-09-07T11:01:34.057-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] c.z.d.jpa.service.EmployeeServiceTest : No active profile set, falling back to 1 default profile: "default" 2024-09-07T11:01:34.822-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2024-09-07T11:01:34.906-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 69 ms. Found 4 JPA repository interfaces. 2024-09-07T11:01:35.439-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2024-09-07T11:01:35.567-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.5.2.Final 2024-09-07T11:01:35.628-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled 2024-09-07T11:01:36.077-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2024-09-07T11:01:36.113-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2024-09-07T11:01:36.398-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:d4de5281-478a-40bd-9176-10f9736ad702 user=SA 2024-09-07T11:01:36.399-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2024-09-07T11:01:36.451-05:00 WARN 38824 --- [jpa-inheritance-comp-demo] [ main] org.hibernate.orm.deprecation : HHH90000025: H2Dialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) 2024-09-07T11:01:37.521-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) Hibernate: create table t_employee (employee_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, first_name varchar(255), home_addr_city varchar(255), home_addr_line1 varchar(255), home_addr_line2 varchar(255), home_addr_state varchar(255), last_name varchar(255), work_addr_city varchar(255), work_addr_line1 varchar(255), work_addr_line2 varchar(255), work_addr_state varchar(255), primary key (employee_id)) Hibernate: create table t_item (item_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, description varchar(255), order_id integer, primary key (item_id)) Hibernate: create table t_order (order_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, name varchar(255), primary key (order_id)) Hibernate: create table t_task (task_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, description varchar(255), employee_id integer, primary key (task_id)) Hibernate: create sequence t_employee_seq start with 1 increment by 50 Hibernate: create sequence t_item_seq start with 1 increment by 50 Hibernate: create sequence t_order_seq start with 1 increment by 50 Hibernate: create sequence t_task_seq start with 1 increment by 50 Hibernate: alter table if exists t_item add constraint FKtesk72ntb0eubn30cxidbymp4 foreign key (order_id) references t_order Hibernate: alter table if exists t_task add constraint FKc7gwb3d0o8quad9ueqxe6kc7y foreign key (employee_id) references t_employee 2024-09-07T11:01:37.576-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2024-09-07T11:01:38.164-05:00 WARN 38824 --- [jpa-inheritance-comp-demo] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning 2024-09-07T11:01:38.559-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:d4de5281-478a-40bd-9176-10f9736ad702' 2024-09-07T11:01:38.634-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ main] c.z.d.jpa.service.EmployeeServiceTest : Started EmployeeServiceTest in 4.909 seconds (process running for 10.605) Hibernate: select next value for t_employee_seq Hibernate: insert into t_employee (created_by,created_date,first_name,home_addr_city,home_addr_line1,home_addr_line2,home_addr_state,last_name,work_addr_city,work_addr_line1,work_addr_line2,work_addr_state,employee_id) values (?,?,?,?,?,?,?,?,?,?,?,?,?) Hibernate: select e1_0.employee_id,e1_0.created_by,e1_0.created_date,e1_0.first_name,e1_0.home_addr_city,e1_0.home_addr_line1,e1_0.home_addr_line2,e1_0.home_addr_state,e1_0.last_name,e1_0.work_addr_city,e1_0.work_addr_line1,e1_0.work_addr_line2,e1_0.work_addr_state from t_employee e1_0 where e1_0.employee_id=? Hibernate: select next value for t_task_seq Hibernate: insert into t_task (created_by,created_date,description,employee_id,task_id) values (?,?,?,?,?) Hibernate: select e1_0.employee_id,e1_0.created_by,e1_0.created_date,e1_0.first_name,e1_0.home_addr_city,e1_0.home_addr_line1,e1_0.home_addr_line2,e1_0.home_addr_state,e1_0.last_name,e1_0.work_addr_city,e1_0.work_addr_line1,e1_0.work_addr_line2,e1_0.work_addr_state from t_employee e1_0 where e1_0.employee_id=? Hibernate: select next value for t_task_seq Hibernate: insert into t_task (created_by,created_date,description,employee_id,task_id) values (?,?,?,?,?) Hibernate: select t1_0.task_id,t1_0.created_by,t1_0.created_date,t1_0.description,t1_0.employee_id from t_task t1_0 where t1_0.employee_id=? Hibernate: select next value for t_employee_seq Hibernate: insert into t_employee (created_by,created_date,first_name,home_addr_city,home_addr_line1,home_addr_line2,home_addr_state,last_name,work_addr_city,work_addr_line1,work_addr_line2,work_addr_state,employee_id) values (?,?,?,?,?,?,?,?,?,?,?,?,?) Hibernate: select e1_0.employee_id,e1_0.created_by,e1_0.created_date,e1_0.first_name,e1_0.home_addr_city,e1_0.home_addr_line1,e1_0.home_addr_line2,e1_0.home_addr_state,e1_0.last_name,e1_0.work_addr_city,e1_0.work_addr_line1,e1_0.work_addr_line2,e1_0.work_addr_state from t_employee e1_0 where e1_0.employee_id=? 2024-09-07T11:01:39.447-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' 2024-09-07T11:01:39.451-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2024-09-07T11:01:39.453-05:00 INFO 38824 --- [jpa-inheritance-comp-demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
- Line 26 -29: verify tables created with both inheritance and composition columns.
- created_by varchar(255), created_date timestamp(6) with time zone are in all 4 tables because of the @MappedSuperclass annotation
- Line 26:
Employee
entity maps toT_EMPLOYEE
table. It shows DDL ascreate table t_employee (employee_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, first_name varchar(255), home_addr_city varchar(255), home_addr_line1 varchar(255), home_addr_line2 varchar(255), home_addr_state varchar(255), last_name varchar(255), work_addr_city varchar(255), work_addr_line1 varchar(255), work_addr_line2 varchar(255), work_addr_state varchar(255), primary key (employee_id)).
- Line 27:
Item
entity maps toT_ITEM
table - Line 28:
Order
entity maps toT_ORDER
table. - Line 29:
Task
entity maps toT_TASK
table
6.4 Test Order Service
In this step, I will create an OrderServiceTest
to test the methods.
OrderServiceTest.java
package com.zheng.demo.jpa.service; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import com.zheng.demo.jpa.model.Item; import com.zheng.demo.jpa.model.Order; @SpringBootTest class OrderServiceTest { @Autowired private OrderService testService; @Test void test_save_get() { Order orderId = testService.save("Order1", "Item 1"); Optional<Order> foundOrder = testService.getOrder(orderId.getId()); assertTrue(foundOrder.isPresent()); assertEquals("Order1", foundOrder.get().getName()); List<Item> orderItems = testService.getItems(orderId.getId()); assertFalse(orderItems.isEmpty()); assertEquals("Item 1", orderItems.get(0).getDescription()); } @Test void test_addItem_getItems() { Order alexId = testService.save("Alex Zheng", "Alex note info"); testService.addItem(alexId.getId(), "Alex note 1"); testService.addItem(alexId.getId(), "Alex note 2"); List<Item> notes = testService.getItems(alexId.getId()); assertEquals(3, notes.size()); assertEquals("Alex note", notes.get(0).getDescription().substring(0, 9)); } }
Run OrderServiceTest
and capture the output here.
OrderServiceTest Output
11:06:33.894 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.zheng.demo.jpa.service.OrderServiceTest]: OrderServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 11:06:33.996 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.zheng.demo.jpa.JpaInheritanceCompDemoApplication for test class com.zheng.demo.jpa.service.OrderServiceTest . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.3.2) 2024-09-07T11:06:34.440-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] c.z.demo.jpa.service.OrderServiceTest : Starting OrderServiceTest using Java 17.0.11 with PID 28860 (started by azpm0 in C:\MaryTools\workspace\jpa-inheritance-comp-demo) 2024-09-07T11:06:34.447-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] c.z.demo.jpa.service.OrderServiceTest : No active profile set, falling back to 1 default profile: "default" 2024-09-07T11:06:35.033-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2024-09-07T11:06:35.117-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 72 ms. Found 4 JPA repository interfaces. 2024-09-07T11:06:35.607-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2024-09-07T11:06:35.666-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.5.2.Final 2024-09-07T11:06:35.701-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled 2024-09-07T11:06:36.000-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2024-09-07T11:06:36.033-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2024-09-07T11:06:36.209-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:f94d384e-ed4b-4c4f-a585-dbb775dc16de user=SA 2024-09-07T11:06:36.211-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2024-09-07T11:06:36.245-05:00 WARN 28860 --- [jpa-inheritance-comp-demo] [ main] org.hibernate.orm.deprecation : HHH90000025: H2Dialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) 2024-09-07T11:06:37.168-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) Hibernate: create table t_employee (employee_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, first_name varchar(255), home_addr_city varchar(255), home_addr_line1 varchar(255), home_addr_line2 varchar(255), home_addr_state varchar(255), last_name varchar(255), work_addr_city varchar(255), work_addr_line1 varchar(255), work_addr_line2 varchar(255), work_addr_state varchar(255), primary key (employee_id)) Hibernate: create table t_item (item_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, description varchar(255), order_id integer, primary key (item_id)) Hibernate: create table t_order (order_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, name varchar(255), primary key (order_id)) Hibernate: create table t_task (task_id integer not null, created_by varchar(255), created_date timestamp(6) with time zone, description varchar(255), employee_id integer, primary key (task_id)) Hibernate: create sequence t_employee_seq start with 1 increment by 50 Hibernate: create sequence t_item_seq start with 1 increment by 50 Hibernate: create sequence t_order_seq start with 1 increment by 50 Hibernate: create sequence t_task_seq start with 1 increment by 50 Hibernate: alter table if exists t_item add constraint FKtesk72ntb0eubn30cxidbymp4 foreign key (order_id) references t_order Hibernate: alter table if exists t_task add constraint FKc7gwb3d0o8quad9ueqxe6kc7y foreign key (employee_id) references t_employee 2024-09-07T11:06:37.212-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2024-09-07T11:06:37.769-05:00 WARN 28860 --- [jpa-inheritance-comp-demo] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning 2024-09-07T11:06:38.112-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:f94d384e-ed4b-4c4f-a585-dbb775dc16de' 2024-09-07T11:06:38.181-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ main] c.z.demo.jpa.service.OrderServiceTest : Started OrderServiceTest in 3.994 seconds (process running for 4.973) Hibernate: select next value for t_order_seq Hibernate: select next value for t_item_seq Hibernate: insert into t_order (created_by,created_date,name,order_id) values (?,?,?,?) Hibernate: insert into t_item (created_by,created_date,description,order_id,item_id) values (?,?,?,?,?) Hibernate: select o1_0.order_id,o1_0.created_by,o1_0.created_date,o1_0.name from t_order o1_0 where o1_0.order_id=? Hibernate: select next value for t_item_seq Hibernate: insert into t_item (created_by,created_date,description,order_id,item_id) values (?,?,?,?,?) Hibernate: select o1_0.order_id,o1_0.created_by,o1_0.created_date,o1_0.name from t_order o1_0 where o1_0.order_id=? Hibernate: insert into t_item (created_by,created_date,description,order_id,item_id) values (?,?,?,?,?) Hibernate: select i1_0.item_id,i1_0.created_by,i1_0.created_date,i1_0.description,i1_0.order_id from t_item i1_0 where i1_0.order_id=? Hibernate: select next value for t_order_seq Hibernate: insert into t_order (created_by,created_date,name,order_id) values (?,?,?,?) Hibernate: insert into t_item (created_by,created_date,description,order_id,item_id) values (?,?,?,?,?) Hibernate: select o1_0.order_id,o1_0.created_by,o1_0.created_date,o1_0.name from t_order o1_0 where o1_0.order_id=? Hibernate: select i1_0.item_id,i1_0.created_by,i1_0.created_date,i1_0.description,i1_0.order_id from t_item i1_0 where i1_0.order_id=? 2024-09-07T11:06:38.923-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' 2024-09-07T11:06:38.926-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2024-09-07T11:06:38.928-05:00 INFO 28860 --- [jpa-inheritance-comp-demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
- Table DDL is printed out as step 6.3.
7. Conclusion
In this example, I demonstrated the following common JPA inheritance and composition annotations in a spring boot project:
@MappedSuperClass
: marks a class whose mapping information is applied to the entities that inherit from it. A mapped superclass has no separate table defined for it.@Embeddable
: specifies a class whose instances are stored as a part of an owning entity. Each of the persistent properties or fields of the embedded object is mapped to the database table for the owning entity.@Embedded
: specifies a persistent field or property of an entity whose value is an instance of an embeddable class. The embeddable class must be annotated as @Embeddable.@OneToMany
: specifies a many-valued association with one-to-many multiplicity.@ManyToOne
: specifies a single-valued association to another entity class that has many-to-one multiplicity.
8. Download
This was an example of a gradle project which includes common JPA inheritance and composition annotations.
You can download the full source code of this example here: JPA Inheritance vs Composition Spring Boot Example