jamesfredley opened a new pull request, #15395:
URL: https://github.com/apache/grails-core/pull/15395
## Summary
Auto-implemented CRUD methods on GORM Data Services (`save()`, `delete()`,
`get()`) ignore `@Transactional(connection = 'secondary')` and always route to
the default datasource.
The finder implementers (`FindAllByImplementer`, `FindOneByImplementer`)
already correctly use `findStaticApiForConnectionId()` to route through
`GormEnhancer.findStaticApi()` with the correct connection qualifier. However,
`SaveImplementer`, `DeleteImplementer`, and the get-by-id optimization in
`AbstractDetachedCriteriaServiceImplementor` bypass this mechanism entirely and
call instance/static methods directly on the domain class.
## Bug Description
Given a Data Service with `@Transactional(connection = 'secondary')`:
```groovy
@Service(Metric)
@Transactional(connection = 'secondary')
abstract class MetricService implements MetricDataService {
// All CRUD methods auto-implemented by GORM
}
```
- `findAllBy*()` → correctly routes to secondary (uses
`findStaticApiForConnectionId`)
- `save(Metric m)` → **routes to DEFAULT** (calls `entity.save()` directly)
- `delete(Serializable id)` → **routes to DEFAULT** (calls `obj.delete()`
directly)
- `get(Serializable id)` → **routes to DEFAULT** (calls
`DomainClass.get(id)` directly)
## Root Cause
Three code paths in the auto-implementers skip connection routing:
1. **`SaveImplementer.doImplement()`** — single-entity parameter path
generates `entity.save(failOnError: true)`, which goes through
`GormEntity.save()` → `GormEnhancer.findInstanceApi(class)` without a
connection qualifier.
2. **`AbstractDetachedCriteriaServiceImplementor.doImplement()`** — the
"optimize by id" path generates `DomainClass.get(id)`, a static call that
routes to the default datastore. (The detached criteria query path already
calls `findConnectionId()` + `withConnection()` correctly.)
3. **`DeleteImplementer.implementById()`** — generates `obj.delete()`, which
goes through `GormEntity.delete()` → default instance API.
Note: `AbstractSaveImplementer.bindParametersAndSave()` (the multi-parameter
constructor-style save) already had the correct `findConnectionId()` check —
only the single-entity path in `SaveImplementer` was missing it.
## Fix
Apply the same `findConnectionId()` pattern already used by
`FindAllByImplementer` and `AbstractSaveImplementer.bindParametersAndSave()`:
- **`SaveImplementer`**: When `findConnectionId()` returns a connection,
route through `GormEnhancer.findInstanceApi(DomainClass,
connectionId).save(entity, args)` instead of `entity.save(args)`
- **`AbstractDetachedCriteriaServiceImplementor`**: When
`findConnectionId()` returns a connection, route get-by-id through
`GormEnhancer.findStaticApi(DomainClass, connectionId).get(id)` instead of
`DomainClass.get(id)`
- **`DeleteImplementer`**: When `findConnectionId()` returns a connection,
route through `GormEnhancer.findInstanceApi(DomainClass,
connectionId).delete(entity)` instead of `entity.delete()`
## Example Application
https://github.com/jamesfredley/grails-auto-crud-datasource-routing
A minimal Grails 7 app with two H2 in-memory databases that proves the bug.
The METRIC table exists only on the secondary database. `MetricService` has
`@Transactional(connection = 'secondary')`, but auto-implemented `save()` tries
to write to primary (where there's no METRIC table), causing a SQL error.
Run `./gradlew bootRun` and visit `http://localhost:8080/bugDemo/index` to
see the verified output.
## Environment Information
- Grails 7.0.7
- GORM 7.0.7
- Spring Boot 3.5.10
- Groovy 4.0.30
- JDK 17+
## Version
7.0.7
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]