Grails null id error on constraints during integration test
constraints, grails, grails-2.2, hibernate, integration-testing
Solution
I think I figured it out... The one-to-many relationship is broken.
Let me explain
- The first line in the test creates the `User john` which is saved and flushed.
- The second line creates an `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: 'john@gmail.com', 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.
Another route
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.
Problem
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: 'john@gmail.com', 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'] } ```