设置failOnError为false仍触发多列唯一约束违反异常求助
这个问题我太熟悉了,核心原因是你搞混了Grails验证层和数据库层面约束的作用范围,咱们一步步拆解:
问题根源
你在mapping块里配置的userAccount(unique:['userName','password']),只是告诉Grails生成数据库的联合唯一索引,但这个约束不会自动同步到Grails的验证逻辑中。
而failOnError: false的作用是:当Grails自身的验证规则(比如nullable: false这类)不通过时,不抛出异常,而是把错误信息存入domain对象的errors集合里。但数据库层面的重复键异常是在数据写入DB时才触发的,这时候Grails的验证已经通过了,所以failOnError根本管不到这个场景。
两种解决办法
办法一:添加自定义验证(推荐)
直接在constraints里添加自定义验证规则,提前检查userName和password的组合是否已经存在,这样就能把拦截逻辑放到Grails验证层,让failOnError生效:
class Account { String userName String password String userAccount static constraints = { userName(nullable: false, validator: { val, obj -> // 检查当前组合是否已存在,注意排除自身(更新场景) def existing = Account.where { userName == val && password == obj.password && id != obj.id }.count() return existing == 0 }) password(nullable: false) userAccount(nullable: false) } static mapping = { table 'custdata' userAccount(unique:['userName','password']) } }
这样在调用account.save(failOnError: false)时,如果组合重复,Grails会自动把错误加入errors,不会抛出数据库异常,同时返回false。
办法二:捕获数据库异常(兜底方案)
如果业务场景需要兼容并发写入的极端情况(比如两个请求同时通过验证,同时写入DB),可以在保存时捕获数据库的约束异常,手动处理:
import java.sql.SQLIntegrityConstraintViolationException // 控制器代码 Account account = new Account(...) try { account.save(failOnError: false) } catch (SQLIntegrityConstraintViolationException e) { // 手动添加错误信息 account.errors.reject('account.duplicate', '用户名和密码组合已存在') }
不过这个方案依赖数据库驱动的异常类型,通用性稍差,建议只作为自定义验证的兜底补充。
额外提示
如果是更新操作,自定义验证里一定要排除当前对象的ID(就像办法一里写的id != obj.id),否则更新时会误判为重复。
内容的提问来源于stack exchange,提问作者GrailsLearner

