Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ def results = c.list {
like('code','995%')
}
----

TIP: The namespace syntax (e.g., `ZipCode.auditing.get(42)`) uses dynamic dispatch and is not compatible with `@CompileStatic`. For statically compiled code, use `GormEnhancer.findStaticApi(ZipCode, 'auditing')` to obtain a `GormStaticApi` handle with the same methods routed to the specified datasource, or use a Data Service with `@Transactional(connection = 'auditing')` for automatic routing. See the <<dataServicesMultipleDataSources,Data Services and Multiple Datasources>> section for the full pattern.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a bug? Recommending people to use GormStaticApi I think is a horrible idea. It's an implementation detail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this back in draft, let's see where we land after fixing the few open bugs and hopefully there is a normal path that is fully unlocked to do this with @CompileStatic

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ include::mappingDomainsToDataSources.adoc[]

include::dataSourceNamespaces.adoc[]

=== Using Data Services with Multiple Datasources

GORM Data Services support multi-datasource routing via `@Transactional(connection = 'connectionName')` on the abstract class. All auto-implemented methods - `get()`, `save()`, `delete()`, `findBy*()`, `countBy*()` - route to the specified datasource automatically. This works with `@CompileStatic`, injected `@Service` properties, and `MultiTenant` domain classes. See the <<dataServicesMultipleDataSources,Data Services and Multiple Datasources>> section for the full pattern, including `GormEnhancer.findStaticApi()` for complex queries.

[[connectionSources]]
=== The ConnectionSources API

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ include::writeOperations.adoc[]

include::serviceValidation.adoc[]

[[dataServicesMultipleDataSources]]
=== Data Services and Multiple Datasources

include::multipleDataSources.adoc[]

=== RxJava Support

include::rxServices.adoc[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
////
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
////

When using Data Services with <<multipleDataSources,multiple datasources>>, the service must declare which connection to use via the `connection` parameter of `@Transactional`.

==== Routing to a Secondary Datasource

Given a domain class mapped to a secondary datasource:

[source,groovy]
----
class Book {

String title
String author

static mapping = {
datasource 'books'
}
}
----

Define an interface for your data access methods and an abstract class that declares the connection:

[source,groovy]
----
import grails.gorm.services.Service

interface BookDataService {

Book get(Serializable id)

Book save(Book book)

void delete(Serializable id)

List<Book> findAllByAuthor(String author)

Long count()
}
----

[source,groovy]
----
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Service(Book)
@Transactional(connection = 'books')
abstract class BookService implements BookDataService {
// All interface methods are auto-implemented by GORM
// and route to the 'books' datasource automatically.
}
----

The `@Transactional(connection = 'books')` annotation on the abstract class ensures that all auto-implemented methods (`get`, `save`, `delete`, `findBy*`, `countBy*`, etc.) route to the `books` datasource. Without this annotation, queries silently route to the default datasource.

NOTE: The `@Service(Book)` annotation identifies the domain class but does not determine which datasource to use. Even if `Book` declares `datasource 'books'` in its mapping block, you must specify `@Transactional(connection = 'books')` on the abstract class to route operations to the correct datasource.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like we should open a feature request.


==== How Connection Routing Works

When GORM compiles a `@Service` abstract class, the `ServiceTransformation` AST transform:

1. Copies the `@Transactional(connection = 'books')` annotation from the abstract class to the generated implementation class
2. For each auto-implemented method, resolves the connection identifier via `findConnectionId()`
3. Generates method bodies that use the appropriate connection - `GormEnhancer.findStaticApi(Book, 'books')` for CRUD operations and `DetachedCriteria.withConnection('books')` for finder queries
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should give this implementation detail. We don't document internal classes as it could cause people to use them thinking they're a public api


This means auto-implemented methods like `get()`, `save()`, `delete()`, `findBy*()`, and `countBy*()` all respect the connection parameter without requiring manual implementations.

==== Complex Queries with GormEnhancer

Auto-implemented Data Service methods cover most query patterns, including dynamic finders with comparators, pagination, and property projections. For queries that require HQL, criteria builders, or aggregate functions, use `GormEnhancer.findStaticApi()` to obtain a statically compiled API handle for the target datasource:

[source,groovy]
----
import groovy.transform.CompileStatic
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import org.grails.datastore.gorm.GormEnhancer
import org.grails.datastore.gorm.GormStaticApi

@CompileStatic
@Service(Book)
@Transactional(connection = 'books')
abstract class BookService implements BookDataService {

// Auto-implemented methods from interface are inherited
// and route to 'books' datasource automatically.

private GormStaticApi<Book> getBooksApi() {
GormEnhancer.findStaticApi(Book, 'books')
}

List getTopAuthors(int limit) {
booksApi.executeQuery('''
SELECT b.author, COUNT(b) as bookCount
FROM Book b
GROUP BY b.author
ORDER BY bookCount DESC
''', Collections.emptyMap(), [max: limit])
}

List<Book> searchWithCriteria(String titlePattern, String author) {
booksApi.createCriteria().list {
like('title', "%${titlePattern}%")
eq('author', author)
order('title', 'asc')
} as List<Book>
}
}
----

The `GormStaticApi` returned by `findStaticApi()` provides these methods, all routed to the specified datasource:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, we shouldn't be referencing the implementation detail.


[cols="1,2"]
|===
| Method | Description

| `executeQuery(String hql, Map params)`
| HQL/JPQL queries

| `executeUpdate(String hql, Map params)`
| Bulk UPDATE/DELETE statements

| `withCriteria(Closure criteria)`
| Criteria query

| `createCriteria()`
| Criteria builder for complex queries

| `where(Closure query)`
| Where query

| `withTransaction(Closure action)`
| Manual transaction management

| `count()`
| Total record count

| `get(Serializable id)`
| Find by primary key

| `list(Map params)`
| Paginated list

| `save(Object instance)`
| Persist an instance
|===

NOTE: `GormEnhancer.findStaticApi()` is the statically compiled equivalent of the <<dataSourceNamespaces,namespace syntax>> (e.g., `Book.books.get(42)`). Unlike the namespace syntax, it works under `@CompileStatic` without requiring `@CompileDynamic` on individual methods.

==== Consuming Multi-Datasource Data Services

Other services inject the Data Service interface type. Spring resolves the abstract class bean automatically:

[source,groovy]
----
import groovy.transform.CompileStatic

@CompileStatic
class LibraryService {

BookDataService bookDataService // injected automatically

Map getLibraryStats() {
Long totalBooks = bookDataService.count()
List<Book> recentBooks = bookDataService.findAllByAuthor('Tolkien')
[total: totalBooks, tolkienBooks: recentBooks.size()]
}
}
----

The consuming service does not need `@Transactional(connection = 'books')`. The Data Service handles datasource routing internally.

Data Services can also inject other Data Services. `@CompileStatic` works on `@Service` abstract classes that declare `@Service`-typed properties:

[source,groovy]
----
import grails.gorm.services.Service

interface AuthorDataService {

Author get(Serializable id)

Author save(Author author)
}
----

[source,groovy]
----
import groovy.transform.CompileStatic
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional

@CompileStatic
@Service(Author)
@Transactional(connection = 'books')
abstract class AuthorService implements AuthorDataService {

BookDataService bookDataService // injected @Service property

Map getAuthorWithBooks(Serializable authorId) {
Author author = get(authorId)
List<Book> books = bookDataService.findAllByAuthor(author.name)
[author: author, books: books]
}
}
----

When the Spring context initializes the generated implementation class, it eagerly populates all `@Service`-typed properties via `datastore.getService()`. By the time any user code runs, injected Data Services are fully available.

==== Multi-Tenancy with Multiple Datasources

Domain classes that implement `GormEntity` with the `MultiTenant` trait and declare an explicit non-default datasource (e.g., `datasource 'analytics'`) route correctly through Data Services. GORM's `GormEnhancer` preserves explicit datasource qualifiers for multi-tenant entities, so Data Services using `@Transactional(connection = 'analytics')` work the same way as for non-tenant domain classes:

[source,groovy]
----
import grails.gorm.MultiTenant

class Metric implements MultiTenant<Metric> {

String name
BigDecimal value

static mapping = {
datasource 'analytics'
}
}
----

[source,groovy]
----
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Service(Metric)
@Transactional(connection = 'analytics')
abstract class MetricService {

abstract Metric get(Serializable id)

abstract Metric save(Metric metric)

abstract List<Metric> list()
}
----

The `connection` parameter on the abstract class routes all auto-implemented operations - including `save()`, `get()`, and `delete()` - to the `analytics` datasource, regardless of multi-tenancy mode (DATABASE or DISCRIMINATOR).
Loading
Loading