问题
I am using Grails v3.3.9.
I cannot get queries to eagerly load one-to-many association. Tried all sorts of ways.
This is posted relative to trying to use this in unit testing (fails on app at runtime as well).
Scenario: I have two domain classes, one called 'OrgRoleInstance' which has Collection<Site> sites, and static hasMany =[sites:Site], and the other called 'Site' which has a static belongsTo = [org:OrgRoleInstance].
This is birdirectional one-to-many from orgs to sites.
I create a new unit test using the new DomainUnitTest trait. In the test setup I create three orgs and add one site each, then I add one last site to the 3rd org (org "C"). Setup works fine and all instances are persisted.
In the where query test, I look for an Org that has 2 sites (which is org "C").
When I run the test in debug, the where query returns an array of size 1 containing org "C", so the test in the where clause can see that sites collection is not null - so far so good.
I also do a direct Site.get(4) to get the last site. I do an assert check to check that the sites.org ref is same instance returned from the where query. This is true and passes.
However when you look at the orgs[0] entry the sites collection is null. That simple println fails with accessing null sites property returned from the query.
class OrgRoleInstanceSpec extends Specification implements DomainUnitTest<OrgRoleInstance> {
def setup() {
OrgRoleInstance
List<OrgRoleInstance> orgs = []
["A","B","C"].each {
OrgRoleInstance org = new OrgRoleInstance(name:it, role:OrgRoleInstance.OrgRoleType.Customer)
org.addToSites(new Site( name: "$it's Head Office", status:"open", org:org))
orgs << org
}
orgs[2].addToSites (new Site( name: "${orgs[2].name}'s Branch Office", status:"open", org:orgs[2]))
OrgRoleInstance.saveAll(orgs)
assert OrgRoleInstance.count() == 3
println "# of sites : " + Site.count()
assert Site.count() == 4
assert Site.get(2).org.id == orgs[1].id
}
void "where query and individual get " () {
given :
def orgs = OrgRoleInstance.where {
sites.size() == 2
}.list(fetch:[sites:"eager"])
def org = OrgRoleInstance.get(2)
List orgSites = org.sites
def branch = Site.get(4)
assert branch.org.is (orgs[0]) //assert is true
println orgs[0].sites[1].name //orgs[0].sites is null !
expect:
orgs.size() == 1
}
}
I have tried this withCriteria, with basic findAll(fetch:[sites:"eager") etc.
However I try this I cannot get query to return eager populated collection of sites.
I want to do that in the queries rather than by mapping clause in OrgeRoleInstance domain class
Update/observations
I enabled SQL logging and started the grails console. I then typed the following script (using my bootstrap data).
Manually typed console script
import com.softwood.domain.*
def orgs = OrgRoleInstance.where {
id == 4
sites{}
}.list() /* (fetch:[sites:"eager"]) */
println orgs[0].sites
which when run outputs the following - which basically does seem to run eager selection - and I get the orgs.sites result with populated entries using an inner join:
groovy> import com.softwood.domain.*
groovy> def orgs = OrgRoleInstance.where {
groovy> id == 4
groovy> sites{}
groovy> }.list() /* (fetch:[sites:"eager"]) */
groovy> println orgs[0].sites
2019-01-23 14:02:00.923 DEBUG --- [ Thread-18] org.hibernate.SQL :
select
this_.id as id1_16_1_,
this_.version as version2_16_1_,
this_.role as role3_16_1_,
this_.name as name4_16_1_,
sites_alia1_.id as id1_21_0_,
sites_alia1_.version as version2_21_0_,
sites_alia1_.org_id as org_id3_21_0_,
sites_alia1_.name as name4_21_0_,
sites_alia1_.status as status5_21_0_
from
org_role_instance this_
inner join
site sites_alia1_
on this_.id=sites_alia1_.org_id
where
this_.id=?
2019-01-23 14:02:00.923 TRACE --- [ Thread-18] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [4]
2019-01-23 14:02:00.923 DEBUG --- [ Thread-18] org.hibernate.SQL :
select
sites0_.org_id as org_id3_21_0_,
sites0_.id as id1_21_0_,
sites0_.id as id1_21_1_,
sites0_.version as version2_21_1_,
sites0_.org_id as org_id3_21_1_,
sites0_.name as name4_21_1_,
sites0_.status as status5_21_1_
from
site sites0_
where
sites0_.org_id=?
2019-01-23 14:02:00.923 TRACE --- [ Thread-18] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [4]
[Site:(name : 1 Barkley Square) belonging to org: com.softwood.domain.OrgRoleInstance : 4, Site:(name : 10 South Close) belonging to org: com.softwood.domain.OrgRoleInstance : 4]
So why doesn't this work in unit testing?
Another update
I went back to grails console and managed to get this criteria query to work. Point 1 - you have to have imported org.hibernate.FetchMode, then the fetchMode function at root level in withCriteria closure will work now. Lastly just do empty closure on the collection to force the eager query.
import com.softwood.domain.*
import org.hibernate.FetchMode
def orgs = OrgRoleInstance.withCriteria {
fetchMode ("sites", FetchMode.SELECT)
sites{}
}
println orgs[0].sites
However this does not work in a unit test. The same query in unit test like this
void "criteria query " () {
given:
OrgRoleInstance org
org = OrgRoleInstance.withCriteria (uniqueResult: true) {
fetchMode ("sites", FetchMode.SELECT)
sites{}
}
expect:
org.id == 3
org.sites.size() == 2
}
fails with this error
groovy.lang.MissingMethodException: No signature of method: grails.gorm.CriteriaBuilder.fetchMode() is applicable for argument types: (java.lang.String, org.hibernate.FetchMode) values: [sites, SELECT]
I suspect therefore that unit testing criteriaQueries or where queries using the new grails DomainUnitTest<T> trait doesn't support join/eager queries etc.
It maybe that you're forced to integration tests with real DB to test queries across tables. If anyone can state categorically state that the new unit testing traits don't work for join/eager queries that might help me.
回答1:
The output of this investigation is that you can't use unit testing for domain model queries that try to join tables using the new unit testing traits.
If you want to query testing you have to do it as integration tests.
When I re-coded my queries this time as integration tests, allowing for bootstrap data that gets loaded before any data in the spock setup() method, then the queries start to work and including eager queries for data.
One thing to remember is that the setup() method doesn't do a rollback (so that the data stays throughout the tests) so you should do a manual clear up at the end if your database is not getting dropped.
So this is no coded as integration test and appears to work!
package com.softwood.domain
import grails.testing.mixin.integration.Integration
import grails.transaction.*
import org.hibernate.FetchMode
import org.hibernate.LazyInitializationException
import spock.lang.Shared
import spock.lang.Specification
@Integration
@Rollback
class OrgRoleInstanceIntegSpecSpec extends Specification {
//operates on separate transaction thats not rolled back
@Shared
List<OrgRoleInstance> orgs = []
@Shared
List<OrgRoleInstance> sites = []
@Shared
NetworkDomain netDomain
@Shared
def bootstrapPreExistingOrgsCount, bootstrapPreExistingSitesCount
//runs in transaction thats not rolled back for each test
def setup() {
def site
bootstrapPreExistingOrgsCount = OrgRoleInstance.count()
bootstrapPreExistingSitesCount = Site.count()
//println "pre exist orgs:$boostrapPreExistingOrgsCount + pre exist sites: $boostrapPreExistingSitesCount"
assert bootstrapPreExistingOrgsCount == 4
assert bootstrapPreExistingSitesCount == 2
["A","B","C"].each {
OrgRoleInstance org = new OrgRoleInstance(name:"test$it", role:OrgRoleInstance.OrgRoleType.Customer)
org.addToSites(site = new Site( name: "test$it's Head Office", status:"open", org:org))
orgs << org
sites << site
}
orgs[2].addToSites (site = new Site( name: "${orgs[2].name}'s Branch Office", status:"open", org:orgs[2]))
orgs[2].addToDomains(netDomain = new NetworkDomain (name:"corporate WAN", customer:[orgs[2]]))
sites << site
OrgRoleInstance.saveAll(orgs)
assert orgs.size() == 3
assert sites.size() ==4
assert OrgRoleInstance.count() == 3 + bootstrapPreExistingOrgsCount
assert Site.count() == 4 + bootstrapPreExistingSitesCount
assert Site.get(4).org.id == orgs[1].id
println "setup integration test data"
}
//manual cleanup of integration test data
def cleanup() {
orgs.each {OrgRoleInstance org ->
org.sites.each {it.delete()}
org.delete(flush:true)
assert OrgRoleInstance.exists(org.id) == false
}
println "deleted integration test data"
}
void "Orgs list with eager fetch query"() {
given :
def orgs = OrgRoleInstance.list(fetch:[sites:"eager"])
println "org ${orgs[6].name} sites : " + orgs[5].sites
println "test site #2 has org as : " + (Site.list())[3].org
expect :
Site.count() == 4 + bootstrapPreExistingSitesCount
orgs.size() == 3 + bootstrapPreExistingOrgsCount
orgs[5].getName() == "testB"
orgs[5].sites.size() == 1
}
void "orgs where query triggering eager site get"() {
given :
//where clause returns instance of DetachedCriteria, so have to trigger with a list/get etc
def orgs = OrgRoleInstance.where {
name =~ "%testB%" &&
sites{}
}.list()
expect :
orgs.size() == 1
orgs[0].name == "testB"
orgs[0].sites.size() == 1
}
void "withCriteria query with eager site fetch (two selects) " () {
given:
OrgRoleInstance org
//with criteria runs the query for you unlike createCriteria() which returns a detachedCriteria
org = OrgRoleInstance.withCriteria (uniqueResult: true) {
fetchMode ("sites", FetchMode.SELECT)
idEq(7L) //internally wont cast Integer to long, so set it explicitly
sites{}
}
/*def orgs = OrgRoleInstance.withCriteria {
//setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
eq 'name', "B"
//fetchMode 'sites', FetchMode.SELECT
sites{}
}*/
expect:
org.id == 7
org.sites.size() == 2
}
void "detached criteria (with distinct) with eager fetch " () {
given:
def orgs = OrgRoleInstance.createCriteria().listDistinct {
//fetchMode 'sites', FetchMode.SELECT
join 'sites'
sites {
org {
eq 'id', 6L
}
}
}
def site = orgs[0].sites[0]
expect:
orgs.size() == 1
orgs[0].sites.size() == 1
site.name == "testB's Head Office"
}
void "where query on id only without list (fetch eager) " () {
given :
def orgs = OrgRoleInstance.where {
id == 7L
}.list ()
def branch = orgs[0].sites[0]
when:
println "branch "+ branch.name //try and access branch
then:
/*
-- seems to do an eager fetch on sites+domains, even though i didnt ask it to
not quite sure why - separate exploration round that i think
*/
//LazyInitializationException ex = thrown()
orgs.size() == 1
orgs[0].sites.size() == 2
orgs[0].domains.size() == 1
}
}
I hope this saves others from some heartache over why your tests don't work.
Note also that running grails console will fire up the console script app but boots a gorm build with it- so with judicious use of include domain packages - you can try out some test queries interactively against any bootstrap data that got loaded when gorm starts.
Integration tests are more laborious and costly in time to run.
It would be nice (and clever) if the unit testing traits could be enhanced to support query testing.
来源:https://stackoverflow.com/questions/54325664/cant-get-where-query-to-do-eager-loading-of-one-to-many-association