borinquenkid commented on code in PR #16066:
URL: https://github.com/apache/grails-core/pull/16066#discussion_r3730551105
##########
grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/Tenants.groovy:
##########
@@ -193,14 +211,15 @@ class Tenants {
throw new UnsupportedOperationException('Datastore implementation
does not support multi-tenancy')
}
}
+
/**
* Execute the given closure with given tenant id
* @param tenantId The tenant id
* @param callable The closure
* @return The result of the closure
*/
- static <T> T withId(Class<? extends Datastore> datastoreClass,
Serializable tenantId, Closure<T> callable) {
- Datastore datastore = GormEnhancer.findDatastoreByType(datastoreClass)
+ static <T> T withId(Class domainClass, Serializable tenantId, Closure<T>
callable) {
Review Comment:
Traced the actual blast radius before picking a fix. The ~40 call sites
cited as evidence (`RxGormStaticApi`, `TenantDelegatingRxGormOperations`)
turned out to be a false positive — `grails-datamapping-rx` has its own, fully
independent `grails.gorm.rx.multitenancy.Tenants` operating on
`RxDatastoreClient`, untouched by this PR (grepped every import in that module
to confirm zero references to the core `Tenants` class).
The real clash is in `TenantDelegatingGormOperations`'s ~100
`Tenants.withId((Class<Datastore>) datastore.getClass(), ...)` calls, but that
class turns out to be dead code on this branch:
`GormStaticApi.withTenant(Serializable)` used to construct it directly, and
this PR's rewrite replaced that with `forQualifier(tenantId.toString())`
instead — nothing else in the tree constructs it except its own unit test.
Fixed anyway since it's cheap and removes the landmine if it's ever wired back
up: added `requireMultiTenantCapableDatastore()` and switched all 100 sites to
the existing, unambiguous `Tenants.withId(MultiTenantCapableDatastore,
Serializable, Closure)` overload rather than a `withIdForDomain` rename — the
domain-class overload has no live erasure conflict anywhere reachable, so a
rename felt like a needless public API break for a theoretical case.
`Tenants.withId(Class domainClass, ...)` itself is untouched.
Commit `4b116c2166`. Also verified the `withTenant()`
decorator→`forQualifier` mechanism swap doesn't change runtime behavior —
`GormRegistrySpec`'s DISCRIMINATOR-mode tests and
`PartitionedMultiTenancySpec`'s DATABASE-mode assertion both exercise the
no-closure chained form against it and pass.
##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApi.groovy:
##########
@@ -19,1019 +19,719 @@
package org.grails.datastore.gorm
import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.transform.TypeCheckingMode
-import org.codehaus.groovy.runtime.InvokerHelper
+import groovy.util.logging.Slf4j
-import org.springframework.beans.PropertyAccessorFactory
-import org.springframework.beans.factory.config.AutowireCapableBeanFactory
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.support.DefaultTransactionDefinition
-import org.springframework.util.Assert
import grails.gorm.CriteriaBuilder
import grails.gorm.DetachedCriteria
-import grails.gorm.MultiTenant
-import grails.gorm.PagedResultList
import grails.gorm.api.GormAllOperations
+import grails.gorm.api.GormInstanceOperations
+import grails.gorm.api.GormStaticOperations
import grails.gorm.multitenancy.Tenants
import grails.gorm.transactions.GrailsTransactionTemplate
-import org.grails.datastore.gorm.finders.DynamicFinder
import org.grails.datastore.gorm.finders.FinderMethod
-import org.grails.datastore.gorm.multitenancy.TenantDelegatingGormOperations
-
+import org.grails.datastore.gorm.transactions.DefaultTransactionTemplateFactory
+import org.grails.datastore.gorm.transactions.TransactionTemplateFactory
import org.grails.datastore.mapping.core.Datastore
import org.grails.datastore.mapping.core.DatastoreUtils
import org.grails.datastore.mapping.core.Session
import org.grails.datastore.mapping.core.SessionCallback
import org.grails.datastore.mapping.core.StatelessDatastore
import org.grails.datastore.mapping.core.connections.ConnectionSource
-import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
import org.grails.datastore.mapping.core.connections.ConnectionSources
import org.grails.datastore.mapping.core.connections.ConnectionSourcesProvider
+import org.grails.datastore.mapping.model.MappingContext
import org.grails.datastore.mapping.model.PersistentEntity
-import org.grails.datastore.mapping.model.PersistentProperty
-import org.grails.datastore.mapping.model.types.Association
-import
org.grails.datastore.mapping.multitenancy.MultiTenancySettings.MultiTenancyMode
-import org.grails.datastore.mapping.query.Query
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
import org.grails.datastore.mapping.query.api.BuildableCriteria
-import org.grails.datastore.mapping.query.api.Criteria
+import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
/**
- * Static methods of the GORM API.
+ * Static methods for GORM
*
* @author Graeme Rocher
- * @param <D> the entity/domain class
*/
-@CompileStatic
+@CompileDynamic
+@Slf4j
class GormStaticApi<D> extends AbstractGormApi<D> implements
GormAllOperations<D> {
- protected final List<FinderMethod> gormDynamicFinders
+ private static final TransactionTemplateFactory
DEFAULT_TRANSACTION_TEMPLATE_FACTORY = new DefaultTransactionTemplateFactory()
- protected final PlatformTransactionManager transactionManager
- protected final String defaultQualifier
- protected final MultiTenancyMode multiTenancyMode
- protected final ConnectionSources connectionSources
+ protected final List<FinderMethod> finders
+ @Deprecated
GormStaticApi(Class<D> persistentClass, Datastore datastore,
List<FinderMethod> finders) {
- this(persistentClass, datastore, finders, null)
+ this(persistentClass, datastore?.mappingContext, finders, datastore !=
null ? ({ datastore } as DatastoreResolver) : null, ConnectionSource.DEFAULT,
null)
}
+ @Deprecated
GormStaticApi(Class<D> persistentClass, Datastore datastore,
List<FinderMethod> finders, PlatformTransactionManager transactionManager) {
- super(persistentClass, datastore)
- gormDynamicFinders = finders
- this.transactionManager = transactionManager
- String qualifier = ConnectionSource.DEFAULT
- if (datastore instanceof ConnectionSourcesProvider) {
- this.connectionSources = ((ConnectionSourcesProvider)
datastore).connectionSources
- ConnectionSource<?, ? extends ConnectionSourceSettings>
defaultConnectionSource = connectionSources.defaultConnectionSource
- qualifier = defaultConnectionSource.name
- multiTenancyMode =
defaultConnectionSource.settings.multiTenancy.mode
-
- }
- else {
- connectionSources = null
- multiTenancyMode = MultiTenancyMode.NONE
- }
- this.defaultQualifier = qualifier
+ this(persistentClass, datastore?.mappingContext, finders, datastore !=
null ? ({ datastore } as DatastoreResolver) : null, ConnectionSource.DEFAULT,
null)
}
- /**
- * @return The PersistentEntity for this class
- */
- PersistentEntity getGormPersistentEntity() {
- persistentEntity
+ GormStaticApi(Class<D> persistentClass, MappingContext mappingContext,
List<FinderMethod> finders) {
+ this(persistentClass, mappingContext, finders, null,
ConnectionSource.DEFAULT, null)
}
- List<FinderMethod> getGormDynamicFinders() {
- gormDynamicFinders
+ GormStaticApi(Class<D> persistentClass, MappingContext mappingContext,
List<FinderMethod> finders, String qualifier) {
+ this(persistentClass, mappingContext, finders, null, qualifier, null)
}
- /**
- * Property missing handler
- *
- * @param name The name of the property
- */
- def propertyMissing(String name) {
- if (datastore instanceof ConnectionSourcesProvider) {
- return GormEnhancer.findStaticApi(persistentClass, name)
- }
- else {
- throw new MissingPropertyException(name, persistentClass)
- }
+ GormStaticApi(Class<D> persistentClass, MappingContext mappingContext,
List<FinderMethod> finders, DatastoreResolver resolver, String qualifier) {
+ this(persistentClass, mappingContext, finders, resolver, qualifier,
null)
}
- /**
- * Property missing handler
- *
- * @param name The name of the property
- */
- void propertyMissing(String name, value) {
- throw new MissingPropertyException(name, persistentClass)
+ GormStaticApi(Class<D> persistentClass, MappingContext mappingContext,
List<FinderMethod> finders, DatastoreResolver resolver, String qualifier,
GormRegistry registry) {
+ super(persistentClass, mappingContext, resolver, qualifier, registry)
+ this.finders = finders
}
- /**
- * Method missing handler that deals with the invocation of dynamic finders
- *
- * @param methodName The method name
- * @param args The arguments
- * @return The result of the method call
- */
- @CompileDynamic
- def methodMissing(String methodName, Object args) {
- FinderMethod method = gormDynamicFinders.find { FinderMethod f ->
f.isMethodMatch(methodName) }
- if (!method) {
- throw new MissingMethodException(methodName, persistentClass, args)
- }
-
- // if the class is multi tenant, don't cache the method because the
tenant will need to be resolved
- // for each method call
- if (!MultiTenant.isAssignableFrom(persistentClass)) {
-
- def mc = persistentClass.getMetaClass()
-
- // register the method invocation for next time
- mc.static."$methodName" = { Object[] varArgs ->
- // FYI... This is relevant to
http://jira.grails.org/browse/GRAILS-3463 and may
- // become problematic if
http://jira.codehaus.org/browse/GROOVY-5876 is addressed...
- final argumentsForMethod
- if (varArgs == null) {
- argumentsForMethod = [null] as Object[]
- }
- // if the argument component type is not an Object then we
have an array passed that is the actual argument
- else if (varArgs.getClass().componentType != Object) {
- // so we wrap it in an object array
- argumentsForMethod = [varArgs] as Object[]
- }
- else {
-
- if (varArgs.length == 1 &&
varArgs[0].getClass().isArray()) {
- argumentsForMethod = varArgs[0]
- } else {
-
- argumentsForMethod = varArgs
- }
- }
- method.invoke(delegate, methodName, argumentsForMethod)
- }
+ @Override
+ PlatformTransactionManager getTransactionManager() {
+ Datastore ds = getDatastore()
+ if (ds instanceof TransactionCapableDatastore) {
+ return ((TransactionCapableDatastore)ds).getTransactionManager()
}
-
- return method.invoke(persistentClass, methodName, args)
- }
-
- /**
- *
- * @param callable Callable closure containing detached criteria definition
- * @return The DetachedCriteria instance
- */
- DetachedCriteria<D> where(Closure callable) {
- new DetachedCriteria<D>(persistentClass).build(callable)
- }
-
- /**
- *
- * @param callable Callable closure containing detached criteria definition
- * @return The DetachedCriteria instance that is lazily initialized
- */
- DetachedCriteria<D> whereLazy(Closure callable) {
- new DetachedCriteria<D>(persistentClass).buildLazy(callable)
- }
- /**
- *
- * @param callable Callable closure containing detached criteria definition
- * @return The DetachedCriteria instance
- */
- DetachedCriteria<D> whereAny(Closure callable) {
- (DetachedCriteria<D>) new
DetachedCriteria<D>(persistentClass).or(callable)
- }
-
- /**
- * Uses detached criteria to build a query and then execute it returning a
list
- *
- * @param callable The callable
- * @return A List of entities
- */
- List<D> findAll(Closure callable) {
- def criteria = new DetachedCriteria<D>(persistentClass).build(callable)
- return criteria.list()
- }
-
- /**
- * Uses detached criteria to build a query and then execute it returning a
list
- *
- * @param args pagination parameters
- * @param callable The callable
- * @return A List of entities
- */
- List<D> findAll(Map args, Closure callable) {
- def criteria = new DetachedCriteria<D>(persistentClass).build(callable)
- return criteria.list(args)
- }
-
- /**
- * Uses detached criteria to build a query and then execute it returning a
list
- *
- * @param callable The callable
- * @return A single entity
- */
- D find(Closure callable) {
- def criteria = new DetachedCriteria<D>(persistentClass).build(callable)
- return criteria.find()
+ return null
}
- /**
- * Saves a list of objects in one go
- * @param objectsToSave The objects to save
- * @return A list of object identifiers
- */
- List<Serializable> saveAll(Object... objectsToSave) {
- (List<Serializable>) execute({ Session session ->
- session.persist(Arrays.asList(objectsToSave))
- } as SessionCallback)
+ @Override
+ protected <T1> T1 executeQualified(String qualifier, SessionCallback<T1>
callback) {
+ GormStaticApi<D> qualifiedApi =
registry.findStaticApi(persistentClass, qualifier)
+ if (qualifiedApi != null && qualifiedApi != this) {
+ return (T1) qualifiedApi.execute(callback)
+ }
+ return DatastoreUtils.execute(getDatastore(), callback)
}
- /**
- * Saves a list of objects in one go
- * @param objectToSave Collection of objects to save
- * @return A list of object identifiers
- */
- List<Serializable> saveAll(Iterable<?> objectsToSave) {
- (List<Serializable>) execute({ Session session ->
- session.persist(objectsToSave)
- } as SessionCallback)
+ @Override
+ PersistentEntity getGormPersistentEntity() {
+ PersistentEntity entity = qualifier != null ?
registry.apiResolver.findEntity(persistentClass, qualifier) : null
+ if (entity == null) {
+ entity = super.getGormPersistentEntity()
+ }
+ if (entity == null) {
+ entity = registry.apiResolver.findEntity(persistentClass)
+ }
+ // Final fallback: resolve from the mapping context captured when this
API was constructed.
+ // Entity metadata is identical across tenants/connections (only the
datastore/session differs),
+ // so this stable reference is the most reliable source and avoids a
null entity when runtime
+ // registry resolution is disturbed by cross-spec state — e.g. a
qualified API created by
+ // withTenant(tenantId) for DISCRIMINATOR/SCHEMA multi-tenancy.
+ if (entity == null && mappingContext != null) {
+ entity = mappingContext.getPersistentEntity(persistentClass.name)
+ }
+ entity
}
- /**
- * Deletes a list of objects in one go
- * @param objectsToDelete The objects to delete
- */
- void deleteAll(Object... objectsToDelete) {
- execute({ Session session ->
- session.delete(Arrays.asList(objectsToDelete))
- } as SessionCallback)
+ @Override
+ List<FinderMethod> getGormDynamicFinders() {
+ return finders
}
- /**
- * Deletes a list of objects in one go and flushes when param is set
- * @param objectsToDelete The objects to delete
- */
- void deleteAll(Map params, Object... objectsToDelete) {
- execute({ Session session ->
- session.delete(Arrays.asList(objectsToDelete))
- if (params?.flush) {
- session.flush()
- }
- } as SessionCallback)
+ GormStaticApi<D> forQualifier(String qualifier) {
+ Datastore ds = getDatastore()
+ DatastoreResolver resolver = new DatastoreResolver() {
+ @Override Datastore resolve() {
registry.apiResolver.findDatastore(persistentClass, qualifier) }
+ }
+ List<FinderMethod> qualifiedFinders =
registry.createDynamicFinders(resolver, ds.mappingContext)
+ createStaticApi(persistentClass, ds.mappingContext, qualifiedFinders,
resolver, qualifier)
}
- /**
- * Deletes a list of objects in one go
- * @param objectsToDelete Collection of objects to delete
- */
- void deleteAll(Iterable objectToDelete) {
- execute({ Session session ->
- session.delete(objectToDelete)
- } as SessionCallback)
+ protected GormStaticApi<D> createStaticApi(Class<D> persistentClass,
MappingContext mappingContext, List<FinderMethod> finders, DatastoreResolver
resolver, String qualifier) {
+ new GormStaticApi<D>(persistentClass, mappingContext, finders,
resolver, qualifier, registry)
}
- /**
- * Deletes a list of objects in one go and flushes when param is set
- * @param objectsToDelete Collection of objects to delete
- */
- void deleteAll(Map params, Iterable objectToDelete) {
- execute({ Session session ->
- session.delete(objectToDelete)
- if (params?.flush) {
- session.flush()
+ @Override
+ Object methodMissing(String name, Object args) {
+ Object[] argsArray = (args instanceof Object[]) ? (Object[]) args :
([args] as Object[])
+ for (FinderMethod fm : finders) {
+ if (fm.isMethodMatch(name)) {
+ return execute({ Session session ->
+ fm.invoke(persistentClass, name, argsArray)
+ } as SessionCallback)
}
- } as SessionCallback)
- }
-
- /**
- * Creates an instance of this class
- * @return The created instance
- */
- @CompileStatic(TypeCheckingMode.SKIP)
- D create() {
- D d = persistentClass.newInstance()
-
- def applicationContext = datastore.applicationContext
-
- if (applicationContext != null) {
-
applicationContext.autowireCapableBeanFactory.autowireBeanProperties(
- d, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false)
}
-
- return d
- }
-
- /**
- * Retrieves an object from the datastore. eg. Book.get(1)
- */
- D get(Serializable id) {
- (D) execute({ Session session ->
- session.retrieve((Class)persistentClass, id)
- } as SessionCallback)
- }
-
- /**
- * Retrieves an object from the datastore. eg. Book.read(1)
- *
- * Since the datastore abstraction doesn't support dirty checking yet this
- * just delegates to {@link #get(Serializable)}
- */
- D read(Serializable id) {
- (D) execute({ Session session ->
- session.retrieve((Class)persistentClass, id)
- } as SessionCallback)
- }
-
- /**
- * Retrieves an object from the datastore as a proxy. eg. Book.load(1)
- */
- D load(Serializable id) {
- (D) execute({ Session session ->
- session.proxy((Class)persistentClass, id)
- } as SessionCallback)
- }
-
- /**
- * Retrieves an object from the datastore as a proxy. eg. Book.proxy(1)
- */
- D proxy(Serializable id) {
- load(id)
- }
-
- /**
- * Retrieve all the objects for the given identifiers
- * @param ids The identifiers to operate against
- * @return A list of identifiers
- */
- List<D> getAll(Iterable<Serializable> ids) {
- return getAll(ids as Serializable[])
- }
-
- /**
- * Retrieve all the objects for the given identifiers
- * @param ids The identifiers to operate against
- * @return A list of identifiers
- */
- List<D> getAll(Serializable... ids) {
- (List<D>) execute({ Session session ->
- session.retrieveAll(persistentClass, ids.flatten())
- } as SessionCallback)
- }
-
- /**
- * @return Synonym for {@link #list()}
- */
- List<D> getAll() {
- list()
- }
-
- /**
- * Creates a criteria builder instance
- */
- BuildableCriteria createCriteria() {
- new CriteriaBuilder(persistentClass, datastore.currentSession)
+ throw new MissingMethodException(name, persistentClass, argsArray)
}
- /**
- * Creates a criteria builder instance
- */
- def withCriteria(@DelegatesTo(Criteria) Closure callable) {
- execute({ Session session ->
- InvokerHelper.invokeMethod(createCriteria(), 'call', callable)
- } as SessionCallback)
- }
-
- /**
- * Creates a criteria builder instance
- */
- def withCriteria(Map builderArgs, @DelegatesTo(Criteria) Closure callable)
{
- def criteriaBuilder = createCriteria()
- def builderBean =
PropertyAccessorFactory.forBeanPropertyAccess(criteriaBuilder)
- for (entry in builderArgs.entrySet()) {
- String propertyName = entry.key.toString()
- if (builderBean.isWritableProperty(propertyName)) {
- builderBean.setPropertyValue(propertyName, entry.value)
+ @Override
+ Object propertyMissing(String name) {
+ for (FinderMethod fm : finders) {
+ if (fm.isMethodMatch(name)) {
+ return { Object... args ->
+ Object[] finderArgs = args == null ? ([null] as Object[])
: args
+ execute({ Session session ->
+ fm.invoke(persistentClass, name, finderArgs)
+ } as SessionCallback)
+ }
}
}
-
- if (builderArgs?.uniqueResult) {
- execute({ Session session ->
- InvokerHelper.invokeMethod(criteriaBuilder, 'get', callable)
- } as SessionCallback)
-
+
+ Datastore ds = getDatastore()
+ if (ds instanceof ConnectionSourcesProvider) {
+ ConnectionSources sources = ((ConnectionSourcesProvider)
ds).connectionSources
+ if (sources != null) {
+ if (sources.getConnectionSource(name) != null) {
+ return registry.findStaticApi(persistentClass, name)
+ }
+ if (name.equalsIgnoreCase(ConnectionSource.DEFAULT) ||
name.equalsIgnoreCase(ConnectionSource.OLD_DEFAULT)) {
+ return registry.findStaticApi(persistentClass,
ConnectionSource.DEFAULT)
+ }
+ }
}
- else {
- execute({ Session session ->
- InvokerHelper.invokeMethod(criteriaBuilder, 'list', callable)
- } as SessionCallback)
+ // Fallback: the preferred/transactional datastore may be a
single-datasource datastore
+ // that doesn't expose the named qualifier in its connectionSources.
Check the registry
+ // directly so that entities mapped to multiple datasources (e.g.
datasource 'ALL') can
+ // still be accessed via the qualifier even when a single-datasource
transaction is active.
+ if (registry.getDatastoreByString(persistentClass.name, name) != null)
{
+ return registry.findStaticApi(persistentClass, name)
}
-
+ throw new MissingPropertyException(name, persistentClass)
}
- /**
- * Locks an instance for an update
- * @param id The identifier
- * @return The instance
- */
- D lock(Serializable id) {
- (D) execute({ Session session ->
- session.lock((Class)persistentClass, id)
- } as SessionCallback)
+ @Override
+ void propertyMissing(String name, Object val) {
+ throw new MissingPropertyException(name, persistentClass)
}
- /**
- * Merges an instance with the current session
- * @param d The object to merge
- * @return The instance
- */
+ // GormInstanceOperations delegation
@Override
def propertyMissing(D instance, String name) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).propertyMissing(instance, name)
+ registry.findInstanceApi(persistentClass,
null).propertyMissing(instance, name)
}
@Override
boolean instanceOf(D instance, Class cls) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).instanceOf(instance, cls)
+ registry.findInstanceApi(persistentClass, null).instanceOf(instance,
cls)
}
@Override
D lock(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).lock(instance)
+ registry.findInstanceApi(persistentClass, null).lock(instance)
}
@Override
- def <T> T mutex(D instance, Closure<T> callable) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).mutex(instance, callable)
+ def <T1> T1 mutex(D instance, Closure<T1> callable) {
+ registry.findInstanceApi(persistentClass, null).mutex(instance,
callable)
}
@Override
D refresh(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).refresh(instance)
+ registry.findInstanceApi(persistentClass, null).refresh(instance)
}
@Override
D save(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).save(instance)
+ registry.findInstanceApi(persistentClass, null).save(instance)
}
@Override
D insert(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).insert(instance)
+ registry.findInstanceApi(persistentClass, null).insert(instance)
}
@Override
D insert(D instance, Map params) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).insert(instance, params)
+ registry.findInstanceApi(persistentClass, null).insert(instance,
params)
}
- D merge(D d) {
- execute({ Session session ->
- session.persist(d)
- return d
- } as SessionCallback)
+ @Override
+ D merge(D instance) {
+ registry.findInstanceApi(persistentClass, null).merge(instance)
}
@Override
D merge(D instance, Map params) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).merge(instance, params)
+ registry.findInstanceApi(persistentClass, null).merge(instance, params)
}
@Override
D save(D instance, boolean validate) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).save(instance, validate)
+ registry.findInstanceApi(persistentClass, null).save(instance,
validate)
}
@Override
D save(D instance, Map params) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).save(instance, params)
+ registry.findInstanceApi(persistentClass, null).save(instance, params)
}
@Override
Serializable ident(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).ident(instance)
+ registry.findInstanceApi(persistentClass, null).ident(instance)
}
@Override
D attach(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).attach(instance)
+ registry.findInstanceApi(persistentClass, null).attach(instance)
}
@Override
boolean isAttached(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).isAttached(instance)
+ registry.findInstanceApi(persistentClass, null).isAttached(instance)
}
@Override
void discard(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).discard(instance)
+ registry.findInstanceApi(persistentClass, null).discard(instance)
}
@Override
void delete(D instance) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).delete(instance)
+ registry.findInstanceApi(persistentClass, null).delete(instance)
}
@Override
void delete(D instance, Map params) {
- GormEnhancer.findInstanceApi(persistentClass,
defaultQualifier).delete(instance, params)
+ registry.findInstanceApi(persistentClass, null).delete(instance,
params)
}
- /**
- * Counts the number of persisted entities
- * @return The number of persisted entities
- */
- Integer count() {
- (Integer) execute({ Session session ->
- def q = session.createQuery(persistentClass)
- q.projections().count()
- def result = q.singleResult()
- if (!(result instanceof Number)) {
- result = result.toString()
- }
- try {
- return result as Integer
- }
- catch (NumberFormatException e) {
- return 0
- }
- } as SessionCallback)
+ // GormStaticOperations
+ @Override
+ D get(Serializable id) {
+ execute({ Session session ->
+ session.retrieve(persistentClass, id)
+ } as SessionCallback<D>)
}
- /**
- * Same as {@link #count()} but allows property-style syntax (Foo.count)
- */
- Integer getCount() {
- count()
+ @Override
+ D read(Serializable id) {
+ get(id)
}
- /**
- * Checks whether an entity exists
- */
- boolean exists(Serializable id) {
- get(id) != null
+ @Override
+ D load(Serializable id) {
+ execute({ Session session ->
+ session.proxy(persistentClass, id)
+ } as SessionCallback<D>)
}
- /**
- * Lists objects in the datastore. eg. Book.list(max:10)
- *
- * @param params Any parameters such as offset, max etc.
- * @return A list of results
- */
- List<D> list(Map params) {
- (List<D>) execute({ Session session ->
- Query q = session.createQuery(persistentClass)
- DynamicFinder.populateArgumentsForCriteria(persistentClass, q,
params)
- if (params?.max) {
- return new PagedResultList(q)
- }
- return q.list()
- } as SessionCallback)
+ @Override
+ D proxy(Serializable id) {
+ load(id)
+ }
+
+ @Override
+ List<D> getAll(Serializable... ids) {
+ execute({ Session session ->
+ session.retrieveAll(persistentClass, ids)
+ } as SessionCallback<List<D>>)
+ }
+
+ @Override
+ List<D> getAll(Iterable<Serializable> ids) {
+ execute({ Session session ->
+ session.retrieveAll(persistentClass, ids)
+ } as SessionCallback<List<D>>)
+ }
+
+ @Override
+ List<D> getAll() {
+ list()
}
- /**
- * List all entities
- *
- * @return The list of all entities
- */
+ @Override
List<D> list() {
- (List<D>) execute({ Session session ->
- session.createQuery(persistentClass).list()
- } as SessionCallback)
+ list(Collections.emptyMap())
}
- /**
- * The same as {@link #list()}
- *
- * @return The list of all entities
- */
- List<D> findAll(Map params = Collections.emptyMap()) {
- list(params)
+ @Override
+ List<D> list(Map params) {
+ execute({ Session session ->
+ org.grails.datastore.mapping.query.Query q =
session.createQuery(persistentClass)
+
org.grails.datastore.gorm.finders.DynamicFinder.populateArgumentsForCriteria(persistentClass,
q, params)
+ if (params?.containsKey('max')) {
+ return new grails.gorm.PagedResultList(q)
+ }
+ q.list()
+ } as SessionCallback<List<D>>)
}
- /**
- * Finds an object by example
- *
- * @param example The example
- * @return A list of matching results
- */
- List<D> findAll(D example) {
- findAll(example, Collections.emptyMap())
+ @Override
+ Integer count() {
+ log.debug('GormStaticApi.count() called for {}', persistentClass.name)
+ Integer result = execute({ Session session ->
+ def query = session.createQuery(persistentClass)
+ query.projections().count()
+ def res = query.singleResult()
+ log.debug('Query singleResult returned {}', res)
+ res instanceof Number ? ((Number)res).intValue() : 0
+ } as SessionCallback<Integer>)
+ log.debug('count() result is {}', result)
+ return result
}
- /**
- * Finds an object by example using the given arguments for pagination
- *
- * @param example The example
- * @param args The arguments
- *
- * @return A list of matching results
- */
- List<D> findAll(D example, Map args) {
- if (!persistentEntity.isInstance(example)) {
- return Collections.emptyList()
- }
+ @Override
+ Integer getCount() {
+ count()
+ }
- def queryMap = createQueryMapForExample(persistentEntity, example)
- return findAllWhere(queryMap, args)
+ @Override
+ boolean exists(Serializable id) {
+ get(id) != null
}
- /**
- * Finds the first object using the natural sort order
- *
- * @return the first object in the datastore, null if none exist
- */
+ @Override
D first() {
first([:])
}
- /**
- * Finds the first object sorted by propertyName
- *
- * @param propertyName the name of the property to sort by
- *
- * @return the first object in the datastore sorted by propertyName, null
if none exist
- */
+ @Override
D first(String propertyName) {
first(sort: propertyName)
}
- /**
- * Finds the first object. If queryParams includes 'sort', that will
- * dictate the sort order, otherwise natural sort order will be used.
- * queryParams may include any of the same parameters that might be passed
- * to the list(Map) method. This method will ignore 'order' and 'max' as
- * those are always 'asc' and 1, respectively.
- *
- * @return the first object in the datastore, null if none exist
- */
- D first(Map queryParams) {
+ @Override
+ D first(Map params) {
+ Map queryParams = new LinkedHashMap(params ?: [:])
queryParams.max = 1
queryParams.order = 'asc'
if (!queryParams.containsKey('sort')) {
- def idPropertyName = persistentEntity.identity?.name
+ String idPropertyName = getGormPersistentEntity()?.identity?.name
if (idPropertyName) {
queryParams.sort = idPropertyName
}
}
- def resultList = list(queryParams)
+ List<D> resultList = list(queryParams)
resultList ? resultList[0] : null
}
- /**
- * Finds the last object using the natural sort order
- *
- * @return the last object in the datastore, null if none exist
- */
+ @Override
D last() {
last([:])
}
- /**
- * Finds the last object sorted by propertyName
- *
- * @param propertyName the name of the property to sort by
- *
- * @return the last object in the datastore sorted by propertyName, null
if none exist
- */
+ @Override
D last(String propertyName) {
- last(sort: propertyName)
+ last(sort: propertyName, order: 'desc')
}
-/**
- * Finds the last object. If queryParams includes 'sort', that will
- * dictate the sort order, otherwise natural sort order will be used.
- * queryParams may include any of the same parameters that might be passed
- * to the list(Map) method. This method will ignore 'order' and 'max' as
- * those are always 'asc' and 1, respectively.
- *
- * @return the last object in the datastore, null if none exist
- */
- D last(Map queryParams) {
+ @Override
+ D last(Map params) {
+ Map queryParams = new LinkedHashMap(params ?: [:])
queryParams.max = 1
queryParams.order = 'desc'
if (!queryParams.containsKey('sort')) {
- def idPropertyName = persistentEntity.identity?.name
+ String idPropertyName = getGormPersistentEntity()?.identity?.name
if (idPropertyName) {
queryParams.sort = idPropertyName
}
}
- def resultList = list(queryParams)
+ List<D> resultList = list(queryParams)
resultList ? resultList[0] : null
}
- /**
- * Finds all results matching all of the given conditions. Eg.
Book.findAllWhere(author:"Stephen King", title:"The Stand")
- *
- * @param queryMap The map of conditions
- * @return A list of results
- */
- List<D> findAllWhere(Map queryMap) {
- findAllWhere(queryMap, Collections.emptyMap())
+ @Override
+ BuildableCriteria createCriteria() {
+ execute({ Session session ->
+ new CriteriaBuilder(persistentClass, session)
+ } as SessionCallback<BuildableCriteria>)
}
- /**
- * Finds all results matching all of the given conditions. Eg.
Book.findAllWhere(author:"Stephen King", title:"The Stand")
- *
- * @param queryMap The map of conditions
- * @param args The Query arguments
- *
- * @return A list of results
- */
- List<D> findAllWhere(Map queryMap, Map args) {
- (List<D>) execute({ Session session ->
- Query q = session.createQuery(persistentClass)
+ @Override
+ def <T1> T1 withCriteria(Closure<T1> callable) {
+ createCriteria().list(callable)
+ }
- Map<String, Object> processedQueryMap = [:]
- queryMap.each { key, value -> processedQueryMap[key.toString()] =
value }
- q.allEq(processedQueryMap)
+ @Override
+ def <T1> T1 withCriteria(Map builderArgs, Closure callable) {
+ createCriteria().list(builderArgs, callable)
+ }
- DynamicFinder.populateArgumentsForCriteria(persistentClass, q,
args)
- q.list()
- } as SessionCallback<List>)
+ @Override
+ D lock(Serializable id) {
+ execute({ Session session ->
+ session.lock(persistentClass, id)
+ } as SessionCallback<D>)
+ }
+
+ @Override
+ grails.gorm.DetachedCriteria<D> where(Closure callable) {
+ new
grails.gorm.DetachedCriteria<D>(persistentClass).withConnection(qualifier).where(callable)
+ }
+
+ @Override
+ grails.gorm.DetachedCriteria<D> whereLazy(Closure callable) {
+ where(callable)
+ }
+
+ @Override
+ grails.gorm.DetachedCriteria<D> whereAny(Closure callable) {
+ new grails.gorm.DetachedCriteria<D>(persistentClass).or(callable)
+ }
+
+ @Override
+ List<Serializable> saveAll(Iterable<?> objectsToSave) {
+ execute({ Session session ->
+ session.persist(objectsToSave)
+ } as SessionCallback<List<Serializable>>)
+ }
+
+ @Override
+ List<Serializable> saveAll(Object... objectsToSave) {
+ saveAll(Arrays.asList(objectsToSave))
+ }
+
+ @Override
+ Number deleteAll() {
+ execute({ Session session ->
+ session.deleteAll(new DetachedCriteria(persistentClass))
+ } as SessionCallback<Number>)
+ }
+
+ @Override
+ Number deleteAll(Map params) {
Review Comment:
Took the implement option, not drop — this is your own stated bar, and your
`fix/gorm-registry-review-feedback` branch already met it, so matched it rather
than reinventing: `deleteAll(Map params)` now honors `params?.flush` instead of
discarding params, both `deleteAll()`/`deleteAll(Map)` are documented in
`grails-doc` (new 'Deleting Every Instance' section in `basicCRUD.adoc` with an
explicit destructive-semantics warning and a pointer to `.where{}.deleteAll()`
for partial deletes), and `TenantDelegatingGormOperations` gets matching
delegators via its own `requireMultiTenantCapableDatastore()` pattern (see the
`Tenants.withId` thread) rather than a class-based fallback.
Commit `e011e0896c`. 3 new specs in `GormStaticApiSpec` (basic
delete-everything, `Map`-returns-count, flush-honored via a `Session` mock) + 2
in `TenantDelegatingGormOperationsSpec`.
##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy:
##########
@@ -540,162 +253,139 @@ class GormEnhancer implements Closeable {
}
}
- @CompileStatic
- List<FinderMethod> getFinders() {
- if (finders == null) {
- finders = Collections.unmodifiableList(createDynamicFinders())
+ @CompileDynamic
+ protected void addStaticMethods(PersistentEntity e) {
+ def cls = e.javaClass
+ ExpandoMetaClass mc = MetaClassUtils.getExpandoMetaClass(cls)
+
+ mc.static.methodMissing = { String name, args ->
Review Comment:
Restored the `dynamicEnhance`-gated bootstrap exactly as it is on 8.0.x:
`registerEntity` no longer calls `addStaticMethods`/`addInstanceMethods`
unconditionally, `enhance()`/`enhance(entity)` are back and gated on
`dynamicEnhance` (hard-coded `false` in the settings constructor, matching
8.0.x's own quirk), and nothing in the tree calls `enhance(..)` — confirmed by
grepping the whole real 8.0.x tree, not just this PR's diff, for any caller
that ever flips `dynamicEnhance` true. `GormEntity`'s own trait
`methodMissing`/`propertyMissing` hooks already cover all dispatch (83 spec
classes stayed green with zero `ExpandoMetaClass` installed). Commit
`da09401c4d`.
Separately adopted your instance-side clobber guard on `addInstanceMethods`
(backs off with a debug log if a `methodMissing` handler is already installed)
— commit `2dd18808ff`. I did not adopt the static-side guard
(`getStaticMetaMethod('methodMissing', ...)`): wrote a few isolated
`ExpandoMetaClass` probe scripts (no GORM involved) and confirmed
`getStaticMetaMethod` can never see a static `methodMissing` handler installed
via the standard `mc.static.methodMissing = {...}` idiom, regardless of install
order — even though the handler is functionally live (`invokeStaticMethod`
proves it dispatches). Your branch has the identical static-side check; it
looks like it never actually fires there either. Happy to share the repro
scripts if useful — didn't want to ship a check that looks protective but is
dead code.
##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/core/AbstractDatastore.java:
##########
@@ -170,8 +215,13 @@ public Session getCurrentSession() throws
ConnectionNotFoundException {
return DatastoreUtils.doGetSession(this, false);
}
+ /**
+ * @return Whether {@link #getCurrentSession()} would return an existing
session rather than
+ * opening a new one: a validated (still connected) session is bound to
the current thread.
+ * Delegates to {@link #getSessionResolver()} so both methods read the
same state.
+ */
public boolean hasCurrentSession() {
- return TransactionSynchronizationManager.hasResource(this);
+ return getSessionResolver().resolve() != null;
Review Comment:
Agreed the mutation shouldn't live in a predicate. Added
`SessionResolver.hasResolvedSession()` — implemented by iterating
`SessionHolder.getSessions()` (the genuinely read-only
`Collections.unmodifiableCollection` view) checking `isConnected()`, same
accuracy as `resolve()` but zero mutation.
`AbstractDatastore.hasCurrentSession()` now delegates to that instead of
`resolve() != null`. Commit `3707a82f63`.
Audited all 8 call sites first: the 2 `ActiveSessionDatastoreSelector`
routing-scan sites needed the pure check; the other 6 all immediately call
`getCurrentSession()` right after anyway, which independently re-validates via
its own `getValidatedSession()`, so none of them were actually relying on the
old eviction side effect. Added a regression spec proving a
`hasCurrentSession()` probe on datastore A leaves datastore B's session binding
completely untouched (present, not evicted).
##########
grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/FirstAndLastMethodSpec.groovy:
##########
@@ -163,7 +163,7 @@ class FirstAndLastMethodSpec extends GrailsDataTckSpec {
}
@PendingFeatureIf(
- value = { System.getProperty('hibernate5.gorm.suite') },
+ value = { System.getProperty('hibernate5.gorm.suite') ||
System.getProperty('hibernate7.gorm.suite') },
Review Comment:
Fixed the regression instead of widening the exclusion. Root cause:
`GormStaticApi.first(Map)`/`last(Map)` (shared, not H7-specific — H5 lost the
same override at merge-base too, both now route through the generic method)
force-applies `max:1`/`order` at the DB level even when a composite-key entity
has no derivable identity to sort by, so it returned an arbitrary row instead
of falling back to natural/insertion order. Fixed generically: only force
max/order when a sort key exists (user-supplied or derived from a simple
identity); otherwise fetch normally and index `[0]`/`[-1]`, matching what the
removed H7-specific override used to do. Reverted the `@PendingFeatureIf` back
to its original H5-only condition — H7's composite-key case now passes 12/12;
H5's own separate pre-existing skip is correctly untouched. Commit `da09401c4d`.
##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/AbstractGormApi.groovy:
##########
@@ -20,76 +20,188 @@ package org.grails.datastore.gorm
import java.lang.reflect.Method
import java.lang.reflect.Modifier
+import java.util.concurrent.ConcurrentHashMap
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
+import grails.gorm.MultiTenant
+import grails.gorm.multitenancy.CurrentTenantHolder
+import grails.gorm.multitenancy.Tenants
import org.grails.datastore.gorm.utils.ReflectionUtils
import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.DatastoreUtils
+import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.core.SessionCallback
+import org.grails.datastore.mapping.core.VoidSessionCallback
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import
org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
import org.grails.datastore.mapping.model.MappingContext
import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
/**
- * Abstract GORM API provider.
+ * Abstract base class for GORM API objects
*
* @author Graeme Rocher
- * @param <D> the entity/domain class
* @since 1.0
*/
@CompileStatic
abstract class AbstractGormApi<D> extends AbstractDatastoreApi {
- static final List<String> EXCLUDES = [
- 'setProperty',
- 'getProperty',
- 'getMetaClass',
- 'setMetaClass',
- 'invokeMethod',
- 'getMethods',
- 'getExtendedMethods',
- 'wait',
- 'equals',
- 'toString',
- 'hashCode',
- 'getClass',
- 'notify',
- 'notifyAll',
- 'setTransactionManager'
+ protected static final List<String> EXCLUDES = [
+ 'wait', 'notify', 'notifyAll', 'toString', 'hashCode', 'equals',
'getClass',
+ 'getMetaClass', 'setMetaClass', 'getProperty', 'setProperty',
'invokeMethod'
]
+ private static final Map<Class, List<Method>> METHODS_CACHE = new
ConcurrentHashMap<>()
+ private static final Map<Class, List<Method>> EXTENDED_METHODS_CACHE = new
ConcurrentHashMap<>()
+
protected Class<D> persistentClass
+ protected final GormRegistry registry
+ protected final String qualifier
+ protected MappingContext mappingContext
+
+ @Deprecated
protected PersistentEntity persistentEntity
+
private List<Method> methods
private List<Method> extendedMethods
AbstractGormApi(Class<D> persistentClass, Datastore datastore) {
+ this(persistentClass, datastore, (GormRegistry) null)
+ }
+
+ AbstractGormApi(Class<D> persistentClass, Datastore datastore,
GormRegistry registry) {
super(datastore)
this.persistentClass = persistentClass
- this.persistentEntity =
datastore.getMappingContext().getPersistentEntity(persistentClass.name)
+ this.registry = registry ?: GormRegistry.instance
+ this.qualifier = ConnectionSource.DEFAULT
+ this.mappingContext = datastore?.mappingContext
+ this.persistentEntity =
datastore?.mappingContext?.getPersistentEntity(persistentClass?.name)
}
- AbstractGormApi(Class<D> persistentClass, MappingContext mappingContext) {
- super(null)
+ AbstractGormApi(Class<D> persistentClass, MappingContext mappingContext,
DatastoreResolver datastoreResolver) {
+ this(persistentClass, mappingContext, datastoreResolver, (String)
null, (GormRegistry) null)
+ }
+
+ AbstractGormApi(Class<D> persistentClass, MappingContext mappingContext,
DatastoreResolver datastoreResolver, String qualifier, GormRegistry registry) {
+ super(datastoreResolver)
this.persistentClass = persistentClass
- this.persistentEntity =
mappingContext.getPersistentEntity(persistentClass.name)
+ this.registry = registry ?: GormRegistry.instance
+ this.qualifier = qualifier ?: ConnectionSource.DEFAULT
+ this.mappingContext = mappingContext
+ this.persistentEntity =
mappingContext?.getPersistentEntity(persistentClass?.name)
}
- @CompileDynamic
- protected initializeMethods(clazz) {
- while (clazz != Object) {
- final methodsToAdd = clazz.declaredMethods.findAll { Method m ->
- def mods = m.getModifiers()
- !m.isSynthetic() && !Modifier.isStatic(mods) &&
Modifier.isPublic(mods) &&
- !AbstractGormApi.EXCLUDES.contains(m.name)
+ @Override
+ protected <T1> T1 execute(SessionCallback<T1> callback) {
+ Datastore ds = getDatastore()
+ if (ds == null) {
+ throw new IllegalStateException('Cannot execute session callback
with null datastore')
+ }
+
+ String currentQualifier = getQualifier()
+ boolean isMultiTenantCapable = ds instanceof
MultiTenantCapableDatastore
+ boolean isMultiTenantEntity =
MultiTenant.isAssignableFrom(persistentClass)
+
+ // Check if we have a non-default qualifier
+ if (currentQualifier != null &&
!ConnectionSource.DEFAULT.equals(currentQualifier) &&
!ConnectionSource.OLD_DEFAULT.equalsIgnoreCase(currentQualifier)) {
+ if (isMultiTenantEntity && isMultiTenantCapable) {
+ // Determine whether the qualifier names a datasource
connection or is a tenant ID.
+ // A datasource connection qualifier resolves via
getDatastoreForConnection(); a tenant ID
+ // (e.g. from withTenant("t1")) does not. When it IS a
connection qualifier we must not
+ // bind it as the tenant ID — doing so overwrites the tenant
context set by the
+ // TenantResolver (e.g. SystemPropertyTenantResolver) and
causes discriminator filters to
+ // match the connection name instead of the real tenant.
+ boolean isConnectionQualifier = false
+ if (ds instanceof MultipleConnectionSourceCapableDatastore) {
+ try {
+ Datastore resolved =
((MultipleConnectionSourceCapableDatastore) ds)
+ .getDatastoreForConnection(currentQualifier)
+ if (resolved != null) {
+ isConnectionQualifier = true
+ }
+ } catch (Exception ignored) {
Review Comment:
Both fixed. Exception-as-control-flow: added
`ConnectionSourceNameResolver.isConnectionSourceName(datastore, name)` — checks
the datastore's declared connection-source names directly, no throwing — and
swapped the try/catch for that non-throwing call. Commit `b07c5d7f95`, +5 new
specs on `ConnectionSourceNameResolverSpec`.
On the `tenantId.toString()` flattening: traced the actual data flow through
`GormRegistry.resolveStaticApi` before deciding what 'fixed' should mean here.
In the hot-path DISCRIMINATOR-mode case, that flattened string never resolves
to a real registered api (both priority-1/2 lookups miss), falls through to the
entity's own DEFAULT api, and `executeQualified`'s `qualifiedApi != this` check
short-circuits before ever re-entering with the flattened string — so in that
mode it's a wasted `String` allocation, not a live correctness bug. The `1L` vs
`"1"` collision risk you named is real, but only manifests in DATABASE/SCHEMA
mode, where it's a property of `GormRegistry`'s entire String-keyed per-tenant
registration surface (`entityDatastores`, `staticApiRegistry`,
`instanceApiRegistry`, `validationApiRegistry`), not something unique to this
one call site — widening key types across that whole surface is a materially
bigger change than this item's scope. Documented the constraint
in a comment at the call site instead. For what it's worth, your own fix
branch left the identical line unchanged too, which is part of why I read this
as genuinely out of scope for a targeted fix here rather than something I was
skipping.
##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy:
##########
@@ -0,0 +1,992 @@
+/*
+ * 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.
+ */
+package org.grails.datastore.gorm
+
+import java.util.concurrent.ConcurrentHashMap
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import org.springframework.transaction.PlatformTransactionManager
+
+import grails.gorm.MultiTenant
+import grails.gorm.multitenancy.CurrentTenantHolder
+import org.grails.datastore.gorm.finders.FinderMethod
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport
+import
org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore
+import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
+import
org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
+import org.grails.datastore.mapping.reflect.NameUtils
+import org.grails.datastore.mapping.transactions.TransactionCapableDatastore
+
+/**
+ * A registry of GORM API objects. This registry is used to decouple the API
+ * objects from the static state in GormEnhancer.
+ *
+ * It implements an O(M+N) memory strategy where:
+ * M = Number of Entities
+ * N = Number of Connections (Tenants)
+ *
+ * @author Walter Duque de Estrada
+ * @since 8.0.0
+ */
+@Slf4j
+@CompileStatic
+class GormRegistry {
+
+ private static final GormRegistry instance = new GormRegistry()
+ private final GormApiFactory defaultApiFactory = new
DefaultGormApiFactory()
+ final GormApiResolver apiResolver = new GormApiResolver(this)
+ final GormStaticApiRegistry staticApiRegistry = new
GormStaticApiRegistry(this)
+ final GormInstanceApiRegistry instanceApiRegistry = new
GormInstanceApiRegistry(this)
+ final GormValidationApiRegistry validationApiRegistry = new
GormValidationApiRegistry(this)
+
+ final Map<String, Datastore> datastoresByQualifier = new
ConcurrentHashMap<>()
+ private final Map<String, Map<String, Datastore>> entityDatastores = new
ConcurrentHashMap<>()
+ private final Map<Class, String> normalizedEntityKeysByClass = new
ConcurrentHashMap<>()
+ private final Map<String, String> normalizedEntityKeysByName = new
ConcurrentHashMap<>()
+ private final Map<String, String> normalizedQualifiers = new
ConcurrentHashMap<>()
+ final Map<Class, Datastore> datastoresByType = new ConcurrentHashMap<>()
+ private final Map<Class, GormApiFactory> apiFactoriesByDatastoreType = new
ConcurrentHashMap<>()
+ final Set<Datastore> allDatastores = Collections.newSetFromMap(new
ConcurrentHashMap<Datastore, Boolean>())
+
+ static GormRegistry getInstance() {
+ return instance
+ }
+
+ /**
+ * @return The default datastore
+ */
+ Datastore getDefaultDatastore() {
+ return datastoresByQualifier.get(ConnectionSource.DEFAULT)
+ }
+
+ /**
+ * Resets the registry.
+ * Nominally unused in core mapping runtime code, but heavily used by
testing frameworks to reset state between spec executions.
+ */
+ static void reset() {
+ instance.resetInstance()
+ }
+
+ private void resetInstance() {
+ staticApiRegistry.clear()
+ instanceApiRegistry.clear()
+ validationApiRegistry.clear()
+ datastoresByQualifier.clear()
+ entityDatastores.clear()
+ normalizedEntityKeysByClass.clear()
+ normalizedEntityKeysByName.clear()
+ normalizedQualifiers.clear()
+ datastoresByType.clear()
+ apiFactoriesByDatastoreType.clear()
+ allDatastores.clear()
+ GormEnhancerRegistry.getInstance().clearPreferredDatastore()
+ GormEnhancerRegistry.getInstance().clearResolvingDatastoreDepth()
+ }
+
+ static <D> GormStaticApi<D> findStaticApi(Class<D> entity) {
+ instance.resolveStaticApi(entity, (String) null)
+ }
+
+ static <D> GormStaticApi<D> findStaticApi(Class<D> entity, String
qualifier) {
+ instance.resolveStaticApi(entity, qualifier)
+ }
+
+ static <D> GormInstanceApi<D> findInstanceApi(Class<D> entity) {
+ instance.resolveInstanceApi(entity, (String) null)
+ }
+
+ static <D> GormInstanceApi<D> findInstanceApi(Class<D> entity, String
qualifier) {
+ instance.resolveInstanceApi(entity, qualifier)
+ }
+
+ static <D> GormValidationApi<D> findValidationApi(Class<D> entity) {
+ instance.resolveValidationApi(entity, (String) null)
+ }
+
+ static <D> GormValidationApi<D> findValidationApi(Class<D> entity, String
qualifier) {
+ instance.resolveValidationApi(entity, qualifier)
+ }
+
+ static Datastore findDatastore(Class entity) {
+ instance.apiResolver.findDatastore(entity, (String) null)
+ }
+
+ static Datastore findDatastore(Class entity, String qualifier) {
+ instance.apiResolver.findDatastore(entity, qualifier)
+ }
+
+ /**
+ * Registers a custom GormApiFactory for a specific datastore type.
+ * Nominally unused within the core mapping module, but invoked
dynamically by external datastore implementations (e.g. Hibernate, MongoDB) to
customize API generation.
+ */
+ void registerApiFactory(Class datastoreType, GormApiFactory factory) {
+ apiFactoriesByDatastoreType.put(datastoreType, factory)
+ }
+
+ GormApiFactory getApiFactory(Datastore datastore) {
+ GormApiFactory factory =
apiFactoriesByDatastoreType.get(datastore.getClass())
+ if (factory == null) {
+ for (Map.Entry<Class, GormApiFactory> entry in
apiFactoriesByDatastoreType.entrySet()) {
+ if (entry.key.isInstance(datastore)) {
+ return entry.value
+ }
+ }
+ return defaultApiFactory
+ }
+ return factory
+ }
+
+ /**
+ * Finds a single transaction manager if only one datastore is registered.
+ * Nominally unused, but invoked at compile-time by transactional AST
transformations.
+ */
+ PlatformTransactionManager findSingleTransactionManager() {
+ return findSingleTransactionManager(ConnectionSource.DEFAULT)
+ }
+
+ /**
+ * Finds a single transaction manager for a specific qualifier.
+ * Nominally unused, but invoked at compile-time by transactional AST
transformations.
+ */
+ PlatformTransactionManager findSingleTransactionManager(String qualifier) {
+ Datastore ds = getDatastoreByString((String) null, qualifier)
+ if (ds == null) {
+ if (defaultDatastore == null) {
+ throw new IllegalStateException('No GORM implementations
configured. Ensure GORM has been initialized correctly')
+ }
+ return null
+ }
+ if (ds instanceof TransactionCapableDatastore) {
+ return ((TransactionCapableDatastore) ds).transactionManager
+ }
+ return null
+ }
+
+ /**
+ * Finds a transaction manager for a specific entity class and qualifier.
+ * Nominally unused, but invoked at compile-time by transactional/service
AST transformations.
+ */
+ PlatformTransactionManager findTransactionManager(Class entityClass,
String qualifier) {
+ Datastore ds = getDatastore(entityClass, qualifier)
+ if (ds == null) {
+ // The qualifier may be a tenant ID rather than a registered
datastore qualifier
+ // (e.g. DISCRIMINATOR / SCHEMA multi-tenancy). Fall back via the
full resolver
+ // which understands the multi-tenancy mode and returns the
correct datastore.
+ ds = apiResolver.findDatastore(entityClass, qualifier)
+ }
+ if (ds == null) {
+ if (defaultDatastore == null) {
+ throw new IllegalStateException('No GORM implementations
configured. Ensure GORM has been initialized correctly')
+ }
+ return null
+ }
+ if (ds instanceof TransactionCapableDatastore) {
+ return ((TransactionCapableDatastore) ds).transactionManager
+ }
+ return null
+ }
+
+ /**
+ * Finds a transaction manager for a specific entity class.
+ * Nominally unused, but invoked at compile-time by transactional/service
AST transformations.
+ */
+ PlatformTransactionManager findTransactionManager(Class entityClass) {
+ return findTransactionManager(entityClass, ConnectionSource.DEFAULT)
+ }
+
+ /**
+ * Finds a datastore for a specific qualifier (connection name).
+ */
+ Datastore getDatastore(String qualifier) {
+ return getDatastoreByString((String) null, qualifier)
+ }
+
+ /**
+ * Internal method to avoid redundant normalization.
+ */
+ Datastore getDatastoreDirect(String normalizedClassName, String
normalizedQualifier) {
+ if (normalizedClassName != null) {
+ Map<String, Datastore> mappedDatastores =
entityDatastores.get(normalizedClassName)
+ if (mappedDatastores != null) {
+ Datastore ds = mappedDatastores.get(normalizedQualifier)
+ if (ds != null) {
+ return ds
+ }
+ if (ConnectionSource.DEFAULT.equals(normalizedQualifier) &&
!mappedDatastores.isEmpty()) {
+ return mappedDatastores.values().iterator().next()
Review Comment:
Traced this to the removal path specifically — registration already
deterministically tracks the entity's first declared connection and assigns
DEFAULT correctly; the arbitrary pick only happened after
`removeDatastore`/`removeEntityDatastore` stripped an entry and left the map
non-empty but DEFAULT-less. Reproduced first as a failing test (registered an
entity on 3 declared connections, removed the one DEFAULT pointed at, asserted
DEFAULT re-points at the next declared one — failed against unfixed code,
landing on whichever entry `ConcurrentHashMap`'s hash order happened to hand
back). Fixed with `entityConnectionOrder` (a declared-order list per entity)
plus a `repairDefaultRouting` helper called from both removal paths. Commit
`63c2c20021`.
Also removed the `mappedDatastores.values().iterator().next()` fallback
itself as defense-in-depth on top of the repair (same change your branch made)
— any not-yet-found edge case now fails loud instead of silently routing to a
wrong datastore. Commit `b81bda3f73`. One thing I checked: your branch's
version of this fix doesn't actually repair the removal-path bug — deleting the
fallback alone just turns it into DEFAULT resolving to `null` post-removal
rather than a wrong-but-nonnull datastore, which is a harder failure, not a fix
for the underlying issue. Kept both pieces (the repair mechanism plus the
loud-failure fallback) rather than just the latter.
##########
grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/CurrentTenantHolder.groovy:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+package grails.gorm.multitenancy
+
+import groovy.transform.CompileStatic
+
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.connections.ConnectionSource
+
+@CompileStatic
+class CurrentTenantHolder {
+
+ private static final ThreadLocal<Map<Object, Serializable>>
currentTenantThreadLocal = new ThreadLocal<Map<Object, Serializable>>() {
+ @Override
+ protected Map<Object, Serializable> initialValue() {
+ return new HashMap<>()
+ }
+ }
+
+ /**
+ * @return Obtain the current tenant (fallback for any datastore)
+ */
+ static Serializable get() {
+ def map = currentTenantThreadLocal.get()
+ if (!map.isEmpty()) {
+ return map.values().iterator().next()
Review Comment:
Made it total rather than dropping it — grepped every module
(grails-datamapping-core/H5/H7/Mongo/Simple/Neo4j/datastore-core/rx/tck) for
production callers of the no-arg `get()` and found none, only 2 test files,
confirming in-tree callers can all pass a datastore already. `get()` now
collects the distinct bound tenant values; throws `TenantException` (existing
type, not new) naming the ambiguous tenants when more than one is bound,
returns the single value otherwise, or `null` if nothing's bound —
same-tenant-bound-to-multiple-datastores correctly does not throw. Commit
`4909abfc67`. Reproduced the bug as a failing test first, before implementing.
This matches your branch's fix for the same comment.
Also added the `grails-doc` coverage you asked for — this whole class is now
documented in the new multiTenancy.adoc page.
--
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]