Cascading Save and Delete

To explan how cascading of save (inserts updates) and deletes work I am going to use the example of Address, Customer, Order and OrderDetail objects.

They related in a standard way and each of the relationships has the cascade ALL annotation so that save and delete cascade.


...
@Entity
class Address {

}

...
@Entity
class Customer {
  
  @ManyToOne(cascade=CascadeType.ALL)
  Address billingAddress;
  
  @ManyToOne(cascade=CascadeType.ALL)
  Address shippingAddress;
}

...
@Entity
class Order

  @ManyToOne(cascade=CascadeType.ALL)
  Customer customer;

  @OneToMany(cascade=CascadeType.ALL)
  List<OrderDetail> details;

  public Customer getCustomer() {
    return customer;
  }

  public void setCustomer(Customer customer) {
    this.customer = customer;
  }

  public List<OrderDetail> getDetails() {
    return details;
  }

  public void setDetails(List<OrderDetail> details) {
    this.details = details;
  }
...
}

...
@Entity
class OrderDetail {
...
  @ManyToOne
  Order order;

  public Order getOrder() {
    return order;
  }

  public void setOrder(Order order) {
    this.order = order;
  }
}



Save Cascading

Cascading Save is a recursive process that could be described as "walking the object graph". The general algorithm for save goes:

  • Prior to saving a bean we save its ManyToOne associations (And appropriate OneToOnes)
  • Then we save the bean itself
  • Then we save any OneToMany, ManyToMany (And appropriate OneToOnes)

Note: OneToOnes will occur prior and post saving the bean depending on whether the OneToOne owns the foreign key or not. If that doesn't make sense don't worry too much.


  Order order = ....

  // assume:
  // the order has a customer
  // the customer has a billing and shipping address
  // the order has details

  Ebean.save(order);

In this example the order is:

  • Ebean.save(order)...
  • Prior to saving the 'order' we save the 'customer'
  • ... this takes us to the customer
  • Prior to saving the 'customer' we save the billingAddress
  • SAVE billingAddress
  • ... billingAddress has no OneToMany's etc
  • Prior to saving the 'customer' we save the shippingAddress
  • SAVE shippingAddress
  • ... shippingAddress has no OneToMany's etc
  • SAVE customer
  • ... customer has no OneToMany's etc
  • SAVE order
  • SAVE order details

A "Relational" way of describing this is that we go "up the imported foreign keys", then save the bean, then go "down the exported foreign keys".

Perhaps another way of thinking about this is to take the example where every object is new and will need to be inserted.

Before we save the order we need the customer_id so we need to save customer first. BUT before we save customer we need the billing_address_id so we need to save billingAddress first. ... etc. At the other end, we need to save the order to get the order_id BEFORE we save the order details.

Delete Cascading

Delete cascading is very similar except the order is reversed.

  • Prior to deleting a bean we delete its OneToMany, ManyToMany (And appropriate OneToOnes) associations
  • Then we delete the bean itself
  • Then we delte any ManyToOne (And appropriate OneToOnes)

Batching Order

When we use batching there is another complication added to this. Specifically when saving Beans we queue them up according to 'depth' and then batch process them in depth order.

In this example we would end up with 4 depth levels.

  • Depth -2: All the billing addresses and shipping addresses
  • Depth -1: All the customers
  • Depth 0: All the orders
  • Depth 1: All the order details

These would then be batch processed using PreparedStatement batching in lowest depth first order.

All the billing and shipping addresses would be saved, then customers, then orders and lastly order details.

In general you could the reason why this additional queuing is required is because we need to delay the binding of the preparedStatement. Specifically when beans are inserted we are expected to not get back the id values until after executing the statements and we are expecting to use getGeneratedKeys to do this. What this means is that we can not bind dependant objects (as we don't have the foreign key values) until after the higher 'depth' statements have been executed and getGeneratedKeys has got the id values back for us.

In general if your Databases does not support getGeneratedKeys and you are not using an external IdGenerator then ORM batching of inserts is going to be much less valuable. From my perspective ORM batching is greatly enhanced by getGeneratedKeys as we really need those Id values back.

You can of course using UpdateSql and for many this will be a better approach for very large batch inserting as you then use simple scalar foreign key values instead of reference objects.

Introduction User Guide (pdf) Install/Configure Public JavaDoc Whitepapers
General Database Specific Byte Code Deployment Annotations Features
Top Bugs Top Enhancements
woResponse