|
| 1 | +//// |
| 2 | +Licensed to the Apache Software Foundation (ASF) under one |
| 3 | +or more contributor license agreements. See the NOTICE file |
| 4 | +distributed with this work for additional information |
| 5 | +regarding copyright ownership. The ASF licenses this file |
| 6 | +to you under the Apache License, Version 2.0 (the |
| 7 | +"License"); you may not use this file except in compliance |
| 8 | +with the License. You may obtain a copy of the License at |
| 9 | + |
| 10 | +https://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + |
| 12 | +Unless required by applicable law or agreed to in writing, |
| 13 | +software distributed under the License is distributed on an |
| 14 | +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | +KIND, either express or implied. See the License for the |
| 16 | +specific language governing permissions and limitations |
| 17 | +under the License. |
| 18 | +//// |
| 19 | + |
| 20 | +When using Data Services with <<multipleDataSources,multiple datasources>>, the service must declare which connection to use via the `connection` parameter of `@Transactional`. |
| 21 | + |
| 22 | +==== Routing to a Secondary Datasource |
| 23 | + |
| 24 | +Given a domain class mapped to a secondary datasource: |
| 25 | + |
| 26 | +[source,groovy] |
| 27 | +---- |
| 28 | +class Book { |
| 29 | +
|
| 30 | + String title |
| 31 | + String author |
| 32 | +
|
| 33 | + static mapping = { |
| 34 | + datasource 'books' |
| 35 | + } |
| 36 | +} |
| 37 | +---- |
| 38 | + |
| 39 | +Define an interface for your data access methods and an abstract class that declares the connection: |
| 40 | + |
| 41 | +[source,groovy] |
| 42 | +---- |
| 43 | +import grails.gorm.services.Service |
| 44 | +
|
| 45 | +interface BookDataService { |
| 46 | +
|
| 47 | + Book get(Serializable id) |
| 48 | +
|
| 49 | + Book save(Book book) |
| 50 | +
|
| 51 | + void delete(Serializable id) |
| 52 | +
|
| 53 | + List<Book> findAllByAuthor(String author) |
| 54 | +
|
| 55 | + Long count() |
| 56 | +} |
| 57 | +---- |
| 58 | + |
| 59 | +[source,groovy] |
| 60 | +---- |
| 61 | +import grails.gorm.services.Service |
| 62 | +import grails.gorm.transactions.Transactional |
| 63 | +import groovy.transform.CompileStatic |
| 64 | +
|
| 65 | +@CompileStatic |
| 66 | +@Service(Book) |
| 67 | +@Transactional(connection = 'books') |
| 68 | +abstract class BookService implements BookDataService { |
| 69 | + // All interface methods are auto-implemented by GORM |
| 70 | + // and route to the 'books' datasource automatically. |
| 71 | +} |
| 72 | +---- |
| 73 | + |
| 74 | +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. |
| 75 | + |
| 76 | +IMPORTANT: The `connection` parameter is required when using Data Services with secondary datasources. The `@Service(Book)` annotation alone does not determine which datasource to use, even if the `Book` domain class declares `datasource 'books'` in its mapping block. |
| 77 | + |
| 78 | +==== How Connection Routing Works |
| 79 | + |
| 80 | +When GORM compiles a `@Service` abstract class, the `ServiceTransformation` AST transform: |
| 81 | + |
| 82 | +1. Copies the `@Transactional(connection = 'books')` annotation from the abstract class to the generated implementation class |
| 83 | +2. For each auto-implemented method, resolves the connection identifier via `findConnectionId()` |
| 84 | +3. Generates method bodies that use the appropriate connection - `GormEnhancer.findStaticApi(Book, 'books')` for CRUD operations and `DetachedCriteria.withConnection('books')` for finder queries |
| 85 | + |
| 86 | +This means auto-implemented methods like `get()`, `save()`, `delete()`, `findBy*()`, and `countBy*()` all respect the connection parameter without requiring manual implementations. |
| 87 | + |
| 88 | +==== Complex Queries with GormEnhancer |
| 89 | + |
| 90 | +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: |
| 91 | + |
| 92 | +[source,groovy] |
| 93 | +---- |
| 94 | +import groovy.transform.CompileStatic |
| 95 | +import grails.gorm.services.Service |
| 96 | +import grails.gorm.transactions.Transactional |
| 97 | +import org.grails.datastore.gorm.GormEnhancer |
| 98 | +import org.grails.datastore.gorm.GormStaticApi |
| 99 | +
|
| 100 | +@CompileStatic |
| 101 | +@Service(Book) |
| 102 | +@Transactional(connection = 'books') |
| 103 | +abstract class BookService implements BookDataService { |
| 104 | +
|
| 105 | + // Auto-implemented methods from interface are inherited |
| 106 | + // and route to 'books' datasource automatically. |
| 107 | +
|
| 108 | + private GormStaticApi<Book> getBooksApi() { |
| 109 | + GormEnhancer.findStaticApi(Book, 'books') |
| 110 | + } |
| 111 | +
|
| 112 | + List getTopAuthors(int limit) { |
| 113 | + booksApi.executeQuery(''' |
| 114 | + SELECT b.author, COUNT(b) as bookCount |
| 115 | + FROM Book b |
| 116 | + GROUP BY b.author |
| 117 | + ORDER BY bookCount DESC |
| 118 | + ''', Collections.emptyMap(), [max: limit]) |
| 119 | + } |
| 120 | +
|
| 121 | + List<Book> searchWithCriteria(String titlePattern, String author) { |
| 122 | + booksApi.createCriteria().list { |
| 123 | + like('title', "%${titlePattern}%") |
| 124 | + eq('author', author) |
| 125 | + order('title', 'asc') |
| 126 | + } as List<Book> |
| 127 | + } |
| 128 | +} |
| 129 | +---- |
| 130 | + |
| 131 | +The `GormStaticApi` returned by `findStaticApi()` provides these methods, all routed to the specified datasource: |
| 132 | + |
| 133 | +[cols="1,2"] |
| 134 | +|=== |
| 135 | +| Method | Description |
| 136 | + |
| 137 | +| `executeQuery(String hql, Map params)` |
| 138 | +| HQL/JPQL queries |
| 139 | + |
| 140 | +| `executeUpdate(String hql, Map params)` |
| 141 | +| Bulk UPDATE/DELETE statements |
| 142 | + |
| 143 | +| `withCriteria(Closure criteria)` |
| 144 | +| Criteria query |
| 145 | + |
| 146 | +| `createCriteria()` |
| 147 | +| Criteria builder for complex queries |
| 148 | + |
| 149 | +| `where(Closure query)` |
| 150 | +| Where query |
| 151 | + |
| 152 | +| `withTransaction(Closure action)` |
| 153 | +| Manual transaction management |
| 154 | + |
| 155 | +| `count()` |
| 156 | +| Total record count |
| 157 | + |
| 158 | +| `get(Serializable id)` |
| 159 | +| Find by primary key |
| 160 | + |
| 161 | +| `list(Map params)` |
| 162 | +| Paginated list |
| 163 | + |
| 164 | +| `save(Object instance)` |
| 165 | +| Persist an instance |
| 166 | +|=== |
| 167 | + |
| 168 | +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. |
| 169 | + |
| 170 | +==== Consuming Multi-Datasource Data Services |
| 171 | + |
| 172 | +Other services inject the Data Service interface type. Spring resolves the abstract class bean automatically: |
| 173 | + |
| 174 | +[source,groovy] |
| 175 | +---- |
| 176 | +import groovy.transform.CompileStatic |
| 177 | +
|
| 178 | +@CompileStatic |
| 179 | +class LibraryService { |
| 180 | +
|
| 181 | + BookDataService bookDataService // injected automatically |
| 182 | +
|
| 183 | + Map getLibraryStats() { |
| 184 | + Long totalBooks = bookDataService.count() |
| 185 | + List<Book> recentBooks = bookDataService.findAllByAuthor('Tolkien') |
| 186 | + [total: totalBooks, tolkienBooks: recentBooks.size()] |
| 187 | + } |
| 188 | +} |
| 189 | +---- |
| 190 | + |
| 191 | +The consuming service does not need `@Transactional(connection = 'books')`. The Data Service handles datasource routing internally. |
0 commit comments