Support associations delete-orphan on saveOrUpdate()

Description

I think saveOrUpdate() behaves somewhat unexpected for associations with delete-orphan cascading style:

Prerequisite: Association is set to cascade="all,delete-orphan" and nullable=true

1) Within a single session (i.e. a transient object):

  • Load parent

  • Delete child

  • saveOrUpdate()
    Result: Child is deleted from DB, a DELETE statement was issued

2) Across multiple session (i.e. a detached object):

  • Have parent with valid ID, but empty list/set of children

  • saveOrUpdate()
    Result: Child is not deleted from DB, an UPDATE statement is issued which sets FK of child to 'null'

3) like 2) but nullable is FALSE
Result: Nothing happens.

With merge() everything works as expected for detached and transient instances. I would expect saveOrUpdate() behave similar to merge().

Attachments

2
  • 12 Dec 2013, 10:30 AM
  • 12 Dec 2013, 10:22 AM

Activity

Show:

Stephan van Hugten December 12, 2013 at 10:21 AM

I might have a possible solution which passes the unit test in HHH-5267. It looks up the database state and puts the child association value back onto the loaded state. This will trigger a delete action in the method and an entity update action when flushing. See below and in the attached code.

Version 4.2.7.SP1, starting on line 262

Cascade.java

// orphaned if the association was nulled (child == null) or receives a new value while the // entity is managed (without first nulling and manually flushing). if ( child == null || ( loadedValue != null && child != loadedValue ) ) { // Begin patch HHH-3795 if (loadedValue == null) { loadedValue = entry.getDatabaseValue((SessionImplementor) eventSource, propertyName, parent); } final EntityEntry valueEntry = eventSource.getPersistenceContext().getEntry( loadedValue ); // Need to check this in case the context has // already been flushed. See HHH-7829. if ( valueEntry != null ) { final String entityName = valueEntry.getPersister().getEntityName(); if ( LOG.isTraceEnabled() ) { final Serializable id = valueEntry.getPersister().getIdentifier( loadedValue, eventSource ); final String description = MessageHelper.infoString( entityName, id ); LOG.tracev( "Deleting orphaned entity instance: {0}", description ); } if (type.isAssociationType() && ((AssociationType)type).getForeignKeyDirection().equals( ForeignKeyDirection.FOREIGN_KEY_TO_PARENT )) { // If FK direction is to-parent, we must remove the orphan *before* the queued update(s) // occur. Otherwise, replacing the association on a managed entity, without manually // nulling and flushing, causes FK constraint violations. eventSource.removeOrphanBeforeUpdates( entityName, loadedValue ); } else { // Else, we must delete after the updates. eventSource.delete( entityName, loadedValue, isCascadeDeleteEnabled, new HashSet() ); } } // End patch }

Version 4.2.7.SP1, starting on line 296

Cascade.java

// Begin patch HHH-3795 public Object getDatabaseValue(SessionImplementor session, String propertyName, Object owner) { int propertyIndex = ( (UniqueKeyLoadable) persister ).getPropertyIndex(propertyName); Object[] databaseState = getPersister().getDatabaseSnapshot(id, session); if (databaseState == null) { return null; } Object result = getPersister().getPropertyType(propertyName).assemble( (Serializable) databaseState[propertyIndex], session, owner); // Place it back on the loaded state to force an update of the owner during flush loadedState[propertyIndex] = result; return result; } // End patch

Stephan van Hugten November 2, 2012 at 10:49 AM

How is this expected to work when having a bi-directional relation? If you only delete the orphan you will trigger the foreign key constraint.

Steve Ebersole November 1, 2012 at 5:00 PM

With reattachment the only valid solution is to generally (re)obtain the loaded state. This might happen in a number of ways:

  • The enhancement option works if we have the entity track its state. This is already true in the new enhancement code I am working on. The next piece there would be to have that state become part of the entity's serialized state as well (currently I have all the enhanced additions set to be transient to not muck with the managed class's serial signature)

  • (re)load the loaded state. Part of that could be to peek into the second level cache as a first option, before physically querying the database. Personally, I'd like to see select-before-update control this; or maybe we default select-before-update to true for entities which contain one-to-one associations with orphan-removal enabled. But even then, currently select-before-update is too late in being applied to help here, so we'd then also need to add the ability for that loading to be triggered here.

Brett Meyer October 31, 2012 at 7:37 PM

As of 4.1.x, this is still true. saveOrUpdate currently has no way of checking the detatched orphan for deletion. The fix will involve either entity bytecode enhancement or querying the DB for the orphan.

Details

Assignee

Reporter

Priority

Created March 3, 2009 at 6:18 PM
Updated December 12, 2013 at 10:30 AM