can't get cascade save nor delete on embedded

2019-09-06 02:11发布

问题:

i've created two simple grails V3 domain classes where location is embedded attribute type in parent Venue like this

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

however when i write an integration test - i found the cascade create for location didnt work (i have to save the location its no longer tranient before passing to venue. also when i run a delete on the venue with flush:true enabled, and query for the address i still get the returned embedded address - i thought with the flush:true i'd see my GeoAddress cascade delete, but my test fails as i dont get a null when using GeoAddress.get(loc.id) as i was expecting

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

I thought i had set up this correctly but clearly i haven't. can any elucidate as to why my cascade actions for Venue.save(), and delete() don't cascade to my embedded location (GeoAddress) entry.

thanks in advance

回答1:

If I understood it correctly

cascade: "all-delete-orphan"

Is only required if you have a hasMany=[something:Something]

In your case it is hasOne or GeoAddress location would probably be a better setup if i were to create such a relation. I know there is a slight variation between both.

Anyhow you are testing so all theoretical. I think you need to capture the errors for a start to work out why it hasn't cascaded the expected behaviour. so either

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

or wrap it around

try catch block

. I had a similar issue with a hasMany relation, and it was due to record shared with other tables due to the setup of the underlying hasMany table relations itself. The trick was to just remove the entry from the object itself :

lookupVenue .removeFromLocation(loc)

As i say this was a hasMany relation



回答2:

very weird and too tired to figure it out now. I tried the external entity and the embedded - see adjusted model below.

I wrote too new tests that both work - but the original test doesn't. Im doing something weird - just have not spotted it. The two new tests do the same flow - just the variables are different - both work. so the problem is with the first test.

revised test

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

revised subject under test - Venue.groovy with emebbed 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
    }
}

new external version of address/location for hack. dummed down version of geoAddress with same beongsTo/constraint logic

class TempLocation {

    String name

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

    static constraints = {
        name nullable:true
    }
}

will try and re- read on train - not sure why 1st test is failing but next two work fine .... off to bed - too tired



回答3:

I think this is a misconfiguration. Embedded means that the entity is embedded inside the domainclass. Usually this a normal POJO and left outside of the domainclass folder (and in the src/groovy folder). All fields of the embedded entity are included in the table of the embedding entity. The hasone sets a relation between two domainclass entities. So either use embedded or use hasOne, but don't use both at the same time.

Furthermore, there was an issue with cascade saving deeply nested entities, this is solved in 3.2.5.



回答4:

ok - i read closely and looked for the difference - and it occurs if i save the embedded GeoAddress before i pass to the venue constructor like this (modified simple test)

when i add the extra a.save(), after creating the GeoAddress, the test will fail. if i comment the save out and rerun - it works fine. Not sure if this is a feature or a bug. Venue should do transitive save as the GeoAddress has an stati belongsTo = Venue declaration.

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

if any one can comment on bug vs feature for me - then if necessary i can raise a bug on the grails project to get it fixed. Else i'll just have to test carefully and make sure i do the right thing in my code