Grails/GORM:避免在嵌套保存调用时出现 ConcurrentModificationException

Grails/GORM: avoiding ConcurrentModificationException on nested save calls

我正在使用一个有很多怪癖的遗留数据库,这是我尝试构建一个数据访问库以使用 Grails 2.5.6 的最新障碍。

我有一个 User 域 class,它不是在数据库中有自己的 table,而是通过 UserHistory class。在历史table中,最新记录由changeType 属性表示:每个用户只有一条记录为空changeType,这是最新的。因此,为了保存 User 对象,必须使用非空 changeType 更新当前最新记录,并且必须插入新记录 changeType 为空。

我遇到的问题是,当我保存 User 对象时,它抛出一个 ConcurrentModificationException,显然是在执行为刷新注册的操作时。更多详情如下。

这里是有问题的两个 classes 的概要:

// 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
}

我的集成测试如下所示:

// 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"
}

beforeUpdate() 执行之后但在 user.save() 完成之前抛出异常:

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.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.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)

我已经尝试了对 save() 的三个调用中 flush 参数的所有排列。这要么没有区别,要么推迟持久化,以至于检查历史长度的 assert 语句失败。我还尝试使用 User.withSession { Session session -> session.flush() } 手动刷新会话;这抛出相同的 ConcurrentModificationException.

有什么我想念的吗?我怎样才能实现我想做的事情?或者有人可以建议另一种方法吗?

我通过执行以下操作设法解决了这个问题:

  • newHistory 添加到 User 的 collection 历史记录中
  • flush: false
  • 保存两条历史记录
  • flush: true
  • 保存用户

所以看起来不刷新嵌套保存和刷新外部保存的组合是正确的,但我的测试用例仍然失败,因为它正在查看缓存的 collection 而不是重新获取它来自数据库。

这是更新后的 beforeUpdate 方法:

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
}

现在我的测试和代码都可以正常工作了。 (好吧,至少在这方面)