Relationship cascading in JPA

on waitingforcode.com

Relationship cascading in JPA

One of reasons-to-be of relational databases are relations. JPA provides very flexible way of manipulating them, through cascade attribute of annotations responsible for related objects (database tables).

In this article we'll explore the possibilities given by JPA to manage relationships. The first part will explain the cascading concept and which types of cascading we can use in JPA. The second part will show what all of available type do in different entity lifecycle moments (removing, persisting, detaching etc.).

Cascade type in JPA

Let's explain the idea of cascading before talking about cascade types. According to Oxford dictionaries definition, cascade means:

A succession of devices or stages in a process, each of which triggers or initiates the next.

This definition can be applied to JPA and its relationship mapping through @ManyToOne, @OneToMany, @ManyToMany and @OneToOne annotations. To explain simply, cascade in JPA makes that one operation, applied natively on one entity, can be dispatched on its associated entities. For example: imagine that you have one Product entity associated with another entity - ProductCategory. Now, when you define new Product with associated ProductCategory (also newly created) and call EntityManager's persist() method on Product, both entities will be saved in the database (according to chosen cascade type) with this single call.

As you could see in the previous paragraph, entities can be saved with one call according to chosen cascade type. Cascade types in JPA depend on EntityManager actions, as removing, detaching or persisting of entities. This table resumes all of available cascade types:

Cascade type Description
PERSIST Mapped entities will be also making persist after calling of persist() method.
MERGE Mapped entities will be also merged after calling of merge() method.
REFRESH Mapped entities will be also refreshed after calling of refresh() method.
REMOVE Mapped entities will be also removed after calling of remove() method.
DETACH Mapped entities will be also detached after calling of detach() method.
ALL This type gathers together all previously described types. So, it applies persisting, merging, refreshing, removing and detaching on mapped entities.
All cascade types are included in javax.persistence.CascadeType enum.

Example of cascade in JPA

For test cascade types we'll add cascade attribute on mapping annotations. Entities after this change look like:
// REMOVE and MERGE
public class Product {

  @ManyToOne(cascade = CascadeType.REMOVE)
  @JoinColumn(name = "category_id")
  public Category getCategory() {
      return this.category;
  }

  @ManyToOne(cascade = CascadeType.MERGE)
  @JoinColumn(name = "provider_id")
  public Provider getProvider() {
      return provider;
  }

}

// PERSIST and DETACH
public abstract class Order {
  @OneToOne(cascade = CascadeType.PERSIST)
  @JoinColumn(name = "shopping_cart_id")
  public ShoppingCart getShoppingCart() {
    return this.shoppingCart;
  }

  @ManyToOne(cascade = CascadeType.DETACH)
  @JoinColumn(name = "user_id")
  public User getCustomer() {
    return  this.customer;
  }
}

// REFRESH
public class ShoppingCart {

  @ManyToOne(cascade = CascadeType.REFRESH)
  @JoinColumn(name = "user_id")
  public User getCustomer() {
    return this.customer;
  }
}

// ALL
public class StoreAddress {
  @OneToMany(mappedBy = "storeAddress", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  public List<Store> getStores() {
    return this.stores;
  }
}

Now we can write some test cases to prove that cascade works well according to different scenarios. Each method tests separate CascadeType entry:

public class CascadingTest extends AbstractJpaTester {

  @Test
  public void testRemoveCascade() {
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    try {
      Category category = new Category();
      category.setName("Other category");
      category.setSeo(new Seo());
      category.getSeo().setDescription("desc Test");
      category.getSeo().setKeywords("keywords Test");
      category.getSeo().setTitle("title Test");

      Product product = new Product();
      product.setSeo(category.getSeo());
      product.setCategory(category);
      product.setName("other product name");
      product.setPrice(39.99d);
      product.setProvider(provider("test 1"));

      assertNull("Product shouldn't be persisted before persist() call", 
        product.getId());
      assertNull("Category shouldn't be persisted before persist() call",  
        category.getId());

      entityManager.persist(product.getProvider());
      entityManager.persist(category);
      entityManager.persist(product);

      assertTrue("Product should be persisted before persist() call",  
        product.getId() > 0);
      assertTrue("Category should be persisted before persist() call",  
        category.getId() > 0);

      entityManager.remove(product);
      entityManager.flush();

      Category categoryDb = entityManager.find(Category.class,  
        category.getId());
      Product productDb = entityManager.find(Product.class,  
        product.getId());
      assertNull("Because of REMOVE cascading, Category should be null because associated Product was removed previously",
              categoryDb);
      assertNull("Product should be removed by EntityManager",  
        productDb);
    } finally {
      transaction.rollback();
    }
  }

  @Test
  public void testPersistCascade() {
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    try {
      Order order = new OrderConfirmed();
      order.setCustomer(user("Test login"));
      order.setShoppingCart(shoppingCart(order.getCustomer()));

      assertNull("User's id should be null before persisting",  
        order.getCustomer().getId());
      assertNull("ShoppingCart's id should be null before persisting",  
        order.getShoppingCart().getId());

      entityManager.persist(order.getCustomer());
      entityManager.persist(order);
      // No need to call persist() separately for ShoppingCart - thanks to CascadeType.PERSITS, it'll persist automatically
      // when persist() is called for Order entity
      assertTrue("User should be correctly persisted",  
        order.getCustomer().getId() > 0);
      assertTrue("Order should be correctly persisted",  
        order.getId() > 0);
      assertTrue("ShoppingCart should be correctly persisted",  
        order.getShoppingCart().getId() > 0);
    } finally {
      transaction.rollback();
    }
  }

  @Test
  public void testMergeCascade() {
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    try {
      Category category = new Category();
      category.setName("Other category");
      category.setSeo(new Seo());
      category.getSeo().setDescription("desc Test");
      category.getSeo().setKeywords("keywords Test");
      category.getSeo().setTitle("title Test");

      Product product = new Product();
      product.setSeo(category.getSeo());
      product.setCategory(category);
      product.setName("other product name");
      product.setPrice(39.99d);
      product.setProvider(provider("test 1"));

      assertNull("Product shouldn't be persisted before persist() call",  
        product.getId());
      assertNull("Provider shouldn't be persisted before persist() call",  
        product.getProvider().getId());

      entityManager.persist(product.getProvider());
      entityManager.persist(category);
      entityManager.persist(product);

      assertTrue("Product should be persisted before persist() call",  
        product.getId() > 0);
      assertTrue("Provider should be persisted before persist() call",  
        product.getProvider().getId() > 0);

      // Make some changes on Product and Provider and call merge() after that
      product.setName("X");
      product.getProvider().setName("Y");

      entityManager.merge(product);

      Provider dbProvider = entityManager.find(Provider.class,  
        product.getProvider().getId());
      assertEquals("Provider's name should persist in database after merge() call " +
          "because of CascadeType.MERGE cascading attribute", "Y",  
        dbProvider.getName());

    } finally {
      transaction.rollback();
    }
  }

  @Test
  public void testRefreshCascade() {
    // Reminder about refresh() from Javadoc :
    // "Refresh the state of the instance from the database, overwriting changes made to the entity, if any"
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    try {
      User customer = user("Test login");
      ShoppingCart shoppingCart = shoppingCart(customer);

      assertNull("User's id should be null before persisting",  
        customer.getId());
      assertNull("shoppingCart's id should be null before persisting",  
        shoppingCart.getId());

      entityManager.persist(customer);
      entityManager.persist(shoppingCart);

      assertTrue("User should be correctly persisted",  
        customer.getId() > 0);
      assertTrue("shoppingCart should be correctly persisted",  
        shoppingCart.getId() > 0);

      // Make changes on ShoppingCart and User without persisting them , and after that, call a refresh()
      shoppingCart.setState(ShoppingCartState.NOT_CONFIRMED);
      customer.setLogin("Yet another login");

      assertEquals("Before refreshing, ShoppingCart state should be NOT_CONFIRMED",  
        ShoppingCartState.NOT_CONFIRMED, 
        shoppingCart.getState());
      assertEquals("Before refreshing, User login should be 'Yet another login", "Yet another login", 
        customer.getLogin());

      entityManager.refresh(shoppingCart);

      assertEquals("After refreshing, ShoppingCart state should be CONFIRMED",  
        ShoppingCartState.CONFIRMED, 
        shoppingCart.getState());
      assertEquals("After refreshing, User login should be 'Test login'", "Test login",  
        customer.getLogin());

    }  finally {
      transaction.rollback();
    }
  }

  @Test
  public void testDetachCascade() {
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    try {
        Order order = new OrderConfirmed();
        order.setCustomer(user("Test login"));
        order.setShoppingCart(shoppingCart(order.getCustomer()));

        entityManager.persist(order);
        fail("Order shouldn't be added without making User persistent before");
    } catch (PersistenceException pe) {
        // leave empty to not make test failing
    } finally {
        transaction.rollback();
    }

    // Retest again, but with persisting customer separately
    transaction.begin();
    try {
      Order order = new OrderConfirmed();
      order.setCustomer(user("Test login"));
      assertNull("User's id should be null before persisting",  
        order.getCustomer().getId());

      entityManager.persist(order.getCustomer());

      order.setShoppingCart(shoppingCart(order.getCustomer()));

      entityManager.persist(order);
      assertTrue("User should be correctly persisted",  
        order.getCustomer().getId() > 0);
      assertTrue("Order should be correctly persisted",  
        order.getId() > 0);

      // Now, detach Order - Customer should be detached too
      entityManager.detach(order);

      order.getCustomer().setLogin("X");
      entityManager.flush();

      User userFromDb = entityManager.find(User.class,  
        order.getCustomer().getId());

      assertEquals("Login of user in the database shouldn't be modified since cascading is of DETACHED type", 
        "Test login", userFromDb.getLogin());
      assertEquals("Login of detached user should be changed", "X",  
        order.getCustomer().getLogin());
    } finally {
      transaction.rollback();
    }

  }

  @Test
  public void testAllCascade() {
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    try {
      Store store = new Store();
      store.setName("Test store #1");
      StoreAddress address = new StoreAddress();
      address.setCityName("Paris");
      address.setPlace("Centre Commercial");
      address.setStreet("1, rue du Commerce");
      address.setZipCode("75002");
      store.setStoreAddress(address);

      List stores = new ArrayList<>();
      stores.add(store);
      address.setStores(stores);

      assertNull("Store's id should be null before persisting",  
        store.getId());
      assertNull("Store address's id should be null before persisting",  
        address.getId());

      entityManager.persist(address);
      assertTrue("Store address should be correctly persisted",  
        address.getId() > 0);
      assertTrue("Store should be correctly persisted",  
        store.getId() > 0);

      store.setName("X");
      address.setPlace("Y");
      entityManager.merge(store);

      assertEquals("Store new name should persist in database after merge()", "X",  
        store.getName());
      assertEquals("Address new name should persist in database too after merge() on Store " +
              "because of CascadeType.ALL property for cascade attribute", "Y",  
        address.getPlace());

      entityManager.remove(address);
      entityManager.flush();

      Store storeDb = entityManager.find(Store.class, store.getId());
      StoreAddress addressDb = entityManager.find(StoreAddress.class,  
        address.getId());

      assertNull("Because of ALL cascading, Store should be null because associated Address was removed previously",
              storeDb);
      assertNull("Address should be removed by EntityManager",  
        addressDb);
    }  finally {
      transaction.rollback();
    }
  }

  private Provider provider(String name) {
    Provider provider = new Provider();
    provider.setName("Test 1");
    return provider;
  }

  private User user(String login) {
    User user = new User();
    user.setLogin(login);
    user.setType(UserType.CUSTOMER);
    return user;
  }

  private ShoppingCart shoppingCart(User customer) {
    ShoppingCart cart = new ShoppingCart();
    cart.setCustomer(customer);
    cart.setCreationDate(new Date());
    cart.setState(ShoppingCartState.CONFIRMED);
    return cart;
  }

}

Through this article we could see how to configure cascade operations in JPA. The first part explains which cascade types are available for mapped relationships. It describes also the idea of cascade, bringing the definition from the real world. The second part proves by the code how CascadeType enum can be used in entities mapping.

Share on: