When we work with data, we often have relationships between two or more entities. When two entities are related to each other, changes to one entity can impact the other.

For example, suppose we have two entities, one that stores customer data, and one that stores orders from customers. In this example, one customer can have many orders, so there is a one-to-many relationship. In this case, updating and deleting records from a customer could impact the orders’ entity. In turn, changes to orders could impact the customer. To help you handle these relationships, we will look at various¬†cascade operations¬†provided by JPA.

JPA entity states

Before we discuss cascade operations, we need to have an understanding of JPA entity states and their management. When an entity object is first created, it can be placed in a number of different states based on the application requirements. Each of these states plays an important role in the process of data management with JPA. When an entity is first initialized using the new keyword, it is placed in the transient state. If we want the entity to persist in our application, we need to place it in a persistent state. To achieve this, we will need to use an EntityManager object.

Once an entity is set to persistent, it can transition to a number of different states. The detached state can be used to stop an object from being managed by the EntityManager. This can be done explicitly with the help of the detach method. An entity can also be placed in a removed state, which means that it is removed from the persistent database. This is done using the remove method.

One additional important JPA method for state transition is known as the merge method. The merge method is used to attach a previously detached entity to an EntityManager.

Having learned about these JPA states, we can start looking at how cascade operations interact with them.

JPA cascade operations

There is a wide variety of JPA cascade operation types available for use. We will take a look at the following operations in detail:

  • ALL
  • PERSIST
  • MERGE
  • REMOVE
  • REFRESH
  • DETACH

The cascade type ALL is designed to propagate all operations from a parent entity to a child entity. Returning to our example with customers and orders, we can consider customers to be a parent entity, and orders to be a child entity. This is because every customer could potentially have several orders, which means that the customer is the parent of the orders. In this example, we define the entities as shown below:

@Entity
@Table(name = "customers")
public class Customer {
    @Id
    @Column(name = "customer_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long customerId;

    @Column(name = "customer_name")
    private String customerName;

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();

    //getters, setters, toString and constructors

}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @Column(name = "order_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    @Column(name = "order_cost")
    private double orderCost;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    //getters, setters, toString and constructors

}

Note that when using the @Entity annotation, you will need to include the org.springframework.boot:spring-boot-starter-data-jpa dependency in your project.

If we want to make the operations committed to the Customer entity apply to the Order entity, we can add a cascade option to the @OneToMany annotation. In the example above, we added it to Customer, since it is the parent. We then specified that Order was @ManyToOne, joining it to the Customer entity through the customer_id field.

With this cascade added, we now have a way to apply operations on the Customer entity to the Order entities. For example, if you were to delete a customer with the cascade type ALL, it would cause all orders related to the deleted customer to also be deleted. To understand other cascade operations better, we will need to discuss the idea of entity states in JPA.

The other cascade operations we have available can be applied to specific operations, allowing us to be more specific about when exactly operations should cascade. The cascade type PERSIST, for example, can be used to persist a child entity when a parent entity is persisted. In our example with customers and orders, this would mean that if the persist method of EntityManager was run on customers, it would also be run on orders.

The cascade types MERGEREMOVEDETACH, and REFRESH will follow a similar pattern to PERSIST, and each of these types corresponds to the respective method of EntityManager. The MERGE operation cascades with mergeREMOVE cascades with removeDETACH cascades with detach, and REFRESH cascades with refresh.

Although our examples primarily demonstrate these concepts using the EntityManager, it is important to note that all of these operations are also provided by Spring through the implementations of the CrudRepository and JpaRepository interfaces.

As an example, we can return back to our customer and order entities. Suppose that we wanted to ensure that we cascade the REMOVE operation to the orders of a customer. We can define our cascade type as CascadeType.REMOVE.

If we have a situation in which a customer is deleted, the operation will cascade to all related orders. One interesting consideration for the REMOVE cascade is the idea of orphan removal. We can set the property orphanRemoval=true with our REMOVE cascade type. With orphanRemoval set to true, we ensure that if the link between a parent and child is broken, the child is removed. This is best used for situations in which the child entity should not exist without a parent object referencing it. Here is an example of setting this option:

@Entity
@Table(name = "customers")
public class Customer {
    @Id
    @Column(name = "customer_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long customerId;

    @Column(name = "customer_name")
    private String customerName;

    @OneToMany(mappedBy = "customer", cascade = CascadeType.REMOVE, orphanRemoval=true)
    private List<Order> orders = new ArrayList<>();

    //getters, setters, toString and constructors
}

Using cascade operations

Let’s now take a look at a full example of cascading a query with an EntityManager. For this example, we will set the cascade type to CascadeType.ALL so that all operations are cascaded.

@Entity
@Table(name = "customers")
public class Customer {
    @Id
    @Column(name = "customer_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long customerId;

    @Column(name = "customer_name")
    private String customerName;

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();

    //getters, setters, toString and constructors
}

You can initialize an EntityManager object using EntityManagerFactory. Once your EntityManager is initialized, you can use its methods on entities as shown below.

@Service
public class SampleService {

    @Autowired
    EntityManagerFactory entityManagerFactory;

    public void saveCustomer() {

        Customer customer = new Customer();
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        entityManager.persist(customer);

    }
}

From here, we will get a transaction, and create a customer and order object to work with. Once this is done, we can persist the objects, commit the changes, and view the resulting queries from these operations:

public void saveCustomer() {
    EntityManager entityManager = EntityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();

    Customer cust = new Customer();
    cust.setCustomerName("name");
        
    Order order = new Order();
    order.setOrderCost(4.52);
    order.setCustomer(cust);

    cust.setOrders(List.of(order));

    entityManager.persist(cust);
    entityManager.getTransaction().commit();
}

When this is run, we can see that an insert query occurs on both the customers and orders table. This is because we are cascading the insert from Customer to the Order object as well.

Hibernate: insert into customers (customer_name) values (?)
Hibernate: insert into orders (customer_id, order_cost) values (?, ?)

With this, we now have two objects that are related in such a way that queries cascade as required.

Conclusion

When we use JPA entities, we can consider the lifecycle of each instance of the entity object. When we work through the lifecycle of a given entity, we need to make sure that parent changes carry appropriately to children entities. Using cascade operations, it is possible for us to apply various operations to both parent and child entities in a relationship.

Leave a Reply

Your email address will not be published.