Grails save() 尝试在应该更新时创建新对象

Grails save() tries to create new object when it should update

在我的服务代码中,我试图创建或更新一个 Person 域对象:

@Transactional
    def someServiceMethod(some params....) {
        try{
            def person = Person.findByEmail(nperson.email.toLowerCase())
            if (!person) {
                person = new Person()
                person.properties = nperson.properties

            } else {
                // update the person parameters (first/last name)
                person.firstName = nperson.firstName
                person.lastName = nperson.lastName
                person.phone = nperson.phone
            }

            if (person.validate()) {
                person.save(flush: true)
                //... rest of code
            }
            // rest of other code....
           } catch(e) {
                log.error("Unknown error: ${e.getMessage()}", e)
                e.printStackTrace()
                return(null)
    }

现在上面的代码偶尔会在尝试使用现有电子邮件保存 Person 对象时抛出以下异常:

Hibernate operation: could not execute statement; SQL [n/a]; Duplicate entry 'someemail@gmail.com' for key 'email_UNIQUE'; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'someemail@gmail.com' for key 'email_UNIQUE'

这很奇怪,因为我已经通过电子邮件找到了那个人,因此 save() 应该尝试更新记录而不是创建新记录。

我想知道为什么会这样!

编辑:

我使用的是 grails 2.4.5,BuildConfig 中的 Hibernate 插件是: runtime ':hibernate4:4.3.8.1'

编辑 2:

我的应用程序在多个服务器上,因此同步块将不起作用

如果这是并发问题,这就是我们在这种情况下所做的。我们有很多并发后台进程在相同的 tables 上工作。如果有这样的操作,它确实在同步块中,所以代码可能如下所示:

class SomeService {
    static transactional = false //service cannot be transactional

    private Object someLock = new Object() //synchronized block on some object must be used

    def someConcurrentSafeMethod(){
        synchronized(someLock){
            def person = Person.findByEmail(nperson.email.toLowerCase())
            ...
            person.save(flush: true) // flush is very important, must be done in synchronized block
        }
    }
}

有几个重要的点可以使这个工作(根据我们的经验,非官方):

  • 服务不能是事务性的 - 如果服务是事务性的,事务在方法 returns 值之后提交并且方法内部的同步是不够的。程序化交易可能是另一种方式
  • 同步方法不够 synchronized def someConcurrentSafeMethod() 将不起作用 - 可能是因为服务被包裹在代理中
  • 会话必须在同步块内刷新
  • 每个将被保存的对象,都应该在同步块中读取,如果你从外部方法传递它,你可能运行进入乐观锁定失败异常

已更新

由于应用部署在分布式系统上,以上并不能解决这里的问题(可能对其他人有帮助)。在我们对 Slack 进行讨论之后,我总结了可能的方法:

  • 更新对象的悲观锁定和插入的整个 table 锁定(如果可能)
  • 将 'dangerous' 数据库相关方法移动到具有某些 API 的单个服务器,如 REST 并从其他部署调用它(并使用上面的同步方法)
  • 使用多重保存方法 - 如果操作失败,捕获异常并重试。这是由 Spring Integration 或 Apache Camel 等集成库支持的,并且是企业模式之一。请参阅 request-handler-advice-chain Spring 集成示例
  • 使用一些东西来排队操作,例如 JMS 服务器

如果有人有更多想法,请分享。