In older versions of Hibernate, I can see the one-shot delete indicated in the manual. But newer versions no longer have this section. I'm not sure why. So, in this post, I take a look if it still works.
The one-shot delete section says:
Deleting collection elements one by one can sometimes be extremely inefficient. Hibernate knows not to do that in the case of an newly-empty collection (if you called
list.clear()
, for example). In this case, Hibernate will issue a singleDELETE
.Suppose you added a single element to a collection of size twenty and then remove two elements. Hibernate will issue one
INSERT
statement and twoDELETE
statements, unless the collection is a bag. This is certainly desirable.However, suppose that we remove eighteen elements, leaving two and then add thee new elements. There are two possible ways to proceed
- delete eighteen rows one by one and then insert three rows
- remove the whole collection in one SQL
DELETE
and insert all five current elements one by oneHibernate cannot know that the second option is probably quicker. It would probably be undesirable for Hibernate to be that intuitive as such behavior might confuse database triggers, etc.
Fortunately, you can force this behavior (i.e. the second strategy) at any time by discarding (i.e. dereferencing) the original collection and returning a newly instantiated collection with all the current elements.
One-shot-delete does not apply to collections mapped
inverse="true"
.
The inverse="true"
is for (Hibernate Mapping) XML. But in this post, we'll see how "one-shot delete" works in JPA (with Hibernate as the provider).
We will try different approaches and see which one will result to a one-shot delete.
- Bi-directional one-to-many
- Uni-directional one-to-many (with join table)
- Uni-directional one-to-many (with no join table)
- Uni-directional one-to-many (using
ElementCollection
)
We'll use a Cart
entity with many CartItem
s.
Bi-directional One-to-Many
For this, we have references from both sides.
@Entity public class Cart { ... @OneToMany(mappedBy="cart", cascade=ALL, orphanRemoval=true) Collection<OrderItem> items; } @Entity public class CartItem { ... @ManyToOne Cart cart; }
To test this, we insert one row to the table for Cart
, and three or more rows to the table for CartItem
. Then, we run the test.
public class CartTests { ... @Test public void testOneShotDelete() throws Exception { Cart cart = entityManager.find(Cart.class, 53L); for (CartItem item : cart.items) { item.cart = null; // remove reference to cart } cart.items.clear(); // as indicated in Hibernate manual entityManager.flush(); // just so SQL commands can be seen } }
The SQL commands shown had each item deleted individually (and not as a one-shot delete).
delete from CartItem where id=? delete from CartItem where id=? delete from CartItem where id=?
Discarding the original collection did not work either. It even caused an exception.
public class CartTests { ... @Test public void testOneShotDelete() throws Exception { Cart cart = entityManager.find(Cart.class, 53L); // remove reference to cart cart.items = new LinkedList<CartItem>(); // discard, and use new collection entityManager.flush(); // just so SQL commands can be seen } }
javax.persistence.PersistenceException: org.hibernate.HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: ….Cart.items
I tested this with Hibernate 4.3.11 and HSQL 2.3.2. If your results vary, please hit the comments.
Uni-directional One-to-Many (With Join Table)
For this, we make changes to the mapping. This causes a join table to be created.
@Entity public class Cart { ... @OneToMany(cascade=ALL) Collection<OrderItem> items; } @Entity public class CartItem { ... // no @ManyToOne Cart cart; }
Again, we insert one row to the table for Cart
, and three or more rows to the table for CartItem
. We also have to insert appropriate records to the join table (Cart_CartItem
). Then, we run the test.
public class CartTests { ... @Test public void testOneShotDelete() throws Exception { Cart cart = entityManager.find(Cart.class, 53L); cart.items.clear(); // as indicated in Hibernate manual entityManager.flush(); // just so SQL commands can be seen } }
The SQL commands shown had the associated rows in the join table deleted (with one command). But the rows in the table for CartItem
still exist (and did not get deleted).
delete from Cart_CartItem where cart_id=? // no delete commands for CartItem
Hmmm, not exactly what we want, since the rows in the table for CartItem
still exist.
Uni-directional One-to-Many (No Join Table)
Starting with JPA 2.0, the join table can be avoided in a uni-directional one-to-many by specifying a @JoinColumn
.
@Entity public class Cart { ... @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true) @JoinColumn(name="cart_id", updatable=false, nullable=false) Collection<OrderItem> items; } @Entity public class CartItem { ... // no @ManyToOne Cart cart; }
Again, we insert one row to the table for Cart
, and three or more rows to the table for CartItem
. Then, we run the test.
public class CartTests { ... @Test public void testOneShotDelete() throws Exception { Cart cart = entityManager.find(Cart.class, 53L); cart.items.clear(); // as indicated in Hibernate manual entityManager.flush(); // just so SQL commands can be seen } }
Discarding the original collection also did not work either. It also caused the same exception (as with bi-directional one-to-many).
javax.persistence.PersistenceException: org.hibernate.HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: ….Cart.items
Uni-directional One-to-Many (with ElementCollection
)
JPA 2.0 introduced @ElementCollection
. This allows one-to-many relationships to be established with the many-side being either @Basic
or @Embeddable
(i.e. not an @Entity
).
@Entity public class Cart { ... @ElementCollection // @OneToMany for basic and embeddables @CollectionTable(name="CartItem") // defaults to "Cart_items" if not overridden Collection<OrderItem> items; } @Embeddable // not an entity! public class CartItem { // no @Id // no @ManyToOne Cart cart; private String data; // just so that there are columns we can set }
Again, we insert one row to the table for Cart
, and three or more rows to the table for CartItem
. Then, we run the test.
public class CartTests { ... @Test public void testOneShotDelete() throws Exception { Cart cart = entityManager.find(Cart.class, 53L); cart.items.clear(); // as indicated in Hibernate manual entityManager.flush(); // just so SQL commands can be seen } }
Yey! The associated rows for CartItem
were deleted in one shot.
delete from CartItem where Cart_id=?
Closing Thoughts
One-shot delete occurs with uni-directional one-to-many using ElementCollection
(where the many-side is an embeddabled, and not an entity).
In the uni-directional one-to-many with join table scenario, deleting entries in a join table doesn't add much value.
I'm not sure why one-shot delete works (or why it works this way) in Hibernate. But I do have a guess. And that is the underlying JPA provider could not do a one-shot delete because it could not ensure that the many-side entity is not referenced by other entities. Unlike the ElementCollection
, the many-side is not an entity and cannot be referenced by other entities.
Now, this does not mean that you have to use ElementCollection
all the time. Perhaps the one-shot delete only applies to aggregate roots. In those cases, using Embeddable
and ElementCollection
might be appropriate for a collection of value objects that make up an aggregate. When the aggregate root is removed, then it would be good to see that the "child" objects should be removed as well (and in an efficient manner).
I wish there was a way in JPA to indicate that the child entities are privately owned and can be safely removed when the parent entity is removed (e.g. similar to @PrivateOwned
in EclipseLink). Let's see if it will be included in a future version of the API.
Hope this helps.
No comments:
Post a Comment