Let’s take a look at how we would want to configure the cascades and fetch strategies for the four entities (Customer, Order, Order Item, and Retail Item) from the example in the previous essay. Our goal is to create a persistence boundary around the aggregate containing the Order and Order Item entities. So when we retrieve an Order, we would like to see the Order Items retrieved as well, but not the Customer or the Retail Items. Similarly, when we create, update, or delete an Order, we want the Order Item changes to be persisted as well, but any changes to the Customer or Retail Items should not be persisted. These should be handled separately by the user with explicit calls into the CustomerRepo or RetailItemRepo.
For the retrieval side of things, we choose a fetch strategy for each association. An EAGER fetch strategy means we want JPA to load it right away, in the same database query. A LAZY fetch strategy means we don’t want JPA to load it just yet. JPA will give us a proxy object instead, and they will only be loaded from the database when the proxy object is actually used.
To do our best to draw a persistence boundary around our aggregate, we want the Order.orderItems to be eager fetch, and the other associations to be lazy. Here’s a snippet that shows how we set the fetch strategies:
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(fetch = FetchType.EAGER)
private List<OrderItem> orderItems;
}
public class OrderItem {
@ManyToOne(fetch = FetchType.LAZY)
private RetailItem retailItem;
}
The @ManyToOne, @OneToMany, and similar annotations all have default values for FetchType, and in our case, the defaults are not what we want. I find it helpful to always be explicit about the fetch strategy, and not rely on the defaults, to help provide clarity to readers of the code.
Similarly, we want creates, updates and deletes of an Order to trigger the corresponding action in the Order Items, but not the Customer or Retail Items. We accomplish this by configuring the cascades on the JPA entity relationships. When an Order is updated, we also want to delete any Order Item rows from the database that were removed from the Order. This is done by setting orphanRemoval to true. Here’s a snippet that focuses in our approach to cascades:
public class Order {
@ManyToOne(cascade = {})
private Customer customer;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems;
}
public class OrderItem {
@ManyToOne(cascade = {})
private RetailItem retailItem;
}
In real life, the cascade and fetch elements would exist side by side on the annotations. I’m only isolating them here for the sake of clarity. The cascade elements in the annotations take zero or more CascadeTypes, so if we want to be explicit about no cascades, we provide an empty array, such as with the empty curly braces in the above example.
We’ve done pretty well here, and obviated the need for any OrderItemRepo. Persistence operations on the Order are made through the OrderRepo, and these operations will handle all of the corresponding persistence operations for the Order Items. The following simple rules describe what we have done:
- Use lazy fetch and no cascades for associations that cross aggregate boundaries.
- Use eager fetch, CascadeType.ALL, and orphanRemoval = true for associations within aggregates.
I would highly recommend applying these two rules whenever possible. Unfortunately, it’s not always this easy. You may run into problems applying CascadeType.ALL too liberally when there are cycles in your object graphs, or even object graphs where there are two paths from the root down to the same child node. If this is the case for you, I would recommend you get together with your domain experts and revisit those parts of your model. Non-tree structures in your entity graph, particularly within a single aggregate, is a smell that indicates potential problems with your domain. If I were specifically modeling graphs, I would probably choose to have Edges and Nodes as their own aggregate roots, and relegate all graph traversal to the service layer.
I'm going to have to cut this one a little short, because the upcoming discussion on fetch strategies is quite lengthy. Using eager fetch uniformly within aggregates is also not always possible, as we will explore further in the next essay.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.