VaclavDedik
VaclavDedik

Reputation: 1980

GORM - modifying object in the beforeDelete event doesn't work

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

Answers (1)

user553180
user553180

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:

  1. 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
  2. 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

Related Questions