This is following post series about ‘Domain Object Relationships in Grails GORM and SQL Relational Databases‘ where we look at the details how the relationships build in GORM with how physically structrured in DB while cascading and direction addressed.

We are going to use RetnalUnit – Review example where one RentalUnit may have multiple reviews and each review can be assigned to only one rental unit. In another words RentalUnit and Review has One-To-Many relationship .

This post is divided into four sections as following:

In each section, there is ER diagram, Class diagram and code snippet to better cover the details

Database Structure for Unidirectional One-To-Many Relationship of GORM

To better see the structure of the relationship in Database, the ER Diagram and a view of tables is presented here:

From the diagram, the unidirectional one-to-many relationship is constructed by having addition table (i.e. Retnal_Unit_Review) containing two columns each Id fields from tables of the entities forming the relationship(i.e. RentalUnit and Review). The new added table is child to parent tables – Review and RentalUnit.

Section 1 – Unidirectional Non-Cascading One-To-Many

(a)Class Diagram

(b)Code Snippet

class Review {
    String submittedBy
    String content
    String dateReceived
    boolean isApproved

    static constraints = {
        submittedBy blank: false, size: 3..50
        content blank: false, size: 5..2500
    ....
    }
...
}
class RentalUnit {
    String name
    String nickname
    Address address
    static hasMany = [reviews:Review]

    static constraints = {
        name blank: false, unique: true, size: 4..10
        nickname blank: false, size: 5..60
    }
}

No association defined  in the Review class which makes it unidirectional. There is association ‘reviews’ defined in the RentalUnit class(line 5 highlighted).

Section 2 – Unidirectional Cascading One-to-Many Relationship

(a)Class Diagram

(b)Code Snippet

class Review {
    String submittedBy
    String content
    String dateReceived
    boolean isApproved

    static belongsTo = RentalUnit

    static constraints = {
        submittedBy blank: false, size: 3..50
        content blank: false, size: 5..2500
    ....
    }
...
}

By adding ‘belongTo’ (highlighted line) in the Review class, makes this unidirectional relationship cascade (you inform GORM that the Review belongs to Rental Unit, so it knows when Rental Unit is deleted to cascade onto particular Review). No change needed in Rental Unit

But who cares for orphans!?

The cascading will work for all those reviews pointed by rentalUnit you are deleting:

given:
   def rev1 = Review.build().save(flush:true)
   def rev2 = Review.build().save(flush:true)
   def rentalUnit = RentalUnit.build().addToReviews(rev1).addToReviews(rev2).save(flush:true)
when:
   assert 2 == Reviews.count()
   rentalUnit.delete()
then:
   0 == Reviews.count()

In line 7, the rental unit is deleted and it deletes  all reviews associated with this rental unit from database(validated on line 9), however. It will not cascade on delete if the review is orphan:

given:
   def rev1 = Review.build().save(flush:true)
   def rev2 = Review.build().save(flush:true)
   def rentalUnit = RentalUnit.build().addToReviews(rev1).addToReviews(rev2).save(flush:true)
when:
   assert 2 == Reviews.count()
   rentalUnit.removeFromReviews(rev1)
then:
   2 == Reviews.count()

In line 7, the review – rev1 becomes an orphan since removing it from rental unit, the review is no more referencing any rental unit (the entry in review table  pointing to none or rental_unit_in is null). The line 9 validates that cascading on delete has not applied to orphans( the removed review ‘rev1′ is still in the database). If you wish to apply cascading also to orphans then you have to add following lines:

class RentalUnit {
    String name
    String nickname
    Address address
    static hasMany = [reviews:Review]

    static mapping = {
        reviews cascade: "all-delete-orphan"
    }
...
}

By specifying ‘all-delete-orphan’, the cascade on delete is applied to orphans as well. In our situation, the review – rev1, thus, will be deleted from database when removed from Rental Unit (i.e. rentalUnit.removeFromReviews(rev1))

Don’t be Tricked

For a while, i was puzzled because i saw in my follow developers code that  there was missing line ‘static belongtsTo = SOMEClass’ (line 7 in our Review class) on the right side of the association(i.e. Review) that  enables cascading but it was still cascading fine on deletes. It turns out that the cascading can be enabled from the left side of the association(i.e. Renta Unit)  with the following:

static mapping = {
        reviews cascade: "all-delete-orphan"
}

So, please, don’t be puzzled if cascading works if you don’t see it enabled from the right side of the association. Check the left side of the association. The same applies vice versa. In short, its enough enable cascading on only one side to make it work

Database Structure for Bidirectional One-To-Many Relationship of GORM

To better see the structure of the relationship in Database, the ER Diagram and  view of tables is presented here:

From the diagram, the bidirectional one-to-many relationship is constructed by having addition field ‘rental_unit_it’ in the Review table that is also a foreign key. This makes a child-parent relationship between rental unit and review in  bidirectional one-to-many relationship.

Section 3 – Bidirectional Cascading One-to-Many Relationship

(a)Class Diagram

(b)Code Snippet

class Review {
    String submittedBy
    String content
    String dateReceived
    boolean isApproved

    static belongsTo = [rentalUnit: RentalUnit]

    static constraints = {
        submittedBy blank: false, size: 3..50
        content blank: false, size: 5..2500
    ....
    }
...
}
class RentalUnit {
    String name
    String nickname
    Address address
    static hasMany = [reviews:Review]

    static mapping = {
        reviews cascade: "all-delete-orphan"
    }

    static constraints = {
        name blank: false, unique: true, size: 4..10
        nickname blank: false, size: 5..60
    }
}

The highlighted line in Review sets the association to Rental Unit. It also enables cascading on delete (save/update is by default). The highlighted line 5 in the Rental Unit sets the associations to many Reviews.

Section 4 – Bidirectional Non-Cascading One-to-Many

(a)Class Diagram

(b)Code Snippet

class Review {
    String submittedBy
    String content
    String dateReceived
    boolean isApproved

    static belongsTo = [rentalUnit: RentalUnit]

    static constraints = {
        submittedBy blank: false, size: 3..50
        content blank: false, size: 5..2500
    ....
    }
...
}
class RentalUnit {
    String name
    String nickname
    Address address
    static hasMany = [reviews:Review]

    static mapping = {
       reviews cascade: "none"
    }

    static constraints = {
        name blank: false, unique: true, size: 4..10
        nickname blank: false, size: 5..60
    }
}

The highlighted lines remove cascading for association between rental unit and review. In another words, if you, for example, removing some review in rentalUnit1.reviews collection, it does not physically remove the review from the Review table in database

Step Further

If you remove the left side association (the reference into Review from the Rental Unit class)  from the Bidirectional Rental Unit to Review relationship described above, guess what it will become? Let’s look at Class Diagram:

From the class diagram, we see it is now unidirectional relationship, however different than one above(make sure you compare diagrams). This Unidirectional One-To-Many relationship  is pointing oposite direction. The database structure is the same as the Bidirectional one-to-many relationship described above. In another words, you have two versions of Unidirectional one-to-many relationship separated from each other by database structure as well as the direction of the single association.

The cascading is inherited unless you explicitly disable with ‘static mapping’ statement as described  above(highlighted lines). With that, i would stay away from this relationship because it quickly becomes impossible to delete  either some instance of ‘review’ or ‘rentalUnit’ with associations present and no ‘removeTo’ grails method available to help.

Other

java.util.NoSuchElementException - Challenging One-To-Many

How would GORM handle situation where we pass the same Review to two different Rental Units making it not true One-To-Many relationship?

In another words, the relationship One-to-Many from RentalUnit to Review == Many-to-One from Review to RentalUnit. Now, if we are able to add the same shared Review to two different RentalUnits, then the Review, instead of one, is pointing to more than one RentalUnit.  Thus, this is no more Many-to-one from Review to RentalUnit. We run following Spock test to analyze ( with bidirectional cascading one-to-many implementation as described above):

when:"making true one-to-many"
        def review3 = Review.build().save(flush: true)
        def rentalUnit1 = RentalUnit.build().addToReviews(review3).save(flush: true)
        def rentalUnit2 = RentalUnit.build().addToReviews(review3).save(flush: true)
        review3.submittedBy = "ziggy"
        review3.save(flush: true)

then:
        rentalUnit1.reviews.first().id == rentalUnit2.reviews.first().id
        review3.rentalUnit.id == rentalUnit2.id
        rentalUnit1.reviews.first().submittedBy == "ziggy"
        rentalUnit2.reviews.first().submittedBy == "ziggy"

For our surprise, this test pass making it look like the review ‘review3′ is pointing to two rental units ‘rentalUnit1′ and ‘rentalUnit2′, however. From database table structure of one-to-many described above, this is not possible.

In another words, in order this test pass, the GORM would have to clone review3 in the Review table referencing it to the another rental_unit-id, but then still line 9 (highlighted) would fail since all IDs are unique in the Review table. So, how is it possible this test pass? Thank you Victor Sergienko at stackflow.com for being patient helping me understand. The test will fail if adding one line as follows:

when:"making true one-to-many"
        def review3 = Review.build().save(flush: true)
        def rentalUnit1 = RentalUnit.build().addToReviews(review3).save(flush: true)
        def rentalUnit2 = RentalUnit.build().addToReviews(review3).save(flush: true)
        review3.submittedBy = "ziggy"
        review3.save(flush: true)
        rentalUnit1.refresh()

then:
        rentalUnit1.reviews.first().id == rentalUnit2.reviews.first().id
        review3.rentalUnit.id == rentalUnit2.id
        rentalUnit1.reviews.first().submittedBy == "ziggy"
        rentalUnit2.reviews.first().submittedBy == "ziggy"

By adding line to refresh() of rentalUnit1, the test fails with exception – ‘java.util.NoSuchElementException’.

In line 4, when adding ‘review3′ to the ‘rentalUnit2′, two things happen. First, the review3 entry in the database is updated with different rental_unit_id to point to rentalUnit2. Second, at the same time, the session for rentalUnit2 is updated to point to review3, however!!!! The session is not updated in respect to rentalUnit1. In the session, the rentalUnit1 still pointing to review3 even thou in database it is not. This was why our test was passing. By refreshing the rentalUnit1, the session was reloaded with what is actually in database making it in sync. Then test was failing as expected.

Important to note, that refreshing either the ‘review3′ or the ‘rentalUnit2′ would not make the test fail. It has to be the rentalUnit1 since it is the one out of sync.

In summary, updating associations for domain objects in GORM, may cause session be out of sync with database. This is because the association overwritten by new association, does not get update in session making out of sync till reloaded from database.

Things Different For Unidirectional

For the Unidirectional One-To-Many relationship the above ‘refresh()’ doesn’t remove the reference to the shared Reveiw. As result the test fails in Unidirectional but not if Bidirectional relationship:

def "one-to-many relationship between RentalUnit and Review"() {
        when:"adding shared review will result referenced by last rental unit"
        def review3 = Review.build().save(flush: true)
        def rentalUnit1 = RentalUnit.build().addToReviews(review3).save(flush: true)
        def rentalUnit2 = RentalUnit.build().addToReviews(review3).save(flush: true)
        rentalUnit1.refresh()

        then:
        !rentalUnit1.reviews.contains(review3)
    }

This test fails, perhaps, because the data structure is different. As you saw from the post, there is extra table in Unidirectional relationship. And, I am guessing, that the ‘addToReviews’ method does not update that extra table(line 5), so the reloading session (highlighted line) still contains reference to the shared review. This is different in Bidirectional relationship where ‘addToReviews’ handles the reference in dB and then by reloading session the reference to shared Review was cleared/sync with database accordingly.