Reputation: 14184
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
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