Search code examples
hibernategrailsgrails-orm

Grails/GORM: avoiding ConcurrentModificationException on nested save calls


I'm working with a legacy database that has many quirks, and this is the latest snag in my attempt to build a data access library to work with Grails 2.5.6.

I have a User domain class that, rather than having its own table in the database, saves new records to a historical table via a UserHistory class. In the historical table, the latest record is indicated by a changeType property: there is exactly one record per user with a null changeType, and that's the latest. So in order to save a User object the current latest record must be updated with a non-null changeType and a new record must be inserted with changeType being null.

The problem I encounter is that when I save the User object, it throws a ConcurrentModificationException, apparently when going through the actions registered for the flush. More details below.

Here is an outline of the two classes in question:

// User.groovy
class User {
  static mapping = { 
    id name: 'pidm', generator: 'assigned'
  }

  Long pidm
  String firstName

  @Lazy
  Set<UserHistory> histories = { UserHistory.findAllByPidm(pidm) }()

  def beforeUpdate()
  {
    // Mark the current UserHistory object with the appropriate change type
    UserHistory currentHistory = histories.find({ it.changeType == null })
    currentHistory.changeType = 'N'

    // Create a new UserHistory object with the changes applied
    UserHistory newHistory = new UserHistory(pidm: pidm, firstName: firstName)

    // Save the two UserHistory objects.
    currentHistory.save()
    newHistory.save(insert: true)

    // Return false so we don't try to save the User
    return false
  }
}

// UserHistory.groovy
class UserHistory {
  Long pidm
  String firstName
}

My integration test looks like:

// UserIntegrationSpec.groovy
void "test saving a name change"()
{
  when:
  def user = User.get(pidm)

  then:
  user.firstName == oldName

  when:
  def oldHistoryCount = user.histories.size()
  user.firstName = newName
  user.save() // throws exception
  def user2 = User.read(pidm)

  then:
  user2.firstName == newName
  user2.histories.size() == oldHistoryCount + 1

  where:
  pidm | oldName | newName
  123  | "David" | "Barry"
}

An exception is thrown after beforeUpdate() executes, but before user.save() finishes:

Failure:  |
test saving a name change(UserIntegrationSpec)
 |
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1042)
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:463)
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:351)
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:350)
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:56)
    at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1258)
    at org.codehaus.groovy.grails.orm.hibernate.metaclass.SavePersistentMethod.flushSession(SavePersistentMethod.java:87)
    at org.codehaus.groovy.grails.orm.hibernate.metaclass.SavePersistentMethod$1.doInHibernate(SavePersistentMethod.java:60)
    at org.codehaus.groovy.grails.orm.hibernate.GrailsHibernateTemplate.doExecute(GrailsHibernateTemplate.java:188)
    at org.codehaus.groovy.grails.orm.hibernate.GrailsHibernateTemplate.execute(GrailsHibernateTemplate.java:132)
    at org.codehaus.groovy.grails.orm.hibernate.metaclass.SavePersistentMethod.performSave(SavePersistentMethod.java:56)
    at org.codehaus.groovy.grails.orm.hibernate.metaclass.AbstractSavePersistentMethod.doInvokeInternal(AbstractSavePersistentMethod.java:215)
    at org.codehaus.groovy.grails.orm.hibernate.metaclass.AbstractDynamicPersistentMethod.invoke(AbstractDynamicPersistentMethod.java:69)
    at org.codehaus.groovy.grails.orm.hibernate.HibernateGormInstanceApi.save(HibernateGormInstanceApi.groovy:196)
    at UserIntegrationSpec.test saving a name change(UserIntegrationSpec.groovy:43)

I have tried every permutation of the flush parameter on the three calls to save(). This either makes no difference or else defers the persisting such that the assert statement checking the history length fails. I've also tried manually flushing the session with User.withSession { Session session -> session.flush() }; this throws the same ConcurrentModificationException.

Is there something I'm missing? How can I implement what I'm trying to do? Or is there another approach someone can suggest?


Solution

  • I managed to resolve this by doing the following:

    • Add newHistory to the User's collection of histories
    • Save the two histories with flush: false
    • Save the user with flush: true

    So it seems that the combination of not flushing the nested saves and flushing the outer save was the right one, but my test case was still failing because it was looking at the cached collection and not refetching it from the database.

    Here is the updated beforeUpdate method:

    def beforeUpdate()
    {
      // Mark the current UserHistory object with the appropriate change type
      UserHistory currentHistory = histories.find({ it.changeType == null })
      currentHistory.changeType = 'N'
    
      // Create a new UserHistory object with the changes applied
      UserHistory newHistory = new UserHistory(pidm: pidm, firstName: firstName)
      histories.add(newHistory) // this line is the key to the solution
    
      // Save the two UserHistory objects.
      currentHistory.save(flush: false)
      newHistory.save(flush: false, insert: true)
    
      // Return false so we don't try to save the User
      return false
    }
    

    Now both my tests and my code are working properly. (Well, in this area at least)