Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Grails null id error on constraints during integration test

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']
    }
like image 286
Raipe Avatar asked Oct 05 '22 16:10

Raipe


1 Answers

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: '[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.


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.

like image 199
Bart Avatar answered Oct 13 '22 12:10

Bart