Andy
Andy

Reputation: 14184

One-to-one and one-to-many association to same domain class in Grails

I have two domain classes -- Account and Member, and I am having trouble finding the correct combination of associations and mappedBy attributes to model their relationship. In plain English, an Account requires one and only one primary member. It also has 0 - many dependents. My initial attempt looked like:

Account.groovy

class Account {
    String displayId

    Member primaryMember
    SortedSet<Member> dependents = new TreeSet<Member>()

    static belongsTo = [Member]
    static hasMany = [dependents: Member]
}

Member.groovy

class Member implements Comparable<Member> {
    MemberType memberType = MemberType.PRIMARY
    String firstName
    String lastName

    static belongsTo = [account: Account]

    @Override
    int compareTo(Member m) {
        return (this.memberType <=> m?.memberType) * 100 + (this.lastName <=> m?.lastName) * 10 + (this.firstName <=> m?.firstName)
    }
}

This causes problems when I attempt to instantiate and persist the accounts and members. For example,

AccountIntegrationTests.groovy

class AccountIntegrationTests extends GroovyTestCase {

    Account smiths
    Member john
    Member jane

    @Before
    void setup() {}

    @Test
    void testShouldLoadAccountWithNoDependents() {
        // Arrange
        smiths = new Account(displayId: "ABCDEFG")

        john = new Member(firstName: "John", lastName: "Smith", memberType: MemberType.PRIMARY)
        smiths.primaryMember = john
        smiths.save(flush: true, failOnError: true)

        def smithsId = smiths.id
        smiths.discard()

        // Act
        def loadedSmiths = Account.get(smithsId)

        // Assert
        assert loadedSmiths.members.size() == 1
        assert loadedSmiths.primaryMember == john
        assert loadedSmiths.dependents.size() == 0
    }

    @Test
    void testShouldLoadAccountWithOneDependent() {
        // Arrange
        smiths = new Account(displayId: "ABCDEFG")
        john = new Member(firstName: "John", lastName: "Smith", memberType: MemberType.PRIMARY)
        smiths.primaryMember = john
        smiths.addToDependents(new Member(firstName: "Jane", lastName: "Smith", memberType: MemberType.DEPENDENT))
        smiths.save(flush: true, failOnError: true)

        john = smiths.primaryMember
        jane = smiths.dependents.first()
        def smithsId = smiths.id
        smiths.discard()

        // Act
        def loadedSmiths = Account.get(smithsId)

        // Assert
        assert loadedSmiths.members.size() == 2
        assert loadedSmiths.primaryMember.firstName == "john"
        assert loadedSmiths.dependents.size() == 1
        assert loadedSmiths.dependents.first().firstName == "jane"
    }
}

will throw exceptions because the database tables for the second test look like

Account

id | display_id
1  | ABCDEFG

Member

id | first_name | last_name | member_type | account_id
1  | John       | Smith     | Primary     | 1
2  | Jane       | Smith     | Dependent   | 1

Obviously I would like the account to retrieve John as the primary member, and Jane as the dependent, but when GORM tries to load account.primaryMember, it throws a Hibernate exception that there are multiple rows (in Member) matching the account ID (1). I need a mappedBy discriminator to distinguish between the two associations, but the versions I have tried did not work:

Account

static mappedBy = [primaryMember: 'primaryMember', dependents: 'dependents']
- or -
static mappedBy = [dependents: 'account']

I have read the GORM documentation for both the associations and mappedBy , as well as various questions on the site regarding multiple associations to the same model, unfortunately they all seem to be modeling multiple hasMany associations. The one that does reference both one-to-many and one-to-one relationships to the same model illustrates it as:

class Person {
    String name

    static belongsTo = [team: Team]
}

class Team {
    String name

    static belongsTo = [coach: Person]
    static hasMany = [players: Person]
}

However, I tried to use this simple implementation and got:

--Output from testShouldLoadAccountWithNoDependents--
| Failure:  testShouldLoadAccountWithNoDependents(AccountIntegrationTests)
|  org.springframework.dao.InvalidDataAccessApiUsageException: object references an unsaved transient instance - save the transient instance before flushing: Account; nested exception is org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Account
    at AccountIntegrationTests.testShouldLoadAccountWithNoDependents(AccountIntegrationTests.groovy:26)
Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Account
    ... 1 more

AccountIntegrationTests.groovy:26 is the line smiths.save(flush: true, failOnError: true). When I modified the Team/Account class to:

class Team {
    String name
    Person coach

    static hasMany = [players: Person]
}

I encountered the same exception, so I believe some kind of cascade is required. I tried adding a cascade to the Account:

Account

static mapping = {
    primaryMember cascade: 'all'
    dependents cascade: 'all-delete-orphan'
}

then I also tried using event triggers:

Account

def beforeInsert() {
    if (primaryMember?.isDirty()) {
        primaryMember.save()
    }
}

def beforeUpdate() {
    if (primaryMember?.isDirty()) {
        primaryMember.save()
    }
}

Unfortunately, neither of these approaches resolved the transient instance exception. Any assistance on this is greatly appreciated.

Upvotes: 0

Views: 1573

Answers (1)

Andy
Andy

Reputation: 14184

The embarrassingly simple solution was to remove the static belongsTo = [account: Account] from Member. The bidirectional association was causing the issues.

Upvotes: 1

Related Questions