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 服务器
如果有人有更多想法,请分享。
在我的服务代码中,我试图创建或更新一个 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 服务器
如果有人有更多想法,请分享。