Ошибка нулевого идентификатора Grails в ограничениях во время интеграционного теста

Грааль 2.2.0

Я пытаюсь создать пользовательское ограничение, чтобы заставить пользователя иметь только один основной адрес электронной почты. Вот упрощенный код, вызывающий ошибку:

Класс пользовательского домена

class User {

    static hasMany = [emails: Email]

    static constraints = {
    }
}

Класс домена электронной почты

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
        }

    }
}

Интеграционный тест

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)
    }
}

Запуск "grails test-app -integration" вызовет:

| Ошибка: testSomething(webapp.EmailTests)
| org.hibernate.AssertionFailure: нулевой идентификатор в записи webapp.Email (не очищайте сеанс после возникновения исключения) в org.grails.datastore.gorm.GormStaticApi$_methodMissing_closure2.doCall(GormStaticApi.groovy:105) в webapp.Email $__clinit__closure1_closure2.doCall(Email.groovy:13) в org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener.onApplicationEvent(AbstractPersistenceEventListener.java:46) в webapp.EmailTests.testSomething(EmailTests.groovy:21)

Если я изменю уникальное ограничение, чтобы оно было после пользовательского ограничения, проблема не возникнет. Что здесь происходит? Я хочу понять, как здесь порядок ограничений имеет какое-либо значение?

Чтобы было ясно, это НЕ вызывает проблему:

static constraints = {
        isMaster validator: { val, obj ->
            return !val || Email.findByUserAndIsMaster(obj.user, true) == null
        }
        emailAddress unique: ['user']
    }

person Raipe    schedule 18.03.2013    source источник
comment
Разве отношение в тесте не обратное? Я думаю, вы хотите использовать john.addToEmails(). Джон является собственником. В своем тесте вы используете Email, как будто это сторона-владелец :-)   -  person Bart    schedule 19.03.2013
comment
Та же проблема, если я сделаю: void testSomething() { def john = new User(login: 'johnDoe').save(failOnError: true) def email = new Email(emailAddress: '[email protected]', isMaster: true) john.addToEmails(email) assert john.save(failOnError: true) }   -  person Raipe    schedule 19.03.2013


Ответы (1)


Кажется, я разобрался... Связь "один ко многим" нарушена.

Позволь мне объяснить

  • Первая строка в тесте создает User john, который сохраняется и сбрасывается.
  • Вторая строка создает Email и устанавливает john в качестве пользовательского свойства.

Как только вы попытаетесь сохранить экземпляр Email, GORM будет жаловаться. Это потому, что вы назначили Джона на Email, что является обратной стороной отношений. Сторона-владелец не знает об этом и в этот момент ничем не владеет. Проще говоря. Вы не можете сохранить экземпляр и отправить его по электронной почте, прежде чем добавить пользователю.

Вот метод тестирования, который должен работать.

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()  
}

Метод addToEmails() добавляет экземпляр электронной почты в коллекцию и устанавливает пользователя на обратной стороне отношения. Отношения теперь удовлетворены, и спасение Джона также должно сохранить все электронные письма.


Другой маршрут

Поскольку проблема, по-видимому, заключается в ссылке на экземпляр пользователя в валидаторе Email, я думаю, что, возможно, есть другой маршрут, который вы могли бы выбрать.

class User {
    static hasOne = [master: Email]
    static hasMany = [emails: Email]
}

Это устранит необходимость в рассматриваемом валидаторе, который делает вас Email классом в зависимости от User для проверки. Вы можете позволить пользователю взять на себя ответственность за то, какими адресами электронной почты он владеет и какие правила следует применять. Вы можете добавить валидаторы в User, чтобы убедиться, что у вас есть основной адрес, которого нет в списке адресов электронной почты, а также проверить, являются ли все назначенные адреса уникальными. Например:

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
        }
    }
}

Я сделал несколько тестов, и они вышли хорошо, делая это таким образом.

person Bart    schedule 19.03.2013
comment
Имеет смысл. Но я не понимаю, почему я получаю сообщение об ошибке «объект ссылается на несохраненный переходный экземпляр», поскольку это первая пара пользователя и электронной почты, которую я пытаюсь сохранить, и она должна пройти пользовательское ограничение. - person Raipe; 20.03.2013
comment
Я думаю, что проблема в Email.findByUserAndIsMaster(obj.user, true). Он ссылается на несохраненный переходный процесс user. Я думаю, вам, возможно, нужно найти альтернативный способ обеспечить соблюдение вашего ограничения. Может быть, обернуть User.addToEmails, чтобы решить эту проблему «курицы и яйца»? - person Bart; 20.03.2013
comment
В ПОРЯДКЕ. Я надеялся, что все ограничения модели данных будут реализованы как доменные константы. Кажется, я слишком надеялся. - person Raipe; 20.03.2013
comment
Что именно вы имеете в виду под оберткой динамического метода addTo? Переписать метод в User и сделать в нем проверку? Как мне вызвать исходный метод addTo после? Я также подумал, можно ли этого добиться с помощью событий домена (например, beforeInsert)? - person Raipe; 20.03.2013
comment
Не говоря уже о том, что я сказал о обертке. Это не вычисляется :-) Я обновил пост, указав другой маршрут, которым вы могли бы воспользоваться. - person Bart; 20.03.2013
comment
Спасибо за ответы. Я несколько следовал маршруту подхода, который вы предложили. Но только один список адресов электронной почты в пользовательском домене (без дополнительного основного поля электронной почты). Вот что я в конечном итоге использовал: Ограничения пользовательского домена: emails validator: { emails, user, errors -> if(emails.grep({ it.isMaster }).size() > 1){ errors.rejectValue('emails', 'error.emails.multipleMaster', 'Only one master allowed') return false } } Похоже, что он работает нормально в соответствии с моими тестами. Спасибо! Я все еще пытаюсь выполнить уникальные электронные письма для каждого пользователя немного по-другому, но это выходит за рамки - person Raipe; 22.03.2013