Reputation: 1980
I have these two domain classes:
class User {
String username
String password
static hasMany = [organizations: Organization]
static belongsTo = Organization
static constraints = {
username blank: false, unique: true
password blank: false
}
static mapping = {
password column: '`password`'
organizations fetch: 'join'
}
def beforeDelete() {
User.withNewSession {
this.organizations.each {
it.removeFromMembers(this)
}
this.save()
}
}
}
class Organization {
String name;
static hasMany = [members: User]
}
What I'm trying to achieve is I think pretty clear -- I want to be able to delete a user but before that, I have to delete the user from all organizations that he is assigned to. GORM obviously doesn't support this so I tried it to write the beforeDelete event on class User that would delete the user from all organizations just before the delete. The problem is that the above code doesn't work, this is the exception it throws:
| Error 2012-10-10 16:27:56,898 [http-bio-8080-exec-2] ERROR errors.GrailsExceptionResolver - NonUniqueObjectException occurred when processing request: [POST] /theses-management/user/index - parameters:
id: 1
_action_delete: Delete
a different object with the same identifier value was already associated with the session: [com.redhat.theses.auth.User#1]. Stacktrace follows:
Message: a different object with the same identifier value was already associated with the session: [com.redhat.theses.auth.User#1]
Line | Method
->> 36 | doCall in com.redhat.theses.auth.User$_beforeDelete_closure1$$ENlS7bOi
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| 32 | beforeDelete in com.redhat.theses.auth.User$$ENlS7bOi
| 46 | onApplicationEvent in org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener
| 93 | delete in com.redhat.theses.auth.UserController$$ENlS7klM
| 195 | doFilter . . . . . in grails.plugin.cache.web.filter.PageFragmentCachingFilter
| 63 | doFilter in grails.plugin.cache.web.filter.AbstractFilter
| 1110 | runWorker . . . . in java.util.concurrent.ThreadPoolExecutor
| 603 | run in java.util.concurrent.ThreadPoolExecutor$Worker
^ 722 | run . . . . . . . in java.lang.Thread
So I tired to change the beforeDelete event like this:
def beforeDelete() {
User.withNewSession {
this.organizations.each {
it.removeFromMembers(this)
}
this.merge()
this.save()
}
}
But then the same exception is being thrown...
Another attempt was this:
def beforeDelete() {
User.withNewSession {
this.organizations.each {
it.removeFromMembers(this)
}
def user = this.merge()
user.save()
}
}
Result is this exception:
| Error 2012-10-10 16:33:22,669 [http-bio-8080-exec-7] ERROR util.JDBCExceptionReporter - Referential integrity constraint violation: "FK6BC87A0D3E1F37F9: PUBLIC.ORGANIZATION_MEMBERS FOREIGN KEY(USER_ID) REFERENCES PUBLIC.USER(ID)"; SQL statement:
delete from user where id=? and version=? [23503-164]
| Error 2012-10-10 16:33:22,673 [http-bio-8080-exec-7] ERROR events.PatchedDefaultFlushEventListener - Could not synchronize database state with session
Message: could not delete: [com.redhat.theses.auth.User#1]
Line | Method
->> 93 | delete in com.redhat.theses.auth.UserController$$ENlS7klM
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| 195 | doFilter in grails.plugin.cache.web.filter.PageFragmentCachingFilter
| 63 | doFilter in grails.plugin.cache.web.filter.AbstractFilter
| 1110 | runWorker in java.util.concurrent.ThreadPoolExecutor
| 603 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker
^ 722 | run in java.lang.Thread
Caused by JdbcSQLException: Referential integrity constraint violation: "FK6BC87A0D3E1F37F9: PUBLIC.ORGANIZATION_MEMBERS FOREIGN KEY(USER_ID) REFERENCES PUBLIC.USER(ID)"; SQL statement:
delete from user where id=? and version=? [23503-164]
->> 329 | getJdbcSQLException in org.h2.message.DbException
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| 169 | get in ''
| 146 | get . . . in ''
| 398 | checkRow in org.h2.constraint.ConstraintReferential
| 415 | checkRowRefTable in ''
| 291 | checkRow in ''
| 862 | fireConstraints in org.h2.table.Table
| 879 | fireAfterRow in ''
| 99 | update . in org.h2.command.dml.Delete
| 73 | update in org.h2.command.CommandContainer
| 226 | executeUpdate in org.h2.command.Command
| 143 | executeUpdateInternal in org.h2.jdbc.JdbcPreparedStatement
| 129 | executeUpdate in ''
| 105 | executeUpdate in org.apache.commons.dbcp.DelegatingPreparedStatement
| 93 | delete . in com.redhat.theses.auth.UserController$$ENlS7klM
| 195 | doFilter in grails.plugin.cache.web.filter.PageFragmentCachingFilter
| 63 | doFilter in grails.plugin.cache.web.filter.AbstractFilter
| 1110 | runWorker in java.util.concurrent.ThreadPoolExecutor
| 603 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker
^ 722 | run in java.lang.Thread
| Error 2012-10-10 16:33:22,680 [http-bio-8080-exec-7] ERROR errors.GrailsExceptionResolver - JdbcSQLException occurred when processing request: [POST] /theses-management/user/index - parameters:
id: 1
_action_delete: Delete
Referential integrity constraint violation: "FK6BC87A0D3E1F37F9: PUBLIC.ORGANIZATION_MEMBERS FOREIGN KEY(USER_ID) REFERENCES PUBLIC.USER(ID)"; SQL statement:
delete from user where id=? and version=? [23503-164]. Stacktrace follows:
Message: Referential integrity constraint violation: "FK6BC87A0D3E1F37F9: PUBLIC.ORGANIZATION_MEMBERS FOREIGN KEY(USER_ID) REFERENCES PUBLIC.USER(ID)"; SQL statement:
delete from user where id=? and version=? [23503-164]
Line | Method
->> 329 | getJdbcSQLException in org.h2.message.DbException
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| 169 | get in ''
| 146 | get . . . . . . . . . in ''
| 398 | checkRow in org.h2.constraint.ConstraintReferential
| 415 | checkRowRefTable . . in ''
| 291 | checkRow in ''
| 862 | fireConstraints . . . in org.h2.table.Table
| 879 | fireAfterRow in ''
| 99 | update . . . . . . . in org.h2.command.dml.Delete
| 73 | update in org.h2.command.CommandContainer
| 226 | executeUpdate . . . . in org.h2.command.Command
| 143 | executeUpdateInternal in org.h2.jdbc.JdbcPreparedStatement
| 129 | executeUpdate . . . . in ''
| 105 | executeUpdate in org.apache.commons.dbcp.DelegatingPreparedStatement
| 93 | delete . . . . . . . in com.redhat.theses.auth.UserController$$ENlS7klM
| 195 | doFilter in grails.plugin.cache.web.filter.PageFragmentCachingFilter
| 63 | doFilter . . . . . . in grails.plugin.cache.web.filter.AbstractFilter
| 1110 | runWorker in java.util.concurrent.ThreadPoolExecutor
| 603 | run . . . . . . . . . in java.util.concurrent.ThreadPoolExecutor$Worker
^ 722 | run in java.lang.Thread
Do you have any idea what I might be doing wrong?
Thanks for your answers in advance.
Upvotes: 0
Views: 982
Reputation: 626
I believe what's happening is that, as you are iterating over the User.organizations collection and removing each association between User/Organization, Hibernate is trying to update that same User.organizations collection (to reflect your deletion) - which prevents the association deletion, which causes your constraint violation.
I've never seen a solution for this issue that uses beforeDelete (unfortunate since it seems like the logical place for that logic), but have seen the following work-arounds:
Place the logic to remove the associations in controller/service layer before the delete (again too bad beforeDelete doesn't work :( in this situation). To avoid concurrently modifying User.organizations, you can make a copy of the collection and iterate over the copy, or query for the associated Organizations, here's a sample query:
select o from Organization o join o.members AS m where m.id=:id
Make the association between User/Organization explicit, like:
class UserOrganization { User user Organization org } class User { ... Set<Organization> getOrganizations() { UserOrganization.findAllByUser(this).collect{ it.org } as Set } }
If you look at the SpringSecurityCore plugin User/Authority code, there's a good example of how to manage associations explicitly.
Upvotes: 1