jdaugherty commented on code in PR #15406:
URL: https://github.com/apache/grails-core/pull/15406#discussion_r2828817239


##########
grails-data-hibernate5/docs/src/docs/asciidoc/services/multipleDataSources.adoc:
##########
@@ -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.
+
+==== 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

Review Comment:
   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



##########
grails-data-hibernate5/docs/src/docs/asciidoc/multipleDataSources/dataSourceNamespaces.adoc:
##########
@@ -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.

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



##########
grails-data-hibernate5/docs/src/docs/asciidoc/services/multipleDataSources.adoc:
##########
@@ -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.
+
+==== 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
+
+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:

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



##########
grails-data-hibernate5/docs/src/docs/asciidoc/services/multipleDataSources.adoc:
##########
@@ -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.

Review Comment:
   seems like we should open a feature request.



##########
grails-doc/src/en/guide/conf/dataSource/multipleDatasources.adoc:
##########
@@ -260,6 +260,262 @@ Note that the datasource specified in a service has no 
bearing on which datasour
 
 If you have a `Foo` domain class in `dataSource1` and a `Bar` domain class in 
`dataSource2`, if `WahooService` uses `dataSource1`, a service method that 
saves a new `Foo` and a new `Bar` will only be transactional for `Foo` since 
they share the same datasource. The transaction won't affect the `Bar` 
instance. If you want both to be transactional you'd need to use two services 
and XA datasources for two-phase commit, e.g. with the Atomikos plugin.
 
+
+==== GORM Data Services and Multiple Datasources
+
+
+GORM Data Services provide a type-safe, statically compiled approach to 
multi-datasource access. When a `@Service` abstract class needs to route 
operations to a non-default datasource, declare the target connection with 
`@Transactional(connection = 'name')`.
+
+
+===== Basic Pattern
+
+
+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.
+
+
+===== 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

Review Comment:
   Same feedback



##########
grails-doc/src/en/guide/conf/dataSource/multipleDatasources.adoc:
##########
@@ -260,6 +260,262 @@ Note that the datasource specified in a service has no 
bearing on which datasour
 
 If you have a `Foo` domain class in `dataSource1` and a `Bar` domain class in 
`dataSource2`, if `WahooService` uses `dataSource1`, a service method that 
saves a new `Foo` and a new `Bar` will only be transactional for `Foo` since 
they share the same datasource. The transaction won't affect the `Bar` 
instance. If you want both to be transactional you'd need to use two services 
and XA datasources for two-phase commit, e.g. with the Atomikos plugin.
 
+
+==== GORM Data Services and Multiple Datasources
+
+
+GORM Data Services provide a type-safe, statically compiled approach to 
multi-datasource access. When a `@Service` abstract class needs to route 
operations to a non-default datasource, declare the target connection with 
`@Transactional(connection = 'name')`.
+
+
+===== Basic Pattern
+
+
+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.
+
+
+===== 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
+
+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 (`Between`, `Like`, `InList`, `IsNull`, etc.), 
pagination via a `Map` parameter, 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:
+
+[cols="1,2"]
+|===
+| Method | Description
+
+| `executeQuery(String hql, Map params)`
+| HQL/JPQL queries
+
+| `executeUpdate(String hql, Map params)`
+| Bulk UPDATE/DELETE statements
+
+| `find(String hql, Map params)`
+| Single-result HQL query
+
+| `findAll()` / `findAll(Map params)`
+| List all records with optional pagination
+
+| `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 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. If the consuming 
service coordinates writes across multiple Data Services, annotate the 
orchestrating method with `@Transactional` to ensure atomicity on the default 
datasource.
+
+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:

Review Comment:
   Let's not reference the GormEnhancer in the docs.  It's an implementation 
detail.



-- 
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]

Reply via email to