3

I am using Grails 1.3.7, and have the following domain classes:

package com.fxpal.test

class UserGroup {

    String name

    static constraints = {
        name(blank: false)
    }
}

class Invitation {

    UserGroup group
    String user

    static belongsTo = [group: UserGroup]

    static constraints = {
        group(nullable: false)
    }
}

I would like to be able to delete all Invitation instances that refer to a UserGroup instance when that UserGroup instance is deleted, without having an explicit relationship that refers to Invitation in UserGroup. In other words, I would like to have a cascading delete from UserGroup to Invitation, without modifying Group.

My test for this fails due to a constraint that represents the Invitation -> UserGroup relationship:

void testCascadingDelete() {
    UserGroup group1 = new UserGroup(name: 'group1').save(flush: true, failOnError: true)
    UserGroup group2 = new UserGroup(name: 'group2').save(flush: true, failOnError: true)

    Invitation invitation = new Invitation(user:'user1', group: group1).save(flush: true, failOnError: true)

    assertEquals("Wrong number of groups.", 2, UserGroup.count())
    assertEquals("Wrong number of invitations.", 1, Invitation.count())

    group1.delete(flush: true, failOnError: true)

    assertEquals("Wrong number of groups.", 1, UserGroup.count())
    assertEquals("Wrong number of invitations.", 0, Invitation.count())
}

When I run the test, it fails like this:

could not delete: [com.fxpal.test.UserGroup#1]; SQL [delete from user_group where id=? and version=?]; constraint [FK473F7799A8642225]; nested exception is org.hibernate.exception.ConstraintViolationException: could not delete: [com.fxpal.test.UserGroup#1]
org.springframework.dao.DataIntegrityViolationException: could not delete: [com.fxpal.test.UserGroup#1]; SQL [delete from user_group where id=? and version=?]; constraint [FK473F7799A8642225]; nested exception is org.hibernate.exception.ConstraintViolationException: could not delete: [com.fxpal.test.UserGroup#1]
    at com.fxpal.test.InvitationIntegrationTests.testCascadingDelete(InvitationIntegrationTests.groovy:23)
Caused by: org.hibernate.exception.ConstraintViolationException: could not delete: [com.fxpal.test.UserGroup#1]
    at com.fxpal.test.InvitationIntegrationTests.testCascadingDelete(InvitationIntegrationTests.groovy:23)
  ...

It seems like the canonical nose-face example, but the cascading delete doesn't seem to work. I really don't want to have to represent the Invitation in the UserGroup, in particular because in the end, the Invitation will reference several other domain classes, the deletion of any of which should cause the corresponding Invitation to get deleted as well.

What am I missing?

Gene

Gene Golovchinsky
  • 6,101
  • 7
  • 53
  • 81

1 Answers1

5

I'm not sure if it's possible without the bidirectional relationship, but you can easily do it yourself in a transactional service method (to make sure the deletes all happen or none happen):

void deleteGroup(UserGroup group) {
   Invitation.executeUpdate(
       'delete from Invitation where group=:group',
       [group: group])
   group.delete(flush: true, failOnError: true)
}

and then your test becomes

def fooService

...

void testCascadingDelete() {
    UserGroup group1 = new UserGroup(name: 'group1').save(flush: true, failOnError: true)
    UserGroup group2 = new UserGroup(name: 'group2').save(flush: true, failOnError: true)

    Invitation invitation = new Invitation(user:'user1', group: group1).save(flush: true, failOnError: true)

    assertEquals("Wrong number of groups.", 2, UserGroup.count())
    assertEquals("Wrong number of invitations.", 1, Invitation.count())

    fooService.deleteGroup group1

    assertEquals("Wrong number of groups.", 1, UserGroup.count())
    assertEquals("Wrong number of invitations.", 0, Invitation.count())
}

A couple of minor unrelated notes - properties are not-null by default, so you can remove the group(nullable: false) constraint. And a belongsTo in Map form like you have defines a variable of that name, so you can omit UserGroup group in Invitation.

UPDATE:

Another less intrusive option is to use the before-delete event in UserGroup:

def beforeDelete() {
   Invitation.executeUpdate(
      'delete from Invitation where group=:group',
      [group: this])
}
Burt Beckwith
  • 75,342
  • 5
  • 143
  • 156
  • Thanks for the quick response, Burt! I was hoping to avoid the deleteGroup() style of solution as that would require me to make sure that such a method was present for every other class that Invitation might use as its context (e.g., the user issuing the invitation). Oh well. – Gene Golovchinsky Jul 25 '11 at 17:36
  • As far as duplicating the property in `belongsTo` goes, I did that intentionally to allow SpringSource Tool Suite (Eclipse) to recognize the property in my class when doing syntax assist. – Gene Golovchinsky Jul 25 '11 at 17:38
  • I updated the answer with a more lightweight option. Still requires extra code, but it's contained within the domain class. – Burt Beckwith Jul 25 '11 at 19:20
  • I wonder if it would be desirable to be able to declare these kinds of semantics instead of having to code the `beforeDelete()` method. It seems that the info required is completely implied by a one-way `def static belongsTo = [group: UserGroup]` declaration in the `Invitation` class. Is it too late to request this for Grails 2.0? – Gene Golovchinsky Jul 25 '11 at 19:40
  • It's not too late but I'm not sure where it'll land on the priority list. We're releasing 2.0.0M1 this week, and can add fixes/enhancements until the RC. Go ahead and JIRA. – Burt Beckwith Jul 25 '11 at 20:28
  • Burt, can you please comment on the GORM documentation which clearly states, `In the case of a unidirectional one-to-one association that defines a belongsTo then the cascade strategy is set to "ALL" for the owning side of the relationship (A->B) and "NONE" from the side that defines the belongsTo (B->A)`. Doesn't this imply that the deletion of the parent should automatically cascade? – mmigdol Dec 16 '11 at 19:43