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?
I managed to resolve this by doing the following:
newHistory
to the User
's collection of historiesflush: false
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)