Skip to content
46 changes: 46 additions & 0 deletions grails-doc/src/en/guide/upgrading/upgrading60x.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -860,3 +860,49 @@ The `+` marker is opt-in and fully backward compatible:
- Existing URL mappings without the `+` marker continue to work as before (splitting at the first dot)
- No changes are required to existing applications
- The feature can be adopted incrementally on a per-mapping basis

===== 12.28 GormService API Changes

The `grails.plugin.scaffolding.GormService` class has been updated to fix a thread-safety issue and improve API clarity.

====== Changes

1. The `resource` field type changed from `GormAllOperations<T>` to `Class<T>`
2. A new `gormStaticApi` field of type `GormAllOperations<T>` was added with thread-safe lazy initialization using `@Lazy`
3. The constructor no longer instantiates the resource class - it now stores the Class reference directly

====== Migration Impact

If your code extends `GormService` or accesses its fields:

**Accessing GORM operations**: Previously the `resource` field provided GORM operations. Now use the `gormStaticApi` field instead:

[source,groovy]
----
// Before (7.1.x and earlier)
class MyService extends GormService<MyDomain> {
void myMethod() {
def result = resource.list() // resource was GormAllOperations<T>
// ...
}
}

// After (7.1.x with fix)
class MyService extends GormService<MyDomain> {
void myMethod() {
def result = gormStaticApi.list() // use gormStaticApi instead
// ...
}
}
----

**Accessing the domain class**: If you need the Class reference, the `resource` field is now properly typed as `Class<T>`:

[source,groovy]
----
// Before
Class<MyDomain> clazz = resource.getClass() // awkward, resource was an instance

// After
Class<MyDomain> clazz = resource // resource is now the Class itself
----
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
/**
* Resolves the appropriate service bean for a given domain class by:
* - Scanning only beans of type GormService
* - Matching via: ((GormEntity<?>) service.getResource()).instanceOf(domainClass)
* - Matching via: domainClass.isAssignableFrom(service.getResource())
*
* Keeps a single shared cache for the whole app.
*/
Expand Down Expand Up @@ -72,28 +72,25 @@ private static <T extends GormEntity<T>> GormService<T> findService(Class<T> dom

for (String name : names) {
GormService<?> gs = (GormService<?>) ctx.getBean(name);
Object resource = gs.getResource();
if (resource instanceof GormEntity) {
GormEntity<?> ge = (GormEntity<?>) resource;
if (ge.instanceOf(domainClass)) {
matchingBeanNames.add(name);
if (match != null) {
throw new IllegalStateException(
"Multiple GormService beans match domain " + domainClass.getName() +
": " + matchingBeanNames
);
}
@SuppressWarnings("unchecked")
GormService<T> svc = (GormService<T>) gs;
match = svc;
Class<?> resourceClass = gs.getResource();
if (resourceClass != null && domainClass.isAssignableFrom(resourceClass)) {
matchingBeanNames.add(name);
if (match != null) {
throw new IllegalStateException(
"Multiple GormService beans match domain " + domainClass.getName() +
": " + matchingBeanNames
);
}
@SuppressWarnings("unchecked")
GormService<T> svc = (GormService<T>) gs;
match = svc;
}
}

if (match == null) {
throw new IllegalStateException(
"No GormService bean found for domain " + domainClass.getName() +
" using resource.instanceOf(..). Scanned " + names.length + " GormService beans."
". Scanned " + names.length + " GormService beans."
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import grails.gorm.api.GormAllOperations
import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import grails.util.GrailsNameUtils
import org.grails.datastore.gorm.GormEnhancer
import org.grails.datastore.gorm.GormEntity
import org.grails.datastore.gorm.GormEntityApi

Expand All @@ -34,28 +35,30 @@ import org.grails.datastore.gorm.GormEntityApi
@CompileStatic
class GormService<T extends GormEntity<T>> {

GormAllOperations<T> resource
@Lazy
GormAllOperations<T> gormStaticApi = GormEnhancer.findStaticApi(resource) as GormAllOperations<T>
Class<T> resource
String resourceName
String resourceClassName
boolean readOnly

GormService(Class<T> resource, boolean readOnly) {
this.resource = resource.getDeclaredConstructor().newInstance() as GormAllOperations<T>
this.resource = resource
this.readOnly = readOnly
resourceClassName = resource.simpleName
resourceName = GrailsNameUtils.getPropertyName(resource)
}

T get(Serializable id) {
resource.get(id)
gormStaticApi.get(id)
}

List<T> list(Map args) {
resource.list(args)
gormStaticApi.list(args)
}

Long count(Map args) {
resource.count()
gormStaticApi.count()
}

@Transactional
Expand Down
Loading