Невозможно получить каскадное сохранение или удалить встроенную ссылку на класс для работы

Я создал два простых доменных класса Grails V3, в которых местоположение является встроенным типом атрибута в родительском месте, как это

import java.time.LocalDate

class Venue {

    String name
    LocalDate dateCreated
    LocalDate lastVisited
    LocalDate lastUpdated
    GeoAddress location

    static hasOne = [location:GeoAddress]

    static embedded =['location']

    static constraints = {
        lastVisited nullable:true
        location    nullable:true
    }
    static mapping = {
        location cascade: "all-delete-orphan", lazy:false  //eager fetch strategy

    }
}


class   GeoAddress {

    String addressLine1
    String addressLine2
    String addressLine3
    String town
    String county
    String country = "UK"
    String postcode

    static belongsTo = Venue

    static constraints = {
        addressLine1 nullable:true
        addressLine2 nullable:true
        addressLine3 nullable:true
        town         nullable:true
        county       nullable:true
        country      nullable:true
        postcode     nullable:true
    }
}

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

Кроме того, когда я запускаю удаление на месте с включенным flush:true и запрашиваю адрес, я все еще получаю возвращенный встроенный адрес - я думал, что с flush:true я увижу каскадное удаление GeoAddress, но мой тест не проходит не получайте значение null при использовании GeoAddress.get(loc.id), как я и ожидал

@Integration
@Rollback
class VenueIntegrationSpec extends Specification {
  void "test venue with an address" () {
        when: "create a venue and an address using transitive save on embedded "
            GeoAddress address = new GeoAddress (addressLine1: "myhouse", town: "Ipswich", county: "suffolk", postcode : "IP4 2TH")
            address.save()  //have to save first - else Venue save fails

            Venue v = new Venue (name: "bistro", location: address)
            def result = v.save()

        then: "retrieve venue and check its location loaded eagerly "
            Venue lookupVenue = Venue.get(v.id)
            GeoAddress loc = lookupVenue.location
            loc.postcode == "IP4 2TH"
            loc.town == "Ipswich"

        when: " we delete the venue, it deletes the embedded location (Address)"
            v.delete (flush:true)
            GeoAddress lookupLoc = GeoAddress.get (loc.id)

        then: "address should disppear"
            lookupLoc == null
    }

Я думал, что настроил это правильно, но, очевидно, это не так. Почему мои каскадные действия для Venue.save() и delete() могут не каскадироваться на запись моего встроенного местоположения (GeoAddress)?


person WILLIAM WOODMAN    schedule 05.02.2017    source источник


Ответы (4)


Если я правильно понял

cascade: "all-delete-orphan"

Требуется, только если у вас есть hasMany=[something:Something]

В вашем случае это hasOne или GeoAddress location, вероятно, было бы лучше, если бы я создал такое отношение. Я знаю, что между ними есть небольшая разница.

Во всяком случае, вы тестируете, так что все теоретическое. Я думаю, вам нужно зафиксировать ошибки для начала, чтобы понять, почему он не привел к ожидаемому каскадному поведению. так что либо

if (!v.delete(flush:true) { 
  println "---  ${v.errors}" 
}

или обернуть вокруг

попробуй поймать блок

. У меня была аналогичная проблема с отношением hasMany, и это было связано с записью, совместно используемой с другими таблицами, из-за настройки самих базовых отношений таблиц hasMany. Хитрость заключалась в том, чтобы просто удалить запись из самого объекта:

lookupVenue .removeFromLocation(loc)

Как я уже сказал, это было отношение hasMany

person V H    schedule 06.02.2017
comment
я пробовал так - и удалял местоположение перед удалением места проведения, а затем тест проходил. Если я удалю loc, удалить его не удастся. когда: мы удаляем место проведения, оно удаляет встроенное местоположение (адрес) loc.delete(flush:true) v.delete (flush:true) if (v.hasErrors()) println errors: $v.errors GeoAddress lookupLoc = GeoAddress .get (loc.id) тогда: адрес должен исчезнуть Venue.get (v.id) == null lookupLoc == null - person WILLIAM WOODMAN; 06.02.2017
comment
и если вы изменили hasOne на GeoAddress location как определенный объект в классе предметной области, а затем сделали его location(nullable:true), тогда он будет удален? - person V H; 07.02.2017
comment
просто на работе в мес - нужно будет сделать небольшую реструктуризацию, чтобы попробовать это - сообщу вам, когда у меня будет первый момент, чтобы сделать это - person WILLIAM WOODMAN; 07.02.2017
comment
stackoverflow.com/questions/12900221/ действительно зависит от ваших потребностей - person V H; 07.02.2017
comment
Я думал об этом в глубине души, и все это объясняется во втором ответе. Похоже, если вы хотите такого поведения, то последуйте моему совету и переключитесь с hasOne на прямое отношение объекта. поскольку у hasOne может быть много родителей, имеющих одного и того же, и поэтому правило каскадного удаления не применяется. - person V H; 07.02.2017

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

Я написал слишком новые тесты, которые оба работают, но оригинальный тест не работает. Я делаю что-то странное - просто не заметил. Два новых теста делают один и тот же поток — только переменные разные — оба работают. так что проблема с первым тестом.

пересмотренный тест

@Integration
@Rollback
class VenueIntegrationSpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    //original test -  this fails, have to explicitly delete loc to make it work 
    void "test venue with an address" () {
        when: "create a venue and an address using transitive save on embedded "
            GeoAddress address = new GeoAddress (addressLine1: "myhouse", town: "Ipswich", county: "suffolk", postcode : "IP4 2TH")
            address.save()
            Venue v = new Venue (name: "bistro", location: address)
            def result = v.save(flush:true)

        then: "retrieve venue and check its location loaded eagerly "
            Venue lookupVenue = Venue.get(v.id)
            GeoAddress loc = lookupVenue.location
            loc.postcode == "IP4 2TH"
            loc.town == "Ipswich"

        when: " we delete the venue, it deletes the embedded location (Address)"
            //loc.delete(flush:true)
            v.delete (flush:true)
            if (v.hasErrors())
                println "errors: $v.errors"

            GeoAddress lookupLoc = GeoAddress.get (loc.id)

        then: "address should disppear"
            Venue.get (v.id) == null
            lookupLoc == null
    }

    //new test - external entity - works 
    void "test with tempLocation" () {
        when: ""
            TempLocation temp = new TempLocation(name:"will")
            Venue v = new Venue (name: "bistro", temp: temp)
            assert v.save(flush:true)

            Venue lookupVenue = Venue.get(v.id)

            TempLocation t = lookupVenue.temp
            assert t.name == "will"

            //try delete
            v.delete (flush:true)


        then : " retrieve temp"
            TempLocation.findAll().size() == 0
    }

    //new test - reuse embedded  entity - works 
    void "test with GeoLocation" () {
        when: ""
        GeoAddress a = new GeoAddress(town:"ipswich")
        Venue v = new Venue (name: "bistro", location: a)
        assert v.save(flush:true)

        Venue lookupVenue = Venue.get(v.id)

        GeoAddress ta = lookupVenue.location
        assert ta.town == "ipswich"

        //try delete
        v.delete (flush:true)


        then : " retrieve temp"
        GeoAddress.findAll().size() == 0
    }
}

пересмотренный объект тестирования - Venue.groovy со встроенным GeoAddress

class Venue {

    String name
    LocalDate dateCreated
    LocalDate lastVisited
    LocalDate lastUpdated
    GeoAddress location
    Collection posts

    //test behaviour
    TempLocation temp

    static hasOne = [location:GeoAddress, temp:TempLocation]
    static hasMany = [posts:Post]
    static embedded =['location']

    static constraints = {
        lastVisited nullable:true
        location    nullable:true, unique:true
        posts       nullable:true
        temp        nullable:true //remove later
    }
    static mapping = {
        location cascade: "all-delete-orphan", lazy:false, unique:true  //eager fetch strategy
        posts    sorted: "desc"
        temp     cascade: "all-delete-orphan", lazy:false, unique:true //remove later
    }
}


class   GeoAddress {

    String addressLine1
    String addressLine2
    String addressLine3
    String town
    String county
    String country = "UK"
    String postcode

    static belongsTo = Venue

    static constraints = {
        addressLine1 nullable:true
        addressLine2 nullable:true
        addressLine3 nullable:true
        town         nullable:true
        county       nullable:true
        country      nullable:true
        postcode     nullable:true
    }
}

новая внешняя версия адреса/локации для взлома. упрощенная версия geoAddress с той же логикой beongsTo/constraint

class TempLocation {

    String name

    //setup birdiectional one to one, cascade owned on venue
    static belongsTo = [venue:Venue]

    static constraints = {
        name nullable:true
    }
}

постараюсь перечитать в поезде - не знаю, почему 1-й тест не работает, но следующие два работают нормально .... пойду спать - слишком устал

person WILLIAM WOODMAN    schedule 07.02.2017

Я думаю, что это неправильная конфигурация. Embedded означает, что сущность встроена в класс предметной области. Обычно это обычный POJO, оставленный вне папки domainclass (и в папке src/groovy). Все поля встраиваемого объекта включаются в таблицу встраиваемого объекта. hasone устанавливает отношение между двумя сущностями доменного класса. Так что либо используйте встроенный, либо используйте hasOne, но не используйте оба одновременно.

Кроме того, была проблема с каскадным сохранением глубоко вложенных сущностей, она решена в 3.2.5.

person Dennie de Lange    schedule 09.02.2017
comment
не уверен, что это что-то другое. я попробовал версию (см. ниже), одну из которых ссылается на доменный класс, а другую — на встроенную. проблема сохраняется. но я что-то не то сделал. единственная разница, которую я вижу, заключается в использовании geoAddress, a.save() до того, как я перешел в конструктор места проведения. Если я удалю этот a.save() и позволю транзитивному сохранению использовать v.save(flush:true), а затем удалю место проведения - тогда встроенное местоположение исчезнет, ​​и тест сработает! поэтому, если я сохраняю адрес напрямую, он терпит неудачу, а если нет, он работает нормально - person WILLIAM WOODMAN; 11.02.2017
comment
я запускаю эти тесты на grails 3.2.5, groovy 2.4.7 и java 121 - person WILLIAM WOODMAN; 11.02.2017
comment
Попробуйте добавить GrailsWebMockUtil.bindMockWebRequest() перед тестом. Привязка вложенных сущностей является частью подключаемого модуля контроллера. См. проблему + обсуждение, которое я создал по этому поводу на github.com/grails/grails. -ядро/вопросы/10436 - person Dennie de Lange; 15.02.2017
comment
попробовал это - не имело никакого значения, что так всегда - тест терпит неудачу, если я сохраняю адрес, прежде чем добавить его в место проведения и сохранить место проведения. Все, что я могу думать об этом, это то, что геолокация предназначена для встроенного определения. поэтому сначала сохранение одного за пределами места проведения, а затем сохранение в месте проведения запутывается в том, какие строки его используют. очень странно. Попробую настроить в bootstrap и посмотреть, смогу ли я увидеть, что он делает с проводником h2 db - может быть, я что-то увижу - person WILLIAM WOODMAN; 15.02.2017
comment
У вас есть пример на git или что-то в этом роде? - person Dennie de Lange; 16.02.2017
comment
разместил его в git hub по адресу github.com/woodmawa/coffeeCoffeeShopApp - скользкий палец с двойным кофе - person WILLIAM WOODMAN; 16.02.2017
comment
Удалите static embedded =['location'] из места проведения и снова запустите тесты. - person Dennie de Lange; 17.02.2017

хорошо - я внимательно прочитал и искал разницу - и это происходит, если я сохраняю встроенный GeoAddress перед тем, как перейти к конструктору места проведения, как это (модифицированный простой тест)

когда я добавлю дополнительный a.save() после создания GeoAddress, тест завершится ошибкой. если я закомментирую сохранение и перезапущу - все работает нормально. Не уверен, что это фича или баг. Место проведения должно выполнять транзитивное сохранение, так как GeoAddress имеет объявление stati ownTo = Venue.

   //new test - reuse embedded  entity - works
    void "test with GeoLocation" () {
        when: ""
        GeoAddress a = new GeoAddress(town:"ipswich")
        a.save()
        Venue v = new Venue (name: "bistro", location: a)
        assert v.save(flush:true)

        Venue lookupVenue = Venue.get(v.id)

        GeoAddress ta = lookupVenue.location
        assert ta.town == "ipswich"

        //try delete
        v.delete (flush:true)


        then : " retrieve temp"
        GeoAddress.findAll().size() == 0
 }

если кто-нибудь может прокомментировать для меня ошибку и функцию - тогда, если необходимо, я могу поднять ошибку в проекте Grails, чтобы исправить ее. В противном случае мне просто нужно тщательно протестировать и убедиться, что я делаю правильные вещи в своем коде.

person WILLIAM WOODMAN    schedule 11.02.2017