Cascading actions from Parent to Child in Hibernate

It’s long past time for me to discuss the cascade attribute in Hibernate’s Collection support. Let’s use the BeanShell to explore the behavior of a simple collection for various attribute values. For reference, here is my abbreviated hibernate mapping file with the cascade element shown in bold:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<hibernate-mapping>
<class name="hb.KeySet"
table="keysets">


<id name="id" ... unsaved-value="-1">
<generator class="identity"/>
</id>

<property name="name" ... />
<property name="info" ... />

<set name="keys" ...
cascade="save-update" >

<key column="set_id"/>
<many-to-many class="hb.Keyword"
column="key_id"
/>

</set>

</class>
</hibernate-mapping>

The primary BeanShell scripted object was extended by adding the findAll method:

1
2
3
4
Collection findAll( String className )
{

return session.find( "from " + className );
}

And now we’re ready to examine different values of the cascade element. We’ll skip none, as we’ve already seen that Hibernate performs a shallow save when cascade=”none”.

save-update: cascades save and update actions from the parent to the child.

Note that the id value of the child Keyword changes from the unsaved-value of -1 to 0 when the parent KeySet is changed. And that the delete action does not cascade to the child.

1
bsh % import hb.*;
bsh % source("bsh/Hibernate.bsh");
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print(hb.findAll("Keyword"));
[]
bsh % print(hb.findAll("KeySet"));
[]
bsh % ireland = new Keyword("Ireland");
bsh % country = new KeySet("Country");
bsh % country.add(ireland);
bsh % print( country );
{-1:Country(null)|[-1:Ireland(null)]}
bsh % hb.save( country );
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet",null);
bsh % hb.delete( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword") );
[0:Ireland(null)]
bsh % print( hb.findAll("KeySet") );
[]

delete: cascades the delete action from the parent to the child.

In this example, it is necessary to explicitly save both the parent and the child (observe the value of the child’s id field when the parent is saved first). The child Keyword is automatically deleted when the parent KeySet is saved.

1
bsh % import hb.*;
bsh % source("bsh/Hibernate.bsh");
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]
bsh % ireland = new Keyword("Ireland");
bsh % country = new KeySet("Country");
bsh % country.add(ireland);
bsh % hb.save(country);
bsh % print( country );
{0:Country(null)|[-1:Ireland(null)]}
bsh % hb.save(ireland);
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet",null);
bsh % hb.delete( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword") );
[]
bsh % print( hb.findAll("KeySet") );
[]
```  
> 
> This example shows why you need to use delete with care -- hibernate will attempt to delete children that are shared with existing parents.  
> 
> ``` shell
bsh % tx = hb.transaction();
bsh % ireland = new Keyword("Ireland");
bsh % country = new KeySet("Country");
bsh % uk = new KeySet("United Kingdom");
bsh % country.add( ireland );
bsh % uk.add( ireland );
bsh % hb.save( ireland );
bsh % hb.save( country );
bsh % hb.save( uk );
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet","Country");
bsh % hb.delete(country);
bsh % tx.commit();
// Error: // Uncaught Exception: Method Invocation tx.commit : at Line: 1
                               : in file: <unknown file> : tx .commit ( )  
> 
> Target exception: net.sf.hibernate.JDBCException: could not delete: [hb.Keyword#1]

all: all actions are cascaded from the parent to the child.

All is the union of save-update and delete. It has the benefits of both (save and delete actions are propagated).

1
bsh % import hb.*;
bsh % source ("bsh/Hibernate.bsh");
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]
bsh % ireland = new Keyword( "Ireland" );
bsh % country = new KeySet( "Country" );
bsh % country.add( ireland );
bsh % print( country );
{-1:Country(null)|[-1:Ireland(null)]}
bsh % hb.save( country );
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet",null );
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % hb.delete( country );
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]
bsh % hb.close();

And the downside of delete – hibernate will attempt to delete children that are shared with existing parents.

1
bsh % import hb.*;
bsh % source("bsh/Hibernate.bsh");
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]
bsh % ireland = new Keyword("Ireland");
bsh % country = new KeySet("Country");
bsh % uk = new KeySet("United Kingdom");
bsh % country.add( ireland );
bsh % uk.add( ireland );
bsh % hb.save( country );
bsh % hb.save( uk );
bsh % tx.commit();
bsh % tx = hb.transaction();
bsh % country = hb.findByName("KeySet","Country");
bsh % print( country );
{0:Country(null)|[0:Ireland(null)]}
bsh % hb.delete( country );
bsh % tx.commit();
// Error: // Uncaught Exception: Method Invocation tx.commit
                               : at Line: 1 : in file:  : tx .commit ( )
> 
> Target exception: net.sf.hibernate.HibernateException: Flush during cascade is
       dangerous - this might occur if an object was deleted and then re-saved
       by cascade (remove deleted object from associations)

Note that delete actions do not cascade to orphan children.

1
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print( hb.findAll("Keyword"));
[0:Ireland(null)]
bsh % print( hb.findAll("KeySet"));
[{0:Country(null)|[0:Ireland(null)]}]
bsh % ireland = hb.findByName("Keyword",null);
bsh % country = hb.findByName("KeySet",null);
bsh % country.remove( ireland );
bsh % hb.delete( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword") );
[0:Ireland(null)]
bsh % print( hb.findAll("KeySet") );
[]

all-delete-orphan: all actions are cascaded from the parent to the child, orphan children are deleted.

Child is removed from the parent. When the parent is saved [updated], the orphan child is deleted.

1
bsh % import hb.*;
bsh % source( "bsh/Hibernate.bsh" );
bsh % hb = Hibernate();
bsh % tx = hb.transaction();
bsh % print ( hb.findAll("Keyword") );
[0:Ireland(null)]
bsh % print ( hb.findAll("KeySet"));
[{2:Country(null)|[0:Ireland(null)]}]
bsh % country = hb.findByName("KeySet",null);
bsh % ireland = hb.findByName("Keyword",null);
bsh % country.remove(ireland);
bsh % hb.save( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[{2:Country(null)|[]}]

Child is removed from the parent. When the parent is deleted, the orphan child is deleted.

1
bsh % print( hb.findAll("Keyword") );
[1:Ireland(null)]
bsh % print( hb.findAll("KeySet") );
[{2:Country(null)|[1:Ireland(null)]}]
bsh % ireland = hb.findByName("Keyword",null);
bsh % country = hb.findByName("KeySet",null);
bsh % country.remove( ireland );
bsh % hb.delete( country );
bsh % tx.commit();
bsh % print( hb.findAll("Keyword"));
[]
bsh % print( hb.findAll("KeySet"));
[]

For my money, save-update is the winner here. If save-update-orphan-deletion (cascade save and update, delete orphans when necessary) existed, then that might be a strong second choice. This is a many-to-many relationship example, your mileage will vary for other relationships.

If you’d like to try these examples on your own, the cascading action example source is available. For the record, here’s my current Hibernate configuration.

Disclaimer: I don’t claim to be an expert on hibernate. Please send comments and corrections.