Cascade Types in JPA and Hibernate

Learn JPA Cascade Types and how they are related to Hibernate Cascade Types. Learn how to control the orphan removal with an example.

JPA Cascade Types

We learned about mapping associated entities in hibernate already in previous tutorials such as one-to-one mapping and one-to-many mappings. There we wanted to save the mapped entities whenever the relationship owner entity gets saved. To enable this behavior, we had used “CascadeType” attribute.

In this JPA Cascade Types tutorial, we will learn about various available options for configuring the cascading behavior via CascadeType.

1. How Cascading Works?

Before moving forward, let’s look at how this cascade-type attribute is defined in our code for more clear understanding. Take a scenario where an employee can have multiple accounts, but one account must be associated with only one employee.

Let’s create entities with the minimum information for the sake of clarity.

@Entity
@Table(name = "Employee")
public class EmployeeEntity implements Serializable {

    @Id
    @Column(name = "ID", unique = true, nullable = false)
    private Integer employeeId;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "EMPLOYEE_ID")
    private Set<AccountEntity> accounts;

    // Getters and setters omitted for brevity
}
@Entity
@Table(name = "Account")
public class AccountEntity implements Serializable {

    @Id
    @Column(name = "ID", unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Integer accountId;

    @Column(name = "ACC_NO", unique = false, nullable = false, length = 100)
    private String accountNumber;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "EMPLOYEE_ID")
    private EmployeeEntity employee;

    // Getters and setters omitted for brevity
}

Look at the bold line in the above source code for EmployeeEntity. It defines “cascade=CascadeType.ALL” and it essentially means that any change happened on EmployeeEntity must cascade to AccountEntity as well.

If we save an employee, then all associated accounts will also be saved into the database. If you delete an Employee, all the accounts related to that Employee must also be deleted. Simple enough !!

But what if we only want the cascading on save operations but not on delete operations? Then we need to specify it using the below code.

@OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
@JoinColumn(name = "EMPLOYEE_ID")
private Set<AccountEntity> accounts;

Now only when save() or persist() methods are called using Employee instance then the accounts will also be persisted. If any other method is called on the session, its effect will not cascade to the accounts.

2. JPA Cascade Types

The cascade types supported by the Java Persistence Architecture are as below:

  • CascadeType.PERSIST: Operations like save() or persist() cascade to related entities.
  • CascadeType.MERGE: Related entities are merged when the owning entity is merged.
  • CascadeType.REFRESH: Similar to merge, but for refresh() operations.
  • CascadeType.REMOVE: Deletes all associated entities when the owning entity is deleted.
  • CascadeType.DETACH: Detaches all related entities during “manual detach”.
  • CascadeType.ALL: Shorthand for all cascade operations listed above.

There is no default cascade type in JPA. By default, no operation is cascaded.

The cascade configuration option accepts an array of CascadeTypes; thus, to include only refreshes and merges in the cascade operation for a One-to-Many relationship as in our example, we might use the following:

@Entity
@Table(name = "Employee")
public class EmployeeEntity implements Serializable {
@Id
@Column(name = "ID", unique = true, nullable = false)
private Integer employeeId;

@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
@JoinColumn(name = "EMPLOYEE_ID")
private Set<AccountEntity> accounts;

// Getters and setters omitted for brevity
}

This configuration ensures that only persist() and merge() operations on an EmployeeEntity instance cascade to associated accounts.

3. CascadeType.REMOVE vs Orphan Removal

  • The orphanRemoval option was introduced in JPA 2.0. This provides a way to delete orphaned entities from the database.
  • While CascadeType.REMOVE is a way to delete a child entity or entities whenever the deletion of its parent happens.

For example, in the next example, we have coded the employee and account relationship.

Whenever we delete an employee, all his accounts will get deleted if we use the CascadeType.REMOVE. But if want that whenever we remove the relationship between an account and an employee, hibernate will check for the accounts in other references. If none is found, hibernate will delete the account since it’s an orphan.

@Entity
@Table(name = "Employee")
public class EmployeeEntity implements Serializable {
    @Id
    @Column(name = "ID", unique = true, nullable = false)
    private Integer employeeId;

    @OneToMany(orphanRemoval = true, mappedBy = "employee")
    private Set<AccountEntity> accounts;

    // Getters and setters omitted for brevity
}

4. Demo

Let’s understand with an example.

In our Employee and Account entity example, I have updated the code as below. We have mentioned “orphanRemoval = true” on accounts. It essentially means that whenever I will remove an ‘account from accounts set’ (which means I am removing the relationship between that account and Employee); the account entity which is not associated with any other Employee on the database (i.e. orphan) should also be deleted.

@Entity
@Table(name = "Employee")
public class EmployeeEntity implements Serializable
{
	private static final long serialVersionUID = -1798070786993154676L;
	@Id
	@Column(name = "ID", unique = true, nullable = false)
	private Integer           employeeId;
	@Column(name = "FIRST_NAME", unique = false, nullable = false, length = 100)
	private String            firstName;
	@Column(name = "LAST_NAME", unique = false, nullable = false, length = 100)
	private String            lastName;

	@OneToMany(orphanRemoval = true, mappedBy = "employee")
	private Set<AccountEntity> accounts;

}
@Entity (name = "Account")
@Table(name = "Account")
public class AccountEntity implements Serializable
{
	private static final long serialVersionUID = 1L;
	@Id
	@Column(name = "ID", unique = true, nullable = false)
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Integer           accountId;
	@Column(name = "ACC_NO", unique = false, nullable = false, length = 100)
	private String            accountNumber;

	@ManyToOne
	private EmployeeEntity employee;
}

In the given example, we are creating one employee and three accounts. All three accounts belong to the employee. Then we delete the relationship between the employee and any one account, thus making the account an orphan.

As soon as we delete the employee-account relationship, even though we didn’t remove the account entity, hibernate will delete the orphaned account from the database itself.

public class TestOrphanRemovalCascade
{
   public static void main(String[] args)
   {
      setupTestData();

      Session sessionOne = HibernateUtil.getSessionFactory().openSession();
      org.hibernate.Transaction tx = sessionOne.beginTransaction();

      //Load the employee in another session
      EmployeeEntity employee = (EmployeeEntity) sessionOne.load(EmployeeEntity.class, 1);
      //Verify there are 3 accounts
      System.out.println("Step 1 : " + employee.getAccounts().size());

      //Remove an account from first position of collection
      employee.getAccounts().remove(employee.getAccounts().iterator().next());

      //Verify there are 2 accounts in collection
      System.out.println("Step 2 : " + employee.getAccounts().size());

      tx.commit();
      sessionOne.close();

      //In another session check the actual data in database
      Session sessionTwo = HibernateUtil.getSessionFactory().openSession();
      sessionTwo.beginTransaction();

      EmployeeEntity employee1 = (EmployeeEntity) sessionTwo.load(EmployeeEntity.class, 1);
      //Verify there are 2 accounts now associated with Employee
      System.out.println("Step 3 : " + employee1.getAccounts().size());

      //Verify there are 2 accounts in Account table
      Query query = sessionTwo.createQuery("from Account a");
      @SuppressWarnings("unchecked")
      List<AccountEntity> accounts = query.list();
      System.out.println("Step 4 : " + accounts.size());

      sessionTwo.close();

      HibernateUtil.shutdown();
   }  

   private static void setupTestData(){
      Session session = HibernateUtil.getSessionFactory().openSession();
      session.beginTransaction();

      //Create Employee
      EmployeeEntity emp = new EmployeeEntity();
      emp.setEmployeeId(1);
      emp.setFirstName("Lokesh");
      emp.setLastName("Gupta");
      session.save(emp);

      //Create Account 1
      AccountEntity acc1 = new AccountEntity();
      acc1.setAccountId(1);
      acc1.setAccountNumber("11111111");
      acc1.setEmployee(emp);
      session.save(acc1);

      //Create Account 2
      AccountEntity acc2 = new AccountEntity();
      acc2.setAccountId(2);
      acc2.setAccountNumber("2222222");
      acc2.setEmployee(emp);
      session.save(acc2);

      //Create Account 3
      AccountEntity acc3 = new AccountEntity();
      acc3.setAccountId(3);
      acc3.setAccountNumber("33333333");
      acc3.setEmployee(emp);
      session.save(acc3);

      session.getTransaction().commit();
      session.close();
   }
}

The program output.

Hibernate: insert into Employee (FIRST_NAME, LAST_NAME, ID) values (?, ?, ?)
Hibernate: insert into Account (ACC_NO, employee_ID, ID) values (?, ?, ?)
Hibernate: insert into Account (ACC_NO, employee_ID, ID) values (?, ?, ?)
Hibernate: insert into Account (ACC_NO, employee_ID, ID) values (?, ?, ?)

Hibernate: select employeeen0_.ID as ID1_1_0_, employeeen0_.FIRST_NAME as FIRST_NA2_1_0_, employeeen0_.LAST_NAME as
LAST_NAM3_1_0_ from Employee employeeen0_ where employeeen0_.ID=?

Hibernate: select accounts0_.employee_ID as employee3_1_0_, accounts0_.ID as ID1_0_0_, accounts0_.ID as ID1_0_1_,
accounts0_.ACC_NO as ACC_NO2_0_1_, accounts0_.employee_ID as employee3_0_1_ from Account accounts0_ where accounts0_.employee_ID=?

Step 1 : 3
Step 2 : 2

Hibernate: delete from Account where ID=?

Hibernate: select employeeen0_.ID as ID1_1_0_, employeeen0_.FIRST_NAME as FIRST_NA2_1_0_, employeeen0_.LAST_NAME as
LAST_NAM3_1_0_ from Employee employeeen0_ where employeeen0_.ID=?

Hibernate: select accounts0_.employee_ID as employee3_1_0_, accounts0_.ID as ID1_0_0_, accounts0_.ID as ID1_0_1_,
accounts0_.ACC_NO as ACC_NO2_0_1_, accounts0_.employee_ID as employee3_0_1_ from Account accounts0_ where accounts0_.employee_ID=?

Step 3 : 2

Hibernate: select accountent0_.ID as ID1_0_, accountent0_.ACC_NO as ACC_NO2_0_, accountent0_.employee_ID as employee3_0_
from Account accountent0_

Step 4 : 2

Orphan removal is a very good way of removing the matching/mismatching items from a collection (i.e. many-to-one or one-to-many relationships). We just remove the item from the collection and hibernate take care of the rest of the things for us. It will check whether an entity is referenced from any place or not; If it is not then it will delete the entity from the database itself.

Happy Learning !!

Leave a Comment

  1. “@ManyToOne (mappedBy=”accounts” ”

    Is this OK? Cause jakarta-persistence-api-3.1.0 OneToMany @interface doesn’t support mappedBy !

    To send this, on this site there are a couple of rules contradicting this solution:
    https://medium.com/@rajibrath20/the-best-way-to-map-a-onetomany-relationship-with-jpa-and-hibernate-dbbf6dba00d3

    “Bidirectional relationships must follow these rules:

    The many side of @ManyToOne bidirectional relationships must not define the mappedBy element. The many side is always the owning side of the relationship.”

    Reply
  2. I have some questions :

    1) the relationship between Account and Employee is ‘1 to N’ , ans as per the rule Many side has to be the owner. which means 1 side has to have @mappedBy annotation. So EmployeeEntity.java should have @mappedBy annotation. in your first example, you have used @mappedBy in AccountEntity.java class

    2) why you have used @OneToOne in AccountEntity .java in your first example , the relationship is ‘1to N’

    Reply
  3. I feel AccountDetail is owner of relationship hence you should mention @JoinColumn(name=”employeeId”) etc. instead of EmployeeEntity which should have “mappedBy=accoutId” , @OneToMany(orphanRemoval = true, mappedBy = “employee”)
    private Set accounts; seems to be wrong, just check you set “employee” over Set of accounts.

    Reply
  4. Hello. There is an error in first example of AccountEntity.java:
    @OneToOne (mappedBy=”accounts”, fetch = FetchType.LAZY)
    private EmployeeEntity employee;

    it must be @ManyToOne

    Reply
  5. Hey I am facing one issue of datached entity. Scenario in addition to you example Account has OneToMany relationship with some other entity like addresses. I am fetching Employee entity and from its account list I want to delete specific account. I remove that account from list and save Employee. But while saving Employee I am getting detached entity error for addresses of deleted account.
    Do you know how to resolve this issue?

    Reply

Leave a Comment

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.