Grails 2.2.0
I am trying to create a custom constraint to force user to have only one master email. Here is the simplified code causing the error:
User domain class
class User {
static hasMany = [emails: Email]
static constraints = {
}
}
Email domain class
class Email {
static belongsTo = [user: User]
String emailAddress
Boolean isMaster
static constraints = {
emailAddress unique: ['user']
isMaster validator: { val, obj ->
return !val || Email.findByUserAndIsMaster(obj.user, true) == null
}
}
}
Integration test
class EmailTests {
@Before
void setUp() {
}
@After
void tearDown() {
// Tear down logic here
}
@Test
void testSomething() {
def john = (new User(login: 'johnDoe')).save(failOnError: true, flush: true)
assert (new Email(emailAddress: '[email protected]', user: john, isMaster: true)).save(failOnError: true)
}
}
Running "grails test-app -integration" will cause:
| Failure: testSomething(webapp.EmailTests)
| org.hibernate.AssertionFailure: null id in webapp.Email entry (don't flush the Session after an exception occurs) at org.grails.datastore.gorm.GormStaticApi$_methodMissing_closure2.doCall(GormStaticApi.groovy:105) at webapp.Email$__clinit__closure1_closure2.doCall(Email.groovy:13) at org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener.onApplicationEvent(AbstractPersistenceEventListener.java:46) at webapp.EmailTests.testSomething(EmailTests.groovy:21)
If I change the unique constraint to be after the custom constraint the problem will not happen. What is happening here? I want to understand how is the order of the constraints of any relevance here?
To be clear this does NOT cause the problem:
static constraints = {
isMaster validator: { val, obj ->
return !val || Email.findByUserAndIsMaster(obj.user, true) == null
}
emailAddress unique: ['user']
}
I think I figured it out... The one-to-many relationship is broken.
User john
which is saved and flushed.Email
and sets john as the user property.Once you try to save the Email
instance GORM will complain. That is because you assigned john to Email
which is the inverse side of the relationship. The owning side isn't aware of this and owns nothing at that point. Simply put. You cannot save and email instance before added to a user.
Here's a test method that should work.
void testSomething() {
def john = new User(login: 'johnDoe')
john.addToEmails(new Email(emailAddress: '[email protected]', isMaster: true))
john.save(flush:true)
assert false == john.errors.hasErrors()
assert 1 == john.emails.size()
}
The addToEmails()
method adds the email instance to the collection and sets the user on the inverse side of the relationship. The relationship is now satisfied and saving john should also save all emails.
Since the problem seems to be the reference to the user instance in the Email
validator I though maybe there's another route you could take.
class User {
static hasOne = [master: Email]
static hasMany = [emails: Email]
}
This would eliminate the need for the validator in question which makes you Email
class depending on a User
for validation. You can let the user take responsibility on what e-mail addresses he owns and what rules should be applied.
You could add validators to User
to verify that you have a master address that is not present in the emails list and also verify if all the addresses assigned are unique.
Like for example:
static constraints = {
master validator: { master, user, errors ->
if (master.emailAddress in user.emails*.emailAddress) {
errors.rejectValue('master', 'error.master', 'Master already in e-mails')
return false
}
}
emails validator: { emails, user, errors ->
def addresses = emails*.emailAddress
if (!addresses.equals(emails*.emailAddress.unique())) {
errors.rejectValue('emails', 'error.emails', 'Non unique e-mail')
return false
}
}
}
I did some tests and they came out fine doing it in this way.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With