Skip to content

AbstractDetachedCriteria.clone() does not copy connectionName, alias, lazyQuery, or associationCriteriaMap #15422

@jamesfredley

Description

@jamesfredley

Description

AbstractDetachedCriteria.clone() copies 8 fields (criteria, projections, projectionList, orders, defaultMax, defaultOffset, fetchStrategies, joinTypes) but omits 4 others: connectionName, alias, lazyQuery, and associationCriteriaMap.

This causes withConnection() settings to be silently lost whenever a chained method calls clone() internally, because the cloned instance reverts connectionName to ConnectionSource.DEFAULT.

Affected Methods

Any method that calls clone() internally loses the connection setting:

  • where() / whereLazy() (line 812-825)
  • max() (line 895-898)
  • offset() (line 907-910)
  • sort() (line 919+)

Steps to Reproduce

Reproducer app: https://github.com/jamesfredley/grails-detachedcriteria-clone-connection

git clone https://github.com/jamesfredley/grails-detachedcriteria-clone-connection.git
cd grails-detachedcriteria-clone-connection
./gradlew integrationTest

Minimal code example

// withConnection('secondary') sets connectionName on the returned criteria
DetachedCriteria<Product> criteria = new DetachedCriteria<>(Product)
    .withConnection('secondary')   // clone() + set connectionName = 'secondary'
    .where { price > 100.0 }       // clone() again -- connectionName lost!

// Query runs against DEFAULT datasource instead of secondary
criteria.list()

Expected Behavior

After withConnection('secondary').where { ... }, the resulting criteria should still have connectionName == 'secondary' and queries should route to the secondary datasource.

Actual Behavior

where() internally calls clone() which creates a new instance without copying connectionName. The resulting criteria has connectionName == 'DEFAULT', so queries silently route to the wrong datasource.

Root Cause

In AbstractDetachedCriteria.groovy lines 873-885:

protected AbstractDetachedCriteria<T> clone() {
    AbstractDetachedCriteria criteria = newInstance()
    criteria.@criteria = new ArrayList(this.criteria)
    final projections = new ArrayList(this.projections)
    criteria.@projections = projections
    criteria.projectionList = new DetachedProjections(projections)
    criteria.@orders = new ArrayList(this.orders)
    criteria.defaultMax = defaultMax
    criteria.defaultOffset = defaultOffset
    criteria.@fetchStrategies = new HashMap<>(this.fetchStrategies)
    criteria.@joinTypes = new HashMap<>(this.joinTypes)
    return criteria
    // Missing: connectionName, alias, lazyQuery, associationCriteriaMap
}

The withConnection() method (line 865-869) works correctly in isolation - it calls clone() then sets connectionName. But any subsequent chained method that also calls clone() creates a fresh copy without the connection setting.

Suggested Fix

Add the missing field copies to clone():

protected AbstractDetachedCriteria<T> clone() {
    AbstractDetachedCriteria criteria = newInstance()
    criteria.@criteria = new ArrayList(this.criteria)
    final projections = new ArrayList(this.projections)
    criteria.@projections = projections
    criteria.projectionList = new DetachedProjections(projections)
    criteria.@orders = new ArrayList(this.orders)
    criteria.defaultMax = defaultMax
    criteria.defaultOffset = defaultOffset
    criteria.@fetchStrategies = new HashMap<>(this.fetchStrategies)
    criteria.@joinTypes = new HashMap<>(this.joinTypes)
    criteria.connectionName = this.connectionName
    criteria.alias = this.alias
    criteria.lazyQuery = this.lazyQuery
    criteria.@associationCriteriaMap = new HashMap<>(this.associationCriteriaMap)
    return criteria
}

Environment

  • Grails: 7.0.7
  • GORM Hibernate: via grails-data-hibernate5
  • JDK: 17

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

In Progress

Relationships

None yet

Development

No branches or pull requests

Issue actions