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