0

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 didn't work (I have to save the location its no longer transient 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 don't 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. Why might my cascade actions for Venue.save() and delete() not cascade to my embedded location (GeoAddress) entry?

halfer
  • 19,824
  • 17
  • 99
  • 186
WILLIAM WOODMAN
  • 1,185
  • 5
  • 19
  • 36

4 Answers4

0

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

V H
  • 8,382
  • 2
  • 28
  • 48
  • i tried like this - and deleted the location prior to delete the venue and the test then passes. If i remove the loc delete it fails. 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 – WILLIAM WOODMAN Feb 06 '17 at 12:58
  • and if you changed the `hasOne` to `GeoAddress location` as a defined object in the domain class then made it `location(nullable:true)` does it get removed then ? – V H Feb 07 '17 at 10:15
  • just at work at the mo - will have a to do a small restructure to try that - will update you when i get first moment to do so – WILLIAM WOODMAN Feb 07 '17 at 13:32
  • http://stackoverflow.com/questions/12900221/grails-hasone-vs-direct-member-variable depends on your needs really – V H Feb 07 '17 at 14:07
  • I was thinking about this in the back of my mind and it is all explained on that 2nd answer. Sounds like if you want this behaviour then take my advice and switch from hasOne to direct object relation. since hasOne can be a lot of parents having one of the same and therefore the rule of cascading a delete would not apply. – V H Feb 07 '17 at 16:44
0

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

WILLIAM WOODMAN
  • 1,185
  • 5
  • 19
  • 36
0

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.

Dennie de Lange
  • 2,776
  • 2
  • 18
  • 31
  • not sure it is its something else. i tried a version (see below) one referencing a domainclass and one using embedded. problem perisists. but i did not something. the only difference i can see appears to be the use of geoAddress, a.save() before i passed into the venue constructor. If i remove that a.save() and allow transitive persist using v.save(flush:true), and then delete the venue - then the embedded location disappears and the test works ! so if i do save on address directly it fails and if i dont it works fine – WILLIAM WOODMAN Feb 11 '17 at 17:13
  • i'm running these tests on grails 3.2.5, groovy 2.4.7, and java 121 – WILLIAM WOODMAN Feb 11 '17 at 17:31
  • Try to add GrailsWebMockUtil.bindMockWebRequest() before the test. The binding of nested entities is part of the controller plugin. Please see the issue +discussion I've created about this at https://github.com/grails/grails-core/issues/10436 – Dennie de Lange Feb 15 '17 at 15:02
  • tried that - made no difference what so ever - test fails if i save the address before i add it to the venue and save the venue. All i can think of this that the geoLocation is intended to be an embedded definitoin. so saving one outside of a venue first, and then storing in the venue is getting messed up on which rows its using. very odd. Will have a go at setting up in bootstrap and see if i can see what it does with the h2 db explorer - maybe i can see somthing – WILLIAM WOODMAN Feb 15 '17 at 15:45
  • Do you have the example on git or something? – Dennie de Lange Feb 15 '17 at 21:36
  • posted it up to git hub at https://github.com/woodmawa/coffeeCoffeeShopApp - slippy finger with the double coffee – WILLIAM WOODMAN Feb 16 '17 at 16:07
  • Remove static embedded =['location'] from Venue and run your tests again. – Dennie de Lange Feb 16 '17 at 22:30
0

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

WILLIAM WOODMAN
  • 1,185
  • 5
  • 19
  • 36