February 22, 2022

Spring + JPA (Hibernate) OneToMany Example

In this post we’ll see an example of Spring integration with JPA (Hibernate JPA implementation), DB used is MySQL. For this example two tables are used having bi-directional association which is depicted using @ManyToOne and @OneToMany annotations in entity classes.

If you want to see how to create Maven project, please check this post- Create Java Project Using Maven in Eclipse

Maven dependencies

<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>

  <groupId>com.knpcode</groupId>
  <artifactId>SpringProject</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>SpringProject</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
    <spring.version>5.1.8.RELEASE</spring.version>
    <spring.data>2.1.10.RELEASE</spring.data>
    <hibernate.jpa>5.4.3.Final</hibernate.jpa>
    <mysql.version>8.0.17</mysql.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>${hibernate.jpa}</version>
    </dependency>
    <!-- MySQL Driver -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>${mysql.version}</version>
    </dependency>
    <dependency>	
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.8.2</version>
    </dependency>
  </dependencies>
  <build>
    <sourceDirectory>src</sourceDirectory>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
          <release>10</release>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.2.1</version>
        <configuration>
          <warSourceDirectory>WebContent</warSourceDirectory>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Dependencies are added for Spring core, Spring context and Spring ORM.

Dependency for Hibernate (hibernate-entitymanager) is added as Hibernate JPA implementation is used. This dependency hibernate-entitymanager gets all the dependent jars too like hibernate-core.

MySQL connector is used for connecting to MySQL DB from Java application.

DB Tables

There are two tables employee and account where an employee may have multiple accounts. For that one-to-many relationship there is a foreign key constraint in account table where primary key in employee table (id) is added as a foreign key in account.

CREATE TABLE `employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `department` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

CREATE TABLE `account` (
  `acct_id` int(11) NOT NULL AUTO_INCREMENT,
  `acct_no` varchar(45) NOT NULL,
  `emp_id` int(11) NOT NULL,
  PRIMARY KEY (`acct_id`),
  UNIQUE KEY `acct_no_UNIQUE` (`acct_no`),
  KEY `id_idx` (`emp_id`),
  CONSTRAINT `emp_fk` FOREIGN KEY (`emp_id`) REFERENCES `employee` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Spring JPA example

Spring JPA example Entity classes

Entity classes that map to the DB tables.

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="account")
public class Account {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name="acct_id")
  private int id;
  @Column(name="acct_no", unique=true)
  private String accountNumber;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "emp_id", nullable = false)
  private Employee employee;

  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getAccountNumber() {
    return accountNumber;
  }
  public void setAccountNumber(String accountNumber) {
    this.accountNumber = accountNumber;
  }
  public Employee getEmployee() {
    return employee;
  }
  public void setEmployee(Employee employee) {
    this.employee = employee;
  }
}

Here are few points for the Account entity class that are worth mentioning-

  1. An employee can have many accounts that multiplicity is mapped in JPA using @ManyToOne annotation on the field (it can also be done on getter).
  2. Target entity (Employee in this case) can automatically be deduced from the type of the field.
  3. @JoinColumn annotation specifies a column for joining an entity association or element collection, which in this case is a foreign key column.
  4. If you want to make the foreign key column NOT NULL, you need to set the attribute to nullable = false. This is helpful if you are generating tables using Hibernate tools.
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="employee")
public class Employee {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name="id", nullable = false)
  private int id;
  @Column(name="first_name")
  private String firstName;
  @Column(name="last_name")
  private String lastName;
  @Column(name="department")
  private String dept;

  @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL)
  private Set<Account> accounts;

  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getFirstName() {
    return firstName;
  }
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
  public String getLastName() {
    return lastName;
  }
  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
  public String getDept() {
    return dept;
  }
  public void setDept(String dept) {
    this.dept = dept;
  }
  public Set<Account> getAccounts() {
    return accounts;
  }
  public void setAccounts(Set<Account> accounts) {
    this.accounts = accounts;
  }
  @Override
  public String toString() {
    return "Id= " + getId() + " First Name= " + 
          getFirstName() + " Last Name= " + getLastName() + 
          " Dept= "+ getDept();
  }
}

Here are few points for the Employee entity class that are worth mentioning-

  1. An employee can have many accounts to accommodate for that a Set reference is added that can store accounts.
  2. @OneToMany annotation defines a many-valued association.
  3. If the relationship is bidirectional, the mappedBy element must be used to specify the relationship field or property of the entity that is the owner of the relationship.
  4. Using CascadeType you can specify the operations that are propagated to the associated entity. CascadeType.ALL cascades all operations (PERSIST, REMOVE, REFRESH, MERGE, DETACH)

Bi-Directional association

Though in this example bi-directional association is used but it may not suit every requirement.

Bi-directional association makes it convenient to fetch the associated collection without explicitly writing any query but at the same time the object graph may be quite huge and complex and fetching it may slow down the whole application.

Even if you fetch the whole association Hibernate may not be fetching the child association because of the lazy loading where the content of the collection are fetched only when you try to access them. That may lead to LazyInitializationException if you try to access collection elements when the session is already closed.

So bottom line is many scenarios it is better to go with uni-directional association (only @ManyToOne side).

DAO Classes

public interface EmployeeDAO {
  public void addEmployee(Employee emp);
  public List<Employee> findAllEmployees();
  public Employee findEmployeeById(int id);
  public void deleteEmployeeById(int id);
}
@Repository
public class EmployeeDAOImpl implements EmployeeDAO {
  @PersistenceContext
  private EntityManager em;
  @Override
  public void addEmployee(Employee emp) {
    em.persist(emp);
  }

  @Override
  public List<Employee> findAllEmployees() {
    List<Employee> employees = em.createQuery("Select emp from Employee emp", Employee.class)
                     .getResultList();
    return employees;
  }

  @Override
  public Employee findEmployeeById(int id) {
    //Employee emp = em.find(Employee.class, id);
    Employee emp = em.createQuery("SELECT e FROM Employee e INNER JOIN e.accounts a where e.id = :id", Employee.class)
         .setParameter("id", id)
         .getSingleResult();
    return emp;
  }
	
  @Override
  public void deleteEmployeeById(int id) {
    Employee emp = findEmployeeById(id);
    em.remove(emp);
  }
}

Notice that @Repository annotation is used on the EmployeeDAOImpl class that makes it a component and eligible for registering as a Spring bean when component scanning is done.

Service Class

import java.util.List;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.knpcode.springproject.dao.EmployeeDAO;
import com.knpcode.springproject.model.Account;
import com.knpcode.springproject.model.Employee;

@Service
public class EmployeeService {
  @Autowired
  private EmployeeDAO dao;

  @Transactional
  public Employee getEmployeeById(int id) {
    Employee emp = dao.findEmployeeById(id);
    emp.getAccounts();
    System.out.println(emp.toString());
    for(Account acct: emp.getAccounts()) {
      System.out.println("Acct No- " + acct.getAccountNumber());
    }
    return emp;
  }

  @Transactional
  public List<Employee> getAllEmployees(){
    return (List<Employee>) dao.findAllEmployees();
  }

  @Transactional
  public void addEmployee(Employee emp) {
    dao.addEmployee(emp);
  }

  @Transactional
  public void deleteEmployeeById(int id) {
    dao.deleteEmployeeById(id);
  }
}

EmployeeService has a dependency on EmployeeDAO which is satisified using @Autowired annotation. From the service class methods in DAO are called.

Configuration class

In this Spring data JPA example Java configuration is used so class is annotated with @Configuration annotation.

For setting up DataSource DB properties are read from a properties file, path for the properties file (config/db.properties) is configured using @PropertySource annotation.

@EnableTransactionManagement annotation enables Spring's annotation-driven transaction management capability.

@ComponentScan annotation enables component scanning, using the path provided as base package.

With in this Java config class we set up a EntityManagerFactory and use Hibernate as persistence provider. Using the method setPackagesToScan path to the package where entity class resides is provided, by doing that persistence.xml config file is not required.

import java.util.Properties;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.knpcode.springproject")
@PropertySource("classpath:config/db.properties")
public class AppConfig {
  @Autowired
  private Environment env;
  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setJpaVendorAdapter(vendorAdapter);
    // Where Entity classes reside
    factory.setPackagesToScan("com.knpcode.springproject.model");
    factory.setDataSource(dataSource());
    factory.setJpaProperties(hibernateProperties());
    return factory;
  }

  @Bean
  public DataSource dataSource() {
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName(env.getProperty("db.driverClassName"));
    ds.setUrl(env.getProperty("db.url"));
    ds.setUsername(env.getProperty("db.username"));
    ds.setPassword(env.getProperty("db.password"));
    return ds;
  }

  Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.setProperty("hibernate.dialect", env.getProperty("hibernate.sqldialect"));
    properties.setProperty("hibernate.show_sql", env.getProperty("hibernate.showsql"));
    return properties;
  }
  @Bean
  public PlatformTransactionManager transactionManager() {
    JpaTransactionManager txManager = new JpaTransactionManager();
    txManager.setEntityManagerFactory(entityManagerFactory().getObject());
    return txManager;
  }
}
config/db.properties
db.driverClassName=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/knpcode
db.username=root
db.password=admin
hibernate.sqldialect=org.hibernate.dialect.MySQLDialect
hibernate.showsql=true

Spring JPA example test

For running our Spring ORM JPA Hibernate example you can use the following test program which adds a new employee and associated accounts.

import java.util.HashSet;
import java.util.Set;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import com.knpcode.springproject.model.Account;
import com.knpcode.springproject.model.Employee;
import com.knpcode.springproject.service.EmployeeService;

public class App {
  public static void main( String[] args ){
    AbstractApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    EmployeeService empService =  context.getBean("employeeService", EmployeeService.class);
    Employee emp = new Employee();
    emp.setFirstName("Jack");
    emp.setLastName("Cullinan");
    emp.setDept("Finance");
    Account acct1 = new Account();
    acct1.setAccountNumber("123yur34");
    acct1.setEmployee(emp);
    Account acct2 = new Account();
    acct2.setAccountNumber("123yur35");
    acct2.setEmployee(emp);
    Set<Account> accounts = new HashSet<Account>();
    accounts.add(acct1);
    accounts.add(acct2);
    emp.setAccounts(accounts);
    empService.addEmployee(emp);
    //Employee employee = empService.getEmployeeById(9);
    context.close();
  }
}

For saving the entities following Hibernate queries are triggered.

Hibernate: insert into employee (department, first_name, last_name) values (?, ?, ?)
Hibernate: insert into account (acct_no, emp_id) values (?, ?)
Hibernate: insert into account (acct_no, emp_id) values (?, ?)

For getting employee by Id.

public class App {
  public static void main( String[] args ){
    //EntityManager
    AbstractApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    EmployeeService empService =  context.getBean("employeeService", EmployeeService.class);   
    Employee employee = empService.getEmployeeById(10);
    //empService.deleteEmployeeById(5);
    context.close();
  }
}

From the logs you can see that first select query gets only employee information not the associated accounts because of lazy loading. Only when the account information is accessed the select queries for getting the accounts are triggered.

Hibernate: select employee0_.id as id1_1_, employee0_.department as departme2_1_, employee0_.first_name as first_na3_1_, employee0_.last_name as last_nam4_1_ from employee employee0_ inner join account accounts1_ on employee0_.id=accounts1_.emp_id where employee0_.id=?

Id= 10 First Name= Jack Last Name= Cullinan Dept= Finance

Hibernate: select accounts0_.emp_id as emp_id3_0_0_, accounts0_.acct_id as acct_id1_0_0_, accounts0_.acct_id as acct_id1_0_1_, accounts0_.acct_no as acct_no2_0_1_, accounts0_.emp_id as emp_id3_0_1_ from account accounts0_ where accounts0_.emp_id=?

Acct No- 123yur34
Acct No- 123yur35

Also notice that displaying information for accounts is put in EmployeeService as that’s where the transaction ends. If you will try to access accounts information after session ends then you will get LazyInitializationException. That is one of the drawback of using bi-directional associations.

Check this post Fix LazyInitializationException: could not initialize proxy Error to see better ways to fix this error.

Exception in thread "main" org.hibernate.LazyInitializationException: 
failed to lazily initialize a collection of role: com.knpcode.springproject.model.Employee.accounts, could not initialize proxy - no Session

That's all for the topic Spring + JPA (Hibernate) OneToMany Example. If something is missing or you have something to share about the topic please write a comment.


You may also like

No comments:

Post a Comment