Can't get where query to do eager loading of one-to-many association

血红的双手。 提交于 2019-12-11 12:59:34

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!