Skip to content

Data Service query methods ignore class-level @Transactional(connection) - @Where and DetachedCriteria operations route to wrong datasource #15416

@jamesfredley

Description

@jamesfredley

Summary

Data Service query methods that use DetachedCriteria internally ignore the class-level @Transactional(connection = '...') annotation and silently route queries to the default datasource instead of the specified connection. This affects @Where-annotated methods and all methods implemented via AbstractDetachedCriteriaServiceImplementor (including count(), list(), findAll*(), findOneBy*(), etc.).

The save() method is NOT affected because AbstractSaveImplementer correctly uses findConnectionId(abstractMethodNode).

Root Cause

Two implementers call findConnectionId(newMethodNode) instead of findConnectionId(abstractMethodNode):

  1. AbstractWhereImplementer.groovy line 99:

    Expression connectionId = findConnectionId(newMethodNode)  // BUG
  2. AbstractDetachedCriteriaServiceImplementor.groovy line 78:

    Expression connectionId = findConnectionId(newMethodNode)  // BUG

findConnectionId resolves the @Transactional annotation via TransactionalTransform.findTransactionalAnnotation(methodNode), which falls back to methodNode.getDeclaringClass(). The newMethodNode (the generated implementation method) declares on the generated $ServiceImplementation class, which does NOT carry the @Transactional annotation. The abstractMethodNode (the original interface/abstract class method) declares on the class that has the annotation.

This causes findConnectionId to return null, so the withConnection(connectionId) call on the DetachedCriteria is never added, and queries run against the default datasource.

Affected Code

  • grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractWhereImplementer.groovy line 99
  • grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractDetachedCriteriaServiceImplementor.groovy line 78

Affected Data Service Methods

Any Data Service method that goes through AbstractDetachedCriteriaServiceImplementor or AbstractWhereImplementer:

  • @Where-annotated methods
  • count() / countBy*()
  • list() / find() / findAll*()
  • findBy*() / findOneBy*()
  • Any method returning domain class iterables

NOT affected: save() (uses AbstractSaveImplementer which correctly uses abstractMethodNode)

Example

@Service(Metric)
@Transactional(connection = 'secondary')
interface MetricDataService {

    Metric save(String name, Double amount)  // WORKS - saves to secondary

    @Where({ amount >= minAmount })
    List<Metric> findByMinAmount(Double minAmount)  // BUG - queries default
}

findByMinAmount() will query the default datasource instead of secondary.

Reproducer

https://github.com/jamesfredley/grails-15416-where-connection-routing

git clone https://github.com/jamesfredley/grails-15416-where-connection-routing.git
cd grails-15416-where-connection-routing
./gradlew integrationTest

The test passes by asserting the buggy behavior: the @Where query returns data from the default datasource instead of secondary. See inline comments in the test for what correct behavior should be.

Fix

Change findConnectionId(newMethodNode) to findConnectionId(abstractMethodNode) in both files:

AbstractWhereImplementer.groovy line 99:

// Before (bug):
Expression connectionId = findConnectionId(newMethodNode)
// After (fix):
Expression connectionId = findConnectionId(abstractMethodNode)

AbstractDetachedCriteriaServiceImplementor.groovy line 78:

// Before (bug):
Expression connectionId = findConnectionId(newMethodNode)
// After (fix):
Expression connectionId = findConnectionId(abstractMethodNode)

Both abstractMethodNode and newMethodNode are available in the doImplement method signature.

Context

Discovered while fixing a similar bug pattern in PR #15395. That PR introduced findConnectionId calls in several other implementers (SaveImplementer, DeleteImplementer, FindAndDeleteImplementer) which correctly use abstractMethodNode. The two implementers listed above pre-existed on the 7.0.x branch with the incorrect newMethodNode usage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions