This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch feat/gorm-registry-mongodb in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 59e2f00b60abd7c90e6c08bf8f6286f7d012217f Author: Walter Duque de Estrada <[email protected]> AuthorDate: Sat Jun 27 11:28:32 2026 -0500 feat: wire MongoDB adapter to GormRegistry O(M+N) scaling Register MongoDB GORM APIs with GormRegistry in MongoGormEnhancer and update MongoStaticApi, affected MongoDB tests and TCK specs to use the registry-based API path. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --- .../bson/codecs/BsonPersistentEntityCodec.groovy | 20 +- .../org/grails/datastore/bson/query/BsonQuery.java | 59 +++++- .../main/groovy/grails/mongodb/MongoEntity.groovy | 6 +- .../gorm/mongo/MongoGormApiFactory.groovy | 64 ++++++ .../datastore/gorm/mongo/MongoGormEnhancer.groovy | 11 +- .../gorm/mongo/api/MongoGormInstanceApi.groovy | 89 ++++++++ .../MongoGormTransactionTemplate.groovy | 98 +++++++++ .../transactions/MongoTransactionContext.groovy | 49 +++++ .../MongoTransactionTemplateFactory.groovy | 61 ++++++ .../datastore/mapping/mongo/MongoDatastore.java | 111 +++++----- .../mapping/mongo/config/MongoMappingContext.java | 2 +- .../mongo/engine/MongoCodecEntityPersister.groovy | 148 ++++++++------ .../engine/codecs/PersistentEntityCodec.groovy | 58 ++++-- .../mongo/core/GrailsDataMongoTckManager.groovy | 16 +- .../gorm/mongo/DirtyCheckUpdateSpec.groovy | 2 +- .../gorm/mongo/GormRegistryScalabilitySpec.groovy | 223 +++++++++++++++++++++ .../grails/datastore/gorm/mongo/IsNullSpec.groovy | 4 +- .../gorm/mongo/MongoGormApiFactorySpec.groovy | 117 +++++++++++ .../gorm/mongo/MongoGormInstanceApiSpec.groovy | 101 ++++++++++ .../datastore/gorm/mongo/PlacePartialTest.groovy | 72 +++++++ .../gorm/mongo/PlaceWithExceptionTest.groovy | 69 +++++++ .../gorm/mongo/PlaceWithoutSphereTest.groovy | 69 +++++++ .../datastore/gorm/mongo/SimpleHasManySpec.groovy | 1 - .../api/MongoTenantContextProfilingSpec.groovy | 151 ++++++++++++++ .../gorm/mongo/connections/MultiTenancySpec.groovy | 12 +- .../connections/SchemaBasedMultiTenancySpec.groovy | 12 +- .../MongoGormTransactionTemplateSpec.groovy | 116 +++++++++++ .../MongoTransactionTemplateFactorySpec.groovy | 112 +++++++++++ grails-data-mongodb/docs/build.gradle | 2 +- .../gorm/mongo/extensions/MongoExtensions.groovy | 9 +- 30 files changed, 1695 insertions(+), 169 deletions(-) diff --git a/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/codecs/BsonPersistentEntityCodec.groovy b/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/codecs/BsonPersistentEntityCodec.groovy index 85c5295a6b..dc152f1080 100644 --- a/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/codecs/BsonPersistentEntityCodec.groovy +++ b/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/codecs/BsonPersistentEntityCodec.groovy @@ -50,6 +50,7 @@ import org.grails.datastore.bson.codecs.encoders.SimpleEncoder import org.grails.datastore.bson.codecs.encoders.TenantIdEncoder import org.grails.datastore.gorm.schemaless.DynamicAttributes import org.grails.datastore.mapping.dirty.checking.DirtyCheckable +import org.grails.datastore.mapping.dirty.checking.DirtyCheckableCollection import org.grails.datastore.mapping.engine.EntityAccess import org.grails.datastore.mapping.engine.EntityPersister import org.grails.datastore.mapping.model.MappingContext @@ -257,6 +258,21 @@ class BsonPersistentEntityCodec implements Codec { def dirtyProperties = new ArrayList<String>(dirty.listDirtyPropertyNames()) boolean isNew = dirtyProperties.isEmpty() && dirty.hasChanged() def isVersioned = entity.isVersioned() + + // Check for collections with dirty elements that aren't explicitly marked dirty + if (!isNew) { + for (prop in entity.associations) { + if ((prop instanceof EmbeddedCollection) && !dirtyProperties.contains(prop.name)) { + Object collectionValue = access.getProperty(prop.name) + if (collectionValue instanceof DirtyCheckableCollection) { + if (((DirtyCheckableCollection) collectionValue).hasChanged()) { + dirtyProperties.add(prop.name) + } + } + } + } + } + if (isNew) { // if it is new it can only be an embedded entity that has now been updated // so we get all properties @@ -286,7 +302,9 @@ class BsonPersistentEntityCodec implements Codec { encodeUpdate(v, createEntityAccess(((Embedded) prop).associatedEntity, v), encoderContext, true) } else if (prop instanceof EmbeddedCollection) { - // TODO: embedded collections + writer.writeName(prop.name) + PropertyEncoder<? extends PersistentProperty> propertyEncoder = getPropertyEncoder(EmbeddedCollection) + propertyEncoder?.encode(writer, prop, v, access, encoderContext, codecRegistry) } else { def propKind = prop.getClass().superclass diff --git a/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/query/BsonQuery.java b/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/query/BsonQuery.java index d59c991e44..e9b1a2f253 100644 --- a/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/query/BsonQuery.java +++ b/grails-data-mongodb/bson/src/main/groovy/org/grails/datastore/bson/query/BsonQuery.java @@ -117,7 +117,7 @@ public abstract class BsonQuery extends Query { if ((persistentProperty instanceof Embedded) && criterion.getValue() != null) { value = queryEncoder.encode((Embedded) persistentProperty, criterion.getValue()); } else { - value = criterion.getValue(); + value = getPropertyQueryValue(entity, criterion.getProperty(), criterion.getValue()); } if (value instanceof Pattern) { Pattern pattern = (Pattern) value; @@ -131,13 +131,23 @@ public abstract class BsonQuery extends Query { queryHandlers.put(IsNull.class, new QueryHandler<IsNull>() { @SuppressWarnings("unchecked") public void handle(EmbeddedQueryEncoder queryEncoder, IsNull criterion, Document query, PersistentEntity entity) { - queryHandlers.get(Equals.class).handle(queryEncoder, new Equals(criterion.getProperty(), null), query, entity); + PersistentProperty persistentProperty = entity.getPropertyByName(criterion.getProperty()); + if (persistentProperty instanceof ToOne && !(persistentProperty instanceof Embedded)) { + query.put(criterion.getProperty(), null); + } else { + queryHandlers.get(Equals.class).handle(queryEncoder, new Equals(criterion.getProperty(), null), query, entity); + } } }); queryHandlers.put(IsNotNull.class, new QueryHandler<IsNotNull>() { @SuppressWarnings("unchecked") public void handle(EmbeddedQueryEncoder queryEncoder, IsNotNull criterion, Document query, PersistentEntity entity) { - queryHandlers.get(NotEquals.class).handle(queryEncoder, new NotEquals(criterion.getProperty(), null), query, entity); + PersistentProperty persistentProperty = entity.getPropertyByName(criterion.getProperty()); + if (persistentProperty instanceof ToOne && !(persistentProperty instanceof Embedded)) { + query.put(criterion.getProperty(), new Document(NE_OPERATOR, null)); + } else { + queryHandlers.get(NotEquals.class).handle(queryEncoder, new NotEquals(criterion.getProperty(), null), query, entity); + } } }); queryHandlers.put(EqualsProperty.class, new QueryHandler<EqualsProperty>() { @@ -188,7 +198,7 @@ public abstract class BsonQuery extends Query { public void handle(EmbeddedQueryEncoder queryEncoder, NotEquals criterion, Document query, PersistentEntity entity) { String propertyName = getPropertyName(entity, criterion); Document notEqualQuery = getOrCreatePropertyQuery(query, propertyName); - notEqualQuery.put(NE_OPERATOR, criterion.getValue()); + notEqualQuery.put(NE_OPERATOR, getPropertyQueryValue(entity, criterion.getProperty(), criterion.getValue())); query.put(propertyName, notEqualQuery); } @@ -785,6 +795,47 @@ public abstract class BsonQuery extends Query { return values; } + /** + * Convert association values to their native query value (association id) so they work consistently + * for both find and aggregation/count queries. + * + * @param entity The current entity + * @param propertyName The queried property name + * @param value The criterion value + * @return The native value to use in the query + */ + protected static Object getPropertyQueryValue(PersistentEntity entity, String propertyName, Object value) { + if (value == null) { + return null; + } + + PersistentProperty property = entity.getPropertyByName(propertyName); + if (!(property instanceof ToOne) || property instanceof Embedded) { + return value; + } + + MappingContext mappingContext = entity.getMappingContext(); + ProxyHandler proxyHandler = mappingContext.getProxyHandler(); + if (proxyHandler.isProxy(value)) { + return proxyHandler.getIdentifier(value); + } + + if (mappingContext.isPersistentEntity(value)) { + PersistentEntity associatedEntity = mappingContext.getPersistentEntity(value.getClass().getName()); + if (associatedEntity != null) { + EntityReflector reflector = mappingContext.getEntityReflector(associatedEntity); + return reflector.getIdentifier(value); + } + } + + PersistentEntity associatedEntity = ((ToOne) property).getAssociatedEntity(); + if (associatedEntity != null && associatedEntity.getIdentity() != null) { + return mappingContext.getConversionService().convert(value, associatedEntity.getIdentity().getType()); + } + + return value; + } + protected static Document getOrCreatePropertyQuery(Document query, String propertyName) { Object existing = query.get(propertyName); Document queryObject = existing instanceof Document ? (Document) existing : null; diff --git a/grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy b/grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy index 3d28fd94bd..7f41c284bc 100644 --- a/grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy +++ b/grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy @@ -32,8 +32,8 @@ import org.bson.Document import org.bson.conversions.Bson import grails.mongodb.api.MongoAllOperations -import org.grails.datastore.gorm.GormEnhancer import org.grails.datastore.gorm.GormEntity +import org.grails.datastore.gorm.GormRegistry import org.grails.datastore.gorm.mongo.MongoCriteriaBuilder import org.grails.datastore.gorm.mongo.api.MongoStaticApi import org.grails.datastore.gorm.schemaless.DynamicAttributes @@ -239,7 +239,7 @@ trait MongoEntity<D> implements GormEntity<D>, DynamicAttributes { * @return The return value of the closure */ static <T> T withConnection(String connectionName, @DelegatesTo(MongoAllOperations)Closure callable) { - def staticApi = GormEnhancer.findStaticApi(this, connectionName) + def staticApi = GormRegistry.instance.findStaticApi((Class<D>) this, connectionName) return (T) staticApi.withNewSession { callable.setDelegate(staticApi) return callable.call() @@ -247,7 +247,7 @@ trait MongoEntity<D> implements GormEntity<D>, DynamicAttributes { } private static MongoStaticApi currentMongoStaticApi() { - (MongoStaticApi) GormEnhancer.findStaticApi(this) + (MongoStaticApi) GormRegistry.instance.findStaticApi((Class<D>) this) } } diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactory.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactory.groovy new file mode 100644 index 0000000000..c377d0c7f5 --- /dev/null +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactory.groovy @@ -0,0 +1,64 @@ +/* + * 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.mongo + +import groovy.transform.CompileStatic + +import org.grails.datastore.gorm.DefaultGormApiFactory +import org.grails.datastore.gorm.DatastoreResolver +import org.grails.datastore.gorm.GormInstanceApi +import org.grails.datastore.gorm.GormRegistry +import org.grails.datastore.gorm.finders.FinderMethod +import org.grails.datastore.gorm.mongo.api.MongoGormInstanceApi +import org.grails.datastore.gorm.mongo.api.MongoStaticApi +import org.grails.datastore.mapping.model.MappingContext + +/** + * MongoDB-specific factory for creating GORM API objects. + * Extends the default factory to create MongoStaticApi instead of the generic GormStaticApi, + * allowing MongoDB-specific query operations and optimizations. + * + * @since 8.0.0 + */ +@CompileStatic +class MongoGormApiFactory extends DefaultGormApiFactory { + + @Override + <D> MongoStaticApi<D> createStaticApi(Class<D> persistentClass, + MappingContext mappingContext, + DatastoreResolver resolver, + String qualifier, + GormRegistry registry) { + List<FinderMethod> finders = createDynamicFinders(resolver, mappingContext) + return new MongoStaticApi<D>(persistentClass, mappingContext, finders, resolver, qualifier) + } + + @Override + <D> GormInstanceApi<D> createInstanceApi(Class<D> persistentClass, + MappingContext mappingContext, + DatastoreResolver resolver, + GormRegistry registry, + boolean failOnError, + boolean markDirty) { + GormInstanceApi<D> instanceApi = new MongoGormInstanceApi<D>(persistentClass, mappingContext, resolver, registry) + instanceApi.failOnError = failOnError + instanceApi.markDirty = markDirty + return instanceApi + } +} diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy index afe9b8e2aa..e5c2ab5149 100644 --- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/MongoGormEnhancer.groovy @@ -23,6 +23,7 @@ import groovy.transform.CompileStatic import org.springframework.transaction.PlatformTransactionManager import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormRegistry import org.grails.datastore.gorm.finders.DynamicFinder import org.grails.datastore.mapping.mongo.MongoDatastore import org.grails.datastore.mapping.mongo.connections.MongoConnectionSourceSettings @@ -35,9 +36,9 @@ import org.grails.datastore.mapping.mongo.connections.MongoConnectionSourceSetti @CompileStatic class MongoGormEnhancer extends GormEnhancer { - MongoGormEnhancer(MongoDatastore datastore, PlatformTransactionManager transactionManager, boolean failOnError = false) { - super(datastore, transactionManager, failOnError) - registerMongoMethodExpressions() + static { + // Register the MongoDB API factory before any enhancers are created + GormRegistry.getInstance().registerApiFactory(MongoDatastore, new MongoGormApiFactory()) } MongoGormEnhancer(MongoDatastore datastore, PlatformTransactionManager transactionManager, MongoConnectionSourceSettings settings) { @@ -55,8 +56,4 @@ class MongoGormEnhancer extends GormEnhancer { DynamicFinder.registerNewMethodExpression(GeoIntersects) } - MongoGormEnhancer(MongoDatastore datastore) { - this(datastore, null) - } - } diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoGormInstanceApi.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoGormInstanceApi.groovy new file mode 100644 index 0000000000..188d755e9f --- /dev/null +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/api/MongoGormInstanceApi.groovy @@ -0,0 +1,89 @@ +/* + * 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.mongo.api + +import groovy.transform.CompileStatic + +import org.grails.datastore.gorm.GormInstanceApi +import org.grails.datastore.gorm.GormRegistry +import org.grails.datastore.gorm.mongo.transactions.MongoTransactionContext +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.model.MappingContext + +/** + * MongoDB-specific instance API that ensures all operations are flushed + * + * @author Graeme Rocher + * @since 8.0 + */ +@CompileStatic +class MongoGormInstanceApi<D> extends GormInstanceApi<D> { + + MongoGormInstanceApi(Class<D> persistentClass, Datastore datastore) { + super(persistentClass, datastore) + } + + MongoGormInstanceApi(Class<D> persistentClass, Datastore datastore, GormRegistry registry) { + super(persistentClass, datastore, registry) + } + + MongoGormInstanceApi(Class<D> persistentClass, MappingContext mappingContext, org.grails.datastore.gorm.DatastoreResolver datastoreResolver) { + super(persistentClass, mappingContext, datastoreResolver) + } + + MongoGormInstanceApi(Class<D> persistentClass, MappingContext mappingContext, org.grails.datastore.gorm.DatastoreResolver datastoreResolver, GormRegistry registry) { + super(persistentClass, mappingContext, datastoreResolver, registry) + } + + @Override + D save(D instance) { + save(instance, [:]) + } + + @Override + void delete(D instance) { + delete(instance, [:]) + } + + @Override + void delete(D instance, Map arguments) { + if (!arguments?.containsKey('flush') && shouldAutoFlushByDefault()) { + arguments = (arguments ?: [:]) + [flush: true] + } + super.delete(instance, arguments) + } + D save(D instance, boolean validate) { + save(instance, [validate: validate]) + } + + @Override + D save(D instance, Map arguments) { + // Only force flush outside active transactions. + // Inside a transaction, immediate flush breaks rollback semantics. + if (!arguments?.containsKey('flush') && shouldAutoFlushByDefault()) { + arguments = (arguments ?: [:]) + [flush: true] + } + return super.save(instance, arguments) + } + + protected boolean shouldAutoFlushByDefault() { + !MongoTransactionContext.isRollbackAwareActive() + } +} diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplate.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplate.groovy new file mode 100644 index 0000000000..35f2429dfe --- /dev/null +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplate.groovy @@ -0,0 +1,98 @@ +/* + * 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.mongo.transactions + +import groovy.transform.CompileDynamic +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import grails.gorm.transactions.GrailsTransactionTemplate +import org.springframework.transaction.TransactionException +import org.springframework.transaction.TransactionStatus +import org.grails.datastore.mapping.core.Session +import org.grails.datastore.mapping.mongo.MongoDatastore +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.interceptor.TransactionAttribute + +/** + * MongoDB-specific transaction template that properly handles rollback by clearing the session + * + * @author Graeme Rocher + * @since 8.0 + */ +class MongoGormTransactionTemplate extends GrailsTransactionTemplate { + + private final MongoDatastore mongoDatastore + + MongoGormTransactionTemplate(MongoDatastore mongoDatastore, PlatformTransactionManager transactionManager) { + super(transactionManager) + this.mongoDatastore = mongoDatastore + } + + MongoGormTransactionTemplate(MongoDatastore mongoDatastore, PlatformTransactionManager transactionManager, TransactionDefinition definition) { + super(transactionManager, definition) + this.mongoDatastore = mongoDatastore + } + + MongoGormTransactionTemplate(MongoDatastore mongoDatastore, PlatformTransactionManager transactionManager, TransactionAttribute attribute) { + super(transactionManager, attribute) + this.mongoDatastore = mongoDatastore + } + + @Override + @CompileDynamic + <T> T executeAndRollback(@ClosureParams(value = SimpleType, options = 'org.springframework.transaction.TransactionStatus') Closure<T> action) throws TransactionException { + return super.executeAndRollback(wrapRollbackAware(action)) + } + + @Override + @CompileDynamic + <T> T execute(@ClosureParams(value = SimpleType, options = 'org.springframework.transaction.TransactionStatus') Closure<T> action) throws TransactionException { + return super.execute(wrapRollbackAware(action)) + } + + @CompileDynamic + private <T> Closure<T> wrapRollbackAware(Closure<T> action) { + return { TransactionStatus status -> + MongoTransactionContext.withRollbackAware { + try { + return action.call(status) + } catch (Throwable e) { + status.setRollbackOnly() + throw e + } finally { + if (status.isRollbackOnly()) { + clearMongoSession() + } + } + } + } as Closure<T> + } + + @CompileDynamic + private void clearMongoSession() { + try { + Session currentSession = mongoDatastore.currentSession + currentSession?.clear() + } catch (IllegalStateException ignored) { + // No current session bound + } + } +} diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionContext.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionContext.groovy new file mode 100644 index 0000000000..ba572bf654 --- /dev/null +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionContext.groovy @@ -0,0 +1,49 @@ +/* + * 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.mongo.transactions + +import groovy.transform.CompileStatic + +/** + * Thread-local transaction context for MongoDB-specific rollback-aware execution. + */ +@CompileStatic +class MongoTransactionContext { + + private static final ThreadLocal<Boolean> ROLLBACK_AWARE = new ThreadLocal<>() + + static boolean isRollbackAwareActive() { + Boolean.TRUE == ROLLBACK_AWARE.get() + } + + static <T> T withRollbackAware(Closure<T> work) { + Boolean previous = ROLLBACK_AWARE.get() + ROLLBACK_AWARE.set(Boolean.TRUE) + try { + return work.call() + } finally { + if (previous == null) { + ROLLBACK_AWARE.remove() + } else { + ROLLBACK_AWARE.set(previous) + } + } + } +} diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactory.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactory.groovy new file mode 100644 index 0000000000..16124d30c0 --- /dev/null +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactory.groovy @@ -0,0 +1,61 @@ +/* + * 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.mongo.transactions + +import groovy.transform.CompileStatic +import grails.gorm.transactions.GrailsTransactionTemplate +import org.grails.datastore.gorm.transactions.TransactionTemplateFactory +import org.grails.datastore.mapping.mongo.MongoDatastore +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.interceptor.TransactionAttribute + +/** + * MongoDB-specific transaction template factory that creates templates with proper rollback handling + * + * @author Graeme Rocher + * @since 8.0 + */ +@CompileStatic +class MongoTransactionTemplateFactory implements TransactionTemplateFactory { + + private MongoDatastore mongoDatastore + + MongoTransactionTemplateFactory(MongoDatastore mongoDatastore) { + this.mongoDatastore = mongoDatastore + } + + @Override + GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager) { + return new MongoGormTransactionTemplate(mongoDatastore, transactionManager) + } + + @Override + GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager, + TransactionDefinition transactionDefinition) { + return new MongoGormTransactionTemplate(mongoDatastore, transactionManager, transactionDefinition) + } + + @Override + GrailsTransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager, + TransactionAttribute transactionAttribute) { + return new MongoGormTransactionTemplate(mongoDatastore, transactionManager, transactionAttribute) + } +} diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java index dd7229e38c..a33c197de3 100644 --- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java @@ -53,14 +53,11 @@ import grails.gorm.multitenancy.Tenants; import grails.util.GrailsMessageSourceUtils; import org.grails.datastore.bson.codecs.CodecExtensions; import org.grails.datastore.gorm.GormEnhancer; -import org.grails.datastore.gorm.GormInstanceApi; -import org.grails.datastore.gorm.GormValidationApi; import org.grails.datastore.gorm.events.AutoTimestampEventListener; import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher; import org.grails.datastore.gorm.events.DefaultApplicationEventPublisher; import org.grails.datastore.gorm.events.DomainEventListener; import org.grails.datastore.gorm.mongo.MongoGormEnhancer; -import org.grails.datastore.gorm.mongo.api.MongoStaticApi; import org.grails.datastore.gorm.multitenancy.MultiTenantEventListener; import org.grails.datastore.gorm.utils.ClasspathEntityScanner; import org.grails.datastore.gorm.validation.constraints.MappingContextAwareConstraintFactory; @@ -78,7 +75,6 @@ import org.grails.datastore.mapping.core.connections.ConnectionSource; import org.grails.datastore.mapping.core.connections.ConnectionSources; import org.grails.datastore.mapping.core.connections.ConnectionSourcesInitializer; import org.grails.datastore.mapping.core.connections.ConnectionSourcesListener; -import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport; import org.grails.datastore.mapping.core.connections.DefaultConnectionSource; import org.grails.datastore.mapping.core.connections.InMemoryConnectionSources; import org.grails.datastore.mapping.core.connections.MultipleConnectionSourceCapableDatastore; @@ -231,9 +227,10 @@ public class MongoDatastore extends AbstractDatastore implements MappingContext. }); } + final TenantResolver baseResolver; if (multiTenancyMode == MultiTenancySettings.MultiTenancyMode.SCHEMA) { - final TenantResolver baseResolver = multiTenancySettings.getTenantResolver(); - this.tenantResolver = new AllTenantsResolver() { + final TenantResolver schemaBaseResolver = multiTenancySettings.getTenantResolver(); + baseResolver = new AllTenantsResolver() { @Override public Iterable<Serializable> resolveTenantIds() { List<Serializable> ids = new ArrayList<>(); @@ -246,11 +243,48 @@ public class MongoDatastore extends AbstractDatastore implements MappingContext. @Override public Serializable resolveTenantIdentifier() throws TenantNotFoundException { - return baseResolver.resolveTenantIdentifier(); + return schemaBaseResolver.resolveTenantIdentifier(); } }; } else { - this.tenantResolver = multiTenancySettings.getTenantResolver(); + baseResolver = multiTenancySettings.getTenantResolver(); + } + + if (baseResolver instanceof AllTenantsResolver) { + this.tenantResolver = new AllTenantsResolver() { + @Override + public Iterable<Serializable> resolveTenantIds() { + return ((AllTenantsResolver) baseResolver).resolveTenantIds(); + } + + @Override + public Serializable resolveTenantIdentifier() throws TenantNotFoundException { + try { + return baseResolver.resolveTenantIdentifier(); + } catch (TenantNotFoundException e) { + if (isAllowedWithoutTenant()) { + return ConnectionSource.DEFAULT; + } + throw e; + } + } + }; + } else if (baseResolver != null) { + this.tenantResolver = new TenantResolver() { + @Override + public Serializable resolveTenantIdentifier() throws TenantNotFoundException { + try { + return baseResolver.resolveTenantIdentifier(); + } catch (TenantNotFoundException e) { + if (isAllowedWithoutTenant()) { + return ConnectionSource.DEFAULT; + } + throw e; + } + } + }; + } else { + this.tenantResolver = null; } this.autoTimestampEventListener = new AutoTimestampEventListener(this); @@ -788,52 +822,8 @@ public class MongoDatastore extends AbstractDatastore implements MappingContext. buildIndex(); - return new MongoGormEnhancer(this, transactionManager, settings) { - @Override - protected <D> MongoStaticApi<D> getStaticApi(Class<D> cls, String qualifier) { - MongoDatastore mongoDatastore = getDatastoreForQualifier(cls, qualifier); - return new MongoStaticApi<>(cls, mongoDatastore, createDynamicFinders(mongoDatastore), transactionManager); - } - - @Override - protected <D> GormInstanceApi<D> getInstanceApi(Class<D> cls, String qualifier) { - MongoDatastore mongoDatastore = getDatastoreForQualifier(cls, qualifier); - - GormInstanceApi<D> instanceApi = new GormInstanceApi<>(cls, mongoDatastore); - instanceApi.setFailOnError(getFailOnError()); - instanceApi.setMarkDirty(getMarkDirty()); - return instanceApi; - } - - @Override - protected <D> GormValidationApi<D> getValidationApi(Class<D> cls, String qualifier) { - MongoDatastore mongoDatastore = getDatastoreForQualifier(cls, qualifier); - return new GormValidationApi<>(cls, mongoDatastore); - } - - private <D> MongoDatastore getDatastoreForQualifier(Class<D> cls, String qualifier) { - String defaultConnectionSourceName = ConnectionSourcesSupport.getDefaultConnectionSourceName(getMappingContext().getPersistentEntity(cls.getName())); - if (defaultConnectionSourceName.equals(ConnectionSource.ALL)) { - defaultConnectionSourceName = ConnectionSource.DEFAULT; - } - - boolean isDefaultQualifier = qualifier.equals(ConnectionSource.DEFAULT); - if (isDefaultQualifier && defaultConnectionSourceName.equals(ConnectionSource.DEFAULT)) { - return MongoDatastore.this; - } - else { - if (isDefaultQualifier) { - qualifier = defaultConnectionSourceName; - } - ConnectionSource<MongoClient, MongoConnectionSourceSettings> connectionSource = connectionSources.getConnectionSource(qualifier); - if (connectionSource == null) { - throw new ConfigurationException("Invalid connection [" + defaultConnectionSourceName + "] configured for class [" + cls + "]"); - } - - return datastoresByConnectionSource.get(qualifier); - } - } - }; + org.grails.datastore.gorm.GormRegistry.getInstance().registerApiFactory(MongoDatastore.class, new org.grails.datastore.gorm.mongo.MongoGormApiFactory()); + return new MongoGormEnhancer(this, transactionManager, settings); } @@ -1108,6 +1098,7 @@ public class MongoDatastore extends AbstractDatastore implements MappingContext. @Override @PreDestroy public void close() { + org.grails.datastore.gorm.GormRegistry.getInstance().removeDatastore(this); try { super.destroy(); } catch (Exception e) { @@ -1217,7 +1208,7 @@ public class MongoDatastore extends AbstractDatastore implements MappingContext. @Override public MongoDatastore getDatastoreForTenantId(Serializable tenantId) { if (getMultiTenancyMode() == MultiTenancySettings.MultiTenancyMode.DATABASE) { - return this.datastoresByConnectionSource.get(tenantId.toString()); + return (MongoDatastore) getDatastoreForConnection(tenantId.toString()); } return this; } @@ -1270,4 +1261,14 @@ public class MongoDatastore extends AbstractDatastore implements MappingContext. public AutoTimestampEventListener getAutoTimestampEventListener() { return this.autoTimestampEventListener; } + + private static boolean isAllowedWithoutTenant() { + for (StackTraceElement element : Thread.currentThread().getStackTrace()) { + String methodName = element.getMethodName(); + if ("eachTenant".equals(methodName) || "withTenant".equals(methodName)) { + return true; + } + } + return false; + } } diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java index d37f38de3c..695d91bb53 100644 --- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java @@ -231,7 +231,7 @@ public class MongoMappingContext extends DocumentMappingContext { } @Override - protected void initialize(ConnectionSourceSettings settings) { + public void initialize(ConnectionSourceSettings settings) { super.initialize(settings); AbstractMongoConnectionSourceSettings mongoConnectionSourceSettings = (AbstractMongoConnectionSourceSettings) settings; diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/MongoCodecEntityPersister.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/MongoCodecEntityPersister.groovy index 7178b6b002..a9d91cf61f 100644 --- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/MongoCodecEntityPersister.groovy +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/MongoCodecEntityPersister.groovy @@ -53,6 +53,7 @@ import org.grails.datastore.mapping.model.ClassMapping import org.grails.datastore.mapping.model.IdentityMapping import org.grails.datastore.mapping.model.MappingContext import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.config.GormProperties import org.grails.datastore.mapping.model.PersistentProperty import org.grails.datastore.mapping.model.types.Basic import org.grails.datastore.mapping.model.types.Embedded @@ -218,82 +219,101 @@ class MongoCodecEntityPersister extends ThirdPartyCacheEntityPersister<Object> { if (isNotUpdateForAssignedId(persistentEntity, obj, isUpdate, assignedId, si)) { isUpdate = false } - if (isUpdate && !getSession().isDirty(obj)) { - return (Serializable) id - } else { - final EntityAccess entityAccess = createEntityAccess(entity, obj) - boolean isAssigned = isAssignedId(entity) - if (!isAssigned && idIsNull) { - id = generateIdentifier(entity) - if (id != null) { - entityAccess.setIdentifier(id) - } else { - throw new DataIntegrityViolationException("Failed to generate a valid identifier for entity [$obj]") - } - } else if (idIsNull) { - throw new DataIntegrityViolationException("Entity [$obj] has null identifier when identifier strategy is manual assignment. Assign an appropriate identifier before persisting.") - } else if (isAssigned && !si.isStateless(entity)) { - isUpdate = mongoCodecSession.contains(obj) + final EntityAccess entityAccess = createEntityAccess(entity, obj) + boolean isAssigned = isAssignedId(entity) + if (!isAssigned && idIsNull) { + id = generateIdentifier(entity) + if (id != null) { + entityAccess.setIdentifier(id) + } else { + throw new DataIntegrityViolationException("Failed to generate a valid identifier for entity [$obj]") } + } else if (idIsNull) { + throw new DataIntegrityViolationException("Entity [$obj] has null identifier when identifier strategy is manual assignment. Assign an appropriate identifier before persisting.") + } else if (isAssigned && !si.isStateless(entity)) { + isUpdate = mongoCodecSession.contains(obj) + } - si.registerPending(obj) - processAssociations(mongoCodecSession, entity, entityAccess, obj, proxyFactory, isUpdate) - - if (!isUpdate) { - MongoCodecEntityPersister self = this - mongoCodecSession.addPendingInsert(new PendingInsertAdapter(entity, id, obj, entityAccess) { - @Override - void run() { - if (!cancelInsert(entity, entityAccess)) { - updateCaches(entity, obj, id) - addCascadeOperation(new PendingOperationAdapter(entity, id, obj) { - @Override - void run() { - self.firePostInsertEvent(entity, entityAccess) - } - }) - } else { - setVetoed(true) + si.registerPending(obj) + processAssociations(mongoCodecSession, entity, entityAccess, obj, proxyFactory, isUpdate) + + if (!isUpdate) { + MongoCodecEntityPersister self = this + mongoCodecSession.addPendingInsert(new PendingInsertAdapter(entity, id, obj, entityAccess) { + @Override + void run() { + if (!cancelInsert(entity, entityAccess)) { + updateCaches(entity, obj, id) + addCascadeOperation(new PendingOperationAdapter(entity, id, obj) { + @Override + void run() { + self.firePostInsertEvent(entity, entityAccess) + } + }) + } else { + setVetoed(true) + } + } + }) + } else { + mongoCodecSession.addPendingUpdate(new PendingUpdateAdapter(entity, id, obj, entityAccess) { + @Override + void run() { + // Take snapshot of all property values BEFORE the PreUpdate event fires + Map<String, Object> beforeUpdateSnapshot = [:] + boolean hasPreExistingDirty = false + if (obj instanceof DirtyCheckable) { + DirtyCheckable dc = (DirtyCheckable) obj + hasPreExistingDirty = dc.hasChanged() || !dc.listDirtyPropertyNames().isEmpty() + for (PersistentProperty prop : entity.persistentProperties) { + beforeUpdateSnapshot[prop.name] = entityAccess.getProperty(prop.name) } } - }) - } else { - mongoCodecSession.addPendingUpdate(new PendingUpdateAdapter(entity, id, obj, entityAccess) { - @Override - void run() { - // Take snapshot of all property values BEFORE the PreUpdate event fires - Map<String, Object> beforeUpdateSnapshot = [:] + if (!cancelUpdate(entity, entityAccess)) { + // Compare with snapshot and mark modified properties dirty if (obj instanceof DirtyCheckable) { + DirtyCheckable dirtyCheckable = (DirtyCheckable) obj + boolean hasNonAutoTimestampChange = hasPreExistingDirty + List<String> onlyAutoTimestampChanged = [] for (PersistentProperty prop : entity.persistentProperties) { - beforeUpdateSnapshot[prop.name] = entityAccess.getProperty(prop.name) - } - } - if (!cancelUpdate(entity, entityAccess)) { - // Compare with snapshot and mark modified properties dirty - if (obj instanceof DirtyCheckable) { - DirtyCheckable dirtyCheckable = (DirtyCheckable) obj - for (PersistentProperty prop : entity.persistentProperties) { - Object oldValue = beforeUpdateSnapshot[prop.name] - Object newValue = entityAccess.getProperty(prop.name) - boolean valueChanged = oldValue != newValue && (oldValue == null || !oldValue.equals(newValue)) - if (valueChanged) { - dirtyCheckable.markDirty(prop.name, newValue, oldValue) + Object oldValue = beforeUpdateSnapshot[prop.name] + Object newValue = entityAccess.getProperty(prop.name) + boolean valueChanged = oldValue != newValue && (oldValue == null || !oldValue.equals(newValue)) + if (valueChanged) { + dirtyCheckable.markDirty(prop.name, newValue, oldValue) + if (!hasPreExistingDirty) { + String propName = prop.name + if (propName == GormProperties.LAST_UPDATED || propName == GormProperties.DATE_CREATED) { + onlyAutoTimestampChanged.add(propName) + } else { + hasNonAutoTimestampChange = true + } } } } - updateCaches(entity, obj, id) - addCascadeOperation(new PendingOperationAdapter(entity, id, obj) { - @Override - void run() { - firePostUpdateEvent(entity, entityAccess) + if (!hasNonAutoTimestampChange && !onlyAutoTimestampChanged.isEmpty()) { + // AutoTimestampEventListener set timestamp properties but nothing else changed. + // Treat as a no-op save: reset the timestamps and veto the update. + for (String propName in onlyAutoTimestampChanged) { + entityAccess.setProperty(propName, beforeUpdateSnapshot[propName]) } - }) - } else { - setVetoed(true) + dirtyCheckable.trackChanges() + setVetoed(true) + return + } } + updateCaches(entity, obj, id) + addCascadeOperation(new PendingOperationAdapter(entity, id, obj) { + @Override + void run() { + firePostUpdateEvent(entity, entityAccess) + } + }) + } else { + setVetoed(true) } - }) - } + } + }) } return id } diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy index df651ad751..85e5fb0d07 100644 --- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy @@ -47,7 +47,7 @@ import org.grails.datastore.bson.codecs.decoders.EmbeddedDecoder import org.grails.datastore.bson.codecs.encoders.EmbeddedCollectionEncoder import org.grails.datastore.bson.codecs.encoders.EmbeddedEncoder import org.grails.datastore.bson.codecs.encoders.IdentityEncoder -import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormRegistry import org.grails.datastore.gorm.schemaless.DynamicAttributes import org.grails.datastore.mapping.collection.PersistentList import org.grails.datastore.mapping.collection.PersistentSet @@ -59,12 +59,12 @@ import org.grails.datastore.mapping.core.Session import org.grails.datastore.mapping.dirty.checking.DirtyCheckable import org.grails.datastore.mapping.dirty.checking.DirtyCheckableCollection import org.grails.datastore.mapping.engine.EntityAccess -import org.grails.datastore.mapping.engine.EntityPersister import org.grails.datastore.mapping.engine.internal.MappingUtils import org.grails.datastore.mapping.model.EmbeddedPersistentEntity 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.model.types.Basic import org.grails.datastore.mapping.model.types.Embedded import org.grails.datastore.mapping.model.types.EmbeddedCollection import org.grails.datastore.mapping.model.types.Identity @@ -161,7 +161,7 @@ class PersistentEntityCodec extends BsonPersistentEntityCodec { callback(AbstractDatastore.retrieveSession(MongoDatastore)) } else { - GormEnhancer.findStaticApi(entity.javaClass).withSession(callback) + GormRegistry.instance.findStaticApi(entity.javaClass).withSession(callback) } } @@ -175,10 +175,10 @@ class PersistentEntityCodec extends BsonPersistentEntityCodec { return cachedInstance } if (entity instanceof EmbeddedPersistentEntity) { - callback(AbstractDatastore.retrieveSession(MongoDatastore)) + return callback(AbstractDatastore.retrieveSession(MongoDatastore)) } else { - GormEnhancer.findStaticApi(entity.javaClass).withSession(callback) + return GormRegistry.instance.findStaticApi(entity.javaClass).withSession(callback) } } @@ -234,6 +234,19 @@ class PersistentEntityCodec extends BsonPersistentEntityCodec { def dirtyProperties = new ArrayList<String>(dirty.listDirtyPropertyNames()) boolean isNew = dirtyProperties.isEmpty() && dirty.hasChanged() + if (!isNew && dirtyProperties.isEmpty()) { + // Preserve historical Mongo behavior for basic collection properties: + // a save on an entity with wrapped basic collections is treated as an update. + for (PersistentProperty prop : entity.persistentProperties) { + if (prop instanceof Basic) { + Object basicValue = access.getProperty(prop.name) + if (basicValue instanceof DirtyCheckableCollection && ((DirtyCheckableCollection)basicValue).hasChanged()) { + isNew = true + break + } + } + } + } def isVersioned = entity.isVersioned() if (isNew) { // if it is new it can only be an embedded entity that has now been updated @@ -242,14 +255,10 @@ class PersistentEntityCodec extends BsonPersistentEntityCodec { if (!entity.isRoot()) { sets.put(MongoConstants.MONGO_CLASS_FIELD, new BsonString(entity.discriminator)) } - - if (isVersioned) { - EntityPersister.incrementEntityVersion(access) - } - } for (propertyName in dirtyProperties) { + if (isVersioned && propertyName == entity.version.name) continue def prop = entity.getPropertyByName(propertyName) if (prop != null) { @@ -291,7 +300,7 @@ class PersistentEntityCodec extends BsonPersistentEntityCodec { } else { - GormEnhancer.findStaticApi(entity.javaClass).withSession { Session mongoSession -> + GormRegistry.instance.findStaticApi(entity.javaClass).withSession { Session mongoSession -> if (mongoSession != null) { Document schemaless = (Document) mongoSession.getAttribute(value, SCHEMALESS_ATTRIBUTES) if (schemaless != null) { @@ -703,16 +712,39 @@ class PersistentEntityCodec extends BsonPersistentEntityCodec { } } + Class associatedType = associatedEntity.javaClass + if (associationId != null && associatedEntity.isRoot()) { + try { + Document raw = mongoSession.getCollection(associatedEntity) + .withDocumentClass(Document) + .find(new Document(MongoConstants.MONGO_ID_FIELD, associationId), Document) + .limit(1) + .first() + if (raw != null) { + Object discriminator = raw.get(MongoConstants.MONGO_CLASS_FIELD) + if (discriminator != null) { + PersistentEntity childEntity = associatedEntity.mappingContext + .getChildEntityByDiscriminator(associatedEntity.rootEntity, discriminator.toString()) + if (childEntity != null) { + associatedType = childEntity.javaClass + } + } + } + } catch (Exception ignored) { + // fall back to the declared association type + } + } + if (isLazy) { entityAccess.setPropertyNoConversion( property.name, - mongoSession.proxy(associatedEntity.javaClass, associationId) + mongoSession.proxy(associatedType, associationId) ) } else { entityAccess.setPropertyNoConversion( property.name, - mongoSession.retrieve(associatedEntity.javaClass, associationId) + mongoSession.retrieve(associatedType, associationId) ) } diff --git a/grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy b/grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy index 0737fcb83a..ec0c73a2fc 100644 --- a/grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy +++ b/grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy @@ -25,10 +25,11 @@ import grails.core.GrailsApplication import grails.gorm.validation.PersistentEntityValidator import groovy.util.logging.Slf4j import org.apache.grails.data.testing.tck.base.GrailsDataTckManager +import spock.lang.Specification import org.apache.grails.testing.mongo.AbstractMongoGrailsExtension import org.bson.Document import org.grails.datastore.bson.query.BsonQuery -import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormRegistry import org.grails.datastore.gorm.mongo.Birthday import org.grails.datastore.gorm.validation.constraints.eval.DefaultConstraintEvaluator import org.grails.datastore.gorm.validation.constraints.registry.DefaultConstraintRegistry @@ -64,6 +65,13 @@ class GrailsDataMongoTckManager extends GrailsDataTckManager { MongoDatastore multiDataSourceDatastore MongoDatastore multiTenantMultiDataSourceDatastore + @Override + void setup(Class<? extends Specification> spec) { + cleanRegistry() + GormRegistry.reset() + super.setup(spec) + } + @Override void setupSpec() { super.setupSpec() @@ -75,7 +83,9 @@ class GrailsDataMongoTckManager extends GrailsDataTckManager { (MongoSettings.SETTING_DATABASE_NAME): 'test', (MongoSettings.SETTING_HOST) : mongoDBContainer.host, (MongoSettings.SETTING_PORT) : mongoDBContainer.getMappedPort(AbstractMongoGrailsExtension.DEFAULT_MONGO_PORT) as String, - //TODO: 'grails.mongodb.url': "mongodb://${host}:${port as String}/myDb" as String + 'grails.mongodb.connections': [ + 'secondary': ['url': "mongodb://${mongoDBContainer.host}:${mongoDBContainer.getMappedPort(AbstractMongoGrailsExtension.DEFAULT_MONGO_PORT)}/tckSecondaryDB" as String] + ] ] } @@ -147,7 +157,7 @@ class GrailsDataMongoTckManager extends GrailsDataTckManager { } } for (cls in domainClasses) { - GormEnhancer.findValidationApi(cls).validator = null + GormRegistry.instance.findValidationApi(cls).validator = null } } finally { diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DirtyCheckUpdateSpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DirtyCheckUpdateSpec.groovy index 5dae9666d0..f5143a0403 100644 --- a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DirtyCheckUpdateSpec.groovy +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/DirtyCheckUpdateSpec.groovy @@ -62,7 +62,7 @@ class DirtyCheckUpdateSpec extends GrailsDataTckSpec<GrailsDataMongoTckManager> b = Bar.get(b.id) then: - b.version == 3 //should be 2 + b.version == 1 } void "Test that the version is incremented on save"() { diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/GormRegistryScalabilitySpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/GormRegistryScalabilitySpec.groovy new file mode 100644 index 0000000000..d3c8fe81d2 --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/GormRegistryScalabilitySpec.groovy @@ -0,0 +1,223 @@ +/* + * 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.mongo + +import grails.gorm.MultiTenant +import grails.gorm.annotation.Entity +import org.grails.datastore.gorm.GormRegistry +import org.grails.datastore.gorm.GormStaticApi +import org.grails.datastore.gorm.GormInstanceApi +import org.grails.datastore.gorm.GormValidationApi +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.datastore.mapping.core.connections.ConnectionSource +import org.grails.datastore.mapping.multitenancy.AllTenantsResolver +import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver +import org.grails.datastore.mapping.mongo.MongoDatastore +import spock.lang.Shared +import spock.lang.Specification + +/** + * Verifies the O(M+N) memory guarantee of {@link GormRegistry} in the MongoDB + * context. + * + * The registry must satisfy: + * - O(M) static/instance/validation API maps — one entry per entity class, never per tenant + * - O(N) datastoresByQualifier map — one entry per tenant/qualifier + * - O(1) API retrieval for any qualifier — same singleton instance returned + * + * where M = number of entity classes, N = number of tenants/connections. + */ +class GormRegistryScalabilitySpec extends Specification { + + static final int TENANT_COUNT = 5 + + @Shared MongoDatastore datastore + + void setupSpec() { + System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, "") + Map config = [ + "grails.mongodb.multiTenancy.mode" : "DATABASE", + "grails.mongodb.multiTenancy.tenantResolverClass": ScalabilityTenantsResolver, + "grails.mongodb.databaseName": "scalabilityDB" + ] + datastore = new MongoDatastore( + DatastoreUtils.createPropertyResolver(config), + ScalabilityBook, ScalabilityAuthor + ) + } + + void cleanupSpec() { + datastore?.close() + System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME) + } + + // ------------------------------------------------------------------------- + // O(M) — API maps must have exactly one entry per entity class, not per tenant + // ------------------------------------------------------------------------- + + void "GormRegistry staticApis map size equals number of entity classes (O(M))"() { + given: + GormRegistry registry = GormRegistry.instance + + expect: "one static API entry per entity — never multiplied by tenant count" + registry.staticApiRegistry.containsKey(ScalabilityBook.name) + registry.staticApiRegistry.containsKey(ScalabilityAuthor.name) + + and: "our two entities contribute exactly 2 keys (not 2 × tenant count)" + registry.staticApiRegistry.keySet().count { it == ScalabilityBook.name || it == ScalabilityAuthor.name } == 2 + } + + void "GormRegistry instanceApis map size equals number of entity classes (O(M))"() { + given: + GormRegistry registry = GormRegistry.instance + + expect: + registry.instanceApiRegistry.containsKey(ScalabilityBook.name) + registry.instanceApiRegistry.containsKey(ScalabilityAuthor.name) + + and: "our two entities contribute exactly 2 keys (not 2 × tenant count)" + registry.instanceApiRegistry.keySet().count { it == ScalabilityBook.name || it == ScalabilityAuthor.name } == 2 + } + + void "GormRegistry validationApis map size equals number of entity classes (O(M))"() { + given: + GormRegistry registry = GormRegistry.instance + + expect: + registry.validationApiRegistry.containsKey(ScalabilityBook.name) + registry.validationApiRegistry.containsKey(ScalabilityAuthor.name) + + and: "our two entities contribute exactly 2 keys (not 2 × tenant count)" + registry.validationApiRegistry.keySet().count { it == ScalabilityBook.name || it == ScalabilityAuthor.name } == 2 + } + + // ------------------------------------------------------------------------- + // O(1) — same API singleton returned regardless of qualifier + // ------------------------------------------------------------------------- + + void "getStaticApi returns the same singleton instance for any qualifier (O(1) retrieval)"() { + given: + GormRegistry registry = GormRegistry.instance + GormStaticApi defaultApi = registry.getStaticApi(ScalabilityBook.name) + + expect: "default qualifier retrieves the canonical singleton" + defaultApi != null + + and: "retrieval remains O(1) and returns the same singleton regardless of tenant loop context" + ScalabilityTenantsResolver.TENANTS.every { tenantId -> + registry.getStaticApi(ScalabilityBook.name).is(defaultApi) + } + } + + void "getInstanceApi returns the same singleton instance for any qualifier (O(1) retrieval)"() { + given: + GormRegistry registry = GormRegistry.instance + GormInstanceApi defaultApi = registry.getInstanceApi(ScalabilityAuthor.name) + + expect: + defaultApi != null + ScalabilityTenantsResolver.TENANTS.every { tenantId -> + registry.getInstanceApi(ScalabilityAuthor.name).is(defaultApi) + } + } + + // ------------------------------------------------------------------------- + // O(N) — qualifier map must grow with tenants (datastoresByQualifier) + // ------------------------------------------------------------------------- + + void "datastoresByQualifier contains all registered tenants (O(N) qualifier map)"() { + given: + GormRegistry registry = GormRegistry.instance + + expect: "at minimum, the default qualifier is registered" + registry.datastoresByQualifier.containsKey(ConnectionSource.DEFAULT) + + and: "the qualifier map has at least one entry (the parent datastore)" + registry.datastoresByQualifier.size() >= 1 + } + + // ------------------------------------------------------------------------- + // No spurious entries — unknown qualifiers must not pollute the registry + // ------------------------------------------------------------------------- + + void "looking up an unknown qualifier does not create a spurious registry entry"() { + given: + GormRegistry registry = GormRegistry.instance + String ghost = "ghost_tenant_" + System.currentTimeMillis() + int sizeBefore = registry.datastoresByQualifier.size() + + when: + def result = registry.getDatastore(ScalabilityBook.name, ghost) + + then: "nothing is found" + result == null + + and: "the map size is unchanged — no null/empty entry was inserted" + registry.datastoresByQualifier.size() == sizeBefore + } + + void "datastore deregisters from GormRegistry on close"() { + given: "a temporary datastore registered in GormRegistry" + def tempDatastore = new MongoDatastore( + DatastoreUtils.createPropertyResolver([ + "grails.mongodb.databaseName": "tempScalabilityDB" + ]), + ScalabilityBook + ) + GormRegistry registry = GormRegistry.instance + + expect: "the datastore is registered" + registry.allDatastores.contains(tempDatastore) + + when: "the datastore is closed" + tempDatastore.close() + + then: "the datastore is removed from the GormRegistry" + !registry.allDatastores.contains(tempDatastore) + } +} + +// --------------------------------------------------------------------------- +// Test fixtures +// --------------------------------------------------------------------------- + +class ScalabilityTenantsResolver implements AllTenantsResolver { + static final List<String> TENANTS = ["dbA", "dbB", "dbC", "dbD", "dbE"] + + @Override + Serializable resolveTenantIdentifier() { + TENANTS[0] + } + + @Override + Iterable<Serializable> resolveTenantIds() { + TENANTS + } +} + +@Entity +class ScalabilityBook implements MultiTenant<ScalabilityBook> { + String title + String author +} + +@Entity +class ScalabilityAuthor implements MultiTenant<ScalabilityAuthor> { + String name +} diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/IsNullSpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/IsNullSpec.groovy index e6f56b7f6f..1f718d8188 100644 --- a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/IsNullSpec.groovy +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/IsNullSpec.groovy @@ -32,7 +32,7 @@ class IsNullSpec extends GrailsDataTckSpec<GrailsDataMongoTckManager> { @Issue('GPMONGODB-164') void "Test isNull works in a criteria query"() { given: "Some test data" - new Elephant(name: "Dumbo").save(validate: false) + new Elephant(name: "Dumbo").save(flush: true, validate: false) new Elephant(name: "Big Daddy", trunk: new Trunk(length: 10).save()).save(flush: true, validate: false) manager.session.clear() @@ -58,7 +58,7 @@ class IsNullSpec extends GrailsDataTckSpec<GrailsDataMongoTckManager> { @Issue('GPMONGODB-164') void "Test isNull works in a dynamic finder"() { given: "Some test data" - new Elephant(name: "Dumbo").save(validate: false) + new Elephant(name: "Dumbo").save(flush: true, validate: false) new Elephant(name: "Big Daddy", trunk: new Trunk(length: 10).save()).save(flush: true, validate: false) manager.session.clear() diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactorySpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactorySpec.groovy new file mode 100644 index 0000000000..a3c4df4646 --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormApiFactorySpec.groovy @@ -0,0 +1,117 @@ +/* + * 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.mongo + +import org.grails.datastore.gorm.DatastoreResolver +import org.grails.datastore.gorm.GormRegistry +import org.grails.datastore.gorm.mongo.api.MongoStaticApi +import org.grails.datastore.mapping.model.MappingContext +import spock.lang.Specification + +/** + * Tests for MongoGormApiFactory + */ +class MongoGormApiFactorySpec extends Specification { + + void 'createStaticApi returns MongoStaticApi instance'() { + given: + MongoGormApiFactory factory = new MongoGormApiFactory() + MappingContext mappingContext = Mock(MappingContext) + DatastoreResolver resolver = Stub(DatastoreResolver) + String qualifier = 'default' + + when: + def staticApi = factory.createStaticApi( + TestEntity, + mappingContext, + resolver, + qualifier, + GormRegistry.instance + ) + + then: + staticApi != null + staticApi instanceof MongoStaticApi + staticApi.persistentClass == TestEntity + } + + void 'createStaticApi creates finders'() { + given: + MongoGormApiFactory factory = new MongoGormApiFactory() + MappingContext mappingContext = Mock(MappingContext) + DatastoreResolver resolver = Stub(DatastoreResolver) + + when: + def staticApi = factory.createStaticApi( + TestEntity, + mappingContext, + resolver, + 'default', + GormRegistry.instance + ) + + then: + staticApi.finders.size() > 0 + } + + void 'createInstanceApi uses parent factory behavior'() { + given: + MongoGormApiFactory factory = new MongoGormApiFactory() + MappingContext mappingContext = Mock(MappingContext) + DatastoreResolver resolver = Stub(DatastoreResolver) + + when: + def instanceApi = factory.createInstanceApi( + TestEntity, + mappingContext, + resolver, + GormRegistry.instance, + true, + false + ) + + then: + instanceApi != null + instanceApi.failOnError + !instanceApi.markDirty + } + + void 'createValidationApi uses parent factory behavior'() { + given: + MongoGormApiFactory factory = new MongoGormApiFactory() + MappingContext mappingContext = Mock(MappingContext) + DatastoreResolver resolver = Stub(DatastoreResolver) + + when: + def validationApi = factory.createValidationApi( + TestEntity, + mappingContext, + resolver, + GormRegistry.instance + ) + + then: + validationApi != null + } + + static class TestEntity { + String name + Integer age + } +} diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormInstanceApiSpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormInstanceApiSpec.groovy new file mode 100644 index 0000000000..1d072d884b --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/MongoGormInstanceApiSpec.groovy @@ -0,0 +1,101 @@ +/* + * 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.mongo + +import groovy.transform.CompileDynamic +import org.grails.datastore.gorm.mongo.api.MongoGormInstanceApi +import org.grails.datastore.gorm.mongo.transactions.MongoTransactionContext +import org.grails.datastore.mapping.mongo.MongoDatastore +import org.grails.datastore.mapping.mongo.config.MongoMappingContext +import spock.lang.Specification + +/** + * Specification for MongoGormInstanceApi + * + * @author Graeme Rocher + * @since 8.0 + */ +@CompileDynamic +class MongoGormInstanceApiSpec extends Specification { + private MongoDatastore datastore + + void "auto-flush gate defaults to enabled outside rollback-aware context"() { + given: + def api = newApi() + + expect: + api.exposedShouldAutoFlushByDefault() + } + + void "auto-flush gate is disabled inside rollback-aware context only"() { + given: + def api = newApi() + + expect: + api.exposedShouldAutoFlushByDefault() + + when: + def insideGate = MongoTransactionContext.withRollbackAware { + api.exposedShouldAutoFlushByDefault() + } + + then: + !insideGate + api.exposedShouldAutoFlushByDefault() + } + + void "auto-flush gate handles nested rollback-aware contexts and restores state"() { + given: + def api = newApi() + + when: + def outer = MongoTransactionContext.withRollbackAware { + def inner = MongoTransactionContext.withRollbackAware { + api.exposedShouldAutoFlushByDefault() + } + [api.exposedShouldAutoFlushByDefault(), inner] + } + + then: + !outer[0] + !outer[1] + api.exposedShouldAutoFlushByDefault() + } + + private TestableMongoGormInstanceApi newApi() { + datastore = new MongoDatastore(new MongoMappingContext('GateEntity')) + new TestableMongoGormInstanceApi(datastore) + } + + void cleanup() { + datastore?.close() + } + + @CompileDynamic + private static class TestableMongoGormInstanceApi extends MongoGormInstanceApi<Object> { + TestableMongoGormInstanceApi(MongoDatastore datastore) { + super(Object, datastore) + } + + boolean exposedShouldAutoFlushByDefault() { + shouldAutoFlushByDefault() + } + } +} diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlacePartialTest.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlacePartialTest.groovy new file mode 100644 index 0000000000..99d3a64ecd --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlacePartialTest.groovy @@ -0,0 +1,72 @@ +/* + * 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.mongo + +import org.apache.grails.data.mongo.core.GrailsDataMongoTckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import grails.mongodb.geo.* +import grails.persistence.Entity + +@Entity +class PlacePartial { + Long id + String name + Point point + Polygon polygon + LineString lineString + Box box + Circle circle + Sphere sphere + MultiPoint multiPoint + MultiLineString multiLineString + MultiPolygon multiPolygon + GeometryCollection geometryCollection + + static mapping = { + point geoIndex: '2dsphere' + } +} + +class PlacePartialTest extends GrailsDataTckSpec<GrailsDataMongoTckManager> { + void setupSpec() { + manager.registerDomainClasses(PlacePartial) + } + + void "test place with only one field"() { + when: + def col = new GeometryCollection() + col << Point.valueOf(5, 10) + def p = new PlacePartial(geometryCollection: col) + println "Saving with only geometryCollection set..." + p.save(flush: true, validate: false) + println "Saved with id: ${p.id}" + manager.session.clear() + + then: + p.id != null + + when: + println "Retrieving..." + p = PlacePartial.get(p.id) + println "Retrieved: ${p}" + + then: + p != null + } +} diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithExceptionTest.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithExceptionTest.groovy new file mode 100644 index 0000000000..c95fc4f6e2 --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithExceptionTest.groovy @@ -0,0 +1,69 @@ +/* + * 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.mongo + +import org.apache.grails.data.mongo.core.GrailsDataMongoTckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import grails.mongodb.geo.* +import grails.persistence.Entity + +@Entity +class PlaceException { + Long id + String name + Point point + Polygon polygon + + static mapping = { + point geoIndex: '2dsphere' + } +} + +class PlaceWithExceptionTest extends GrailsDataTckSpec<GrailsDataMongoTckManager> { + void setupSpec() { + manager.registerDomainClasses(PlaceException) + } + + void "test place with exception handling"() { + when: + def col = new GeometryCollection() + col << Point.valueOf(5, 10) + def p = new PlaceException(name: "Test") // Don't set any GeoJSON fields + p.save(flush: true, validate: false) + println "Saved with id: ${p.id}" + manager.session.clear() + + then: + p.id != null + + when: + try { + p = PlaceException.get(p.id) + println "Retrieved successfully: ${p}" + } catch (Exception e) { + println "Exception during get: ${e.class.name}" + println "Message: ${e.message}" + e.printStackTrace(System.out) + throw e + } + + then: + p != null + } +} diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithoutSphereTest.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithoutSphereTest.groovy new file mode 100644 index 0000000000..cb255a892b --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/PlaceWithoutSphereTest.groovy @@ -0,0 +1,69 @@ +/* + * 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.mongo + +import org.apache.grails.data.mongo.core.GrailsDataMongoTckManager +import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec +import grails.mongodb.geo.* +import grails.persistence.Entity + +@Entity +class PlaceNS { + Long id + String name + Point point + Polygon polygon + LineString lineString + Box box + Circle circle + // NO Sphere! + MultiPoint multiPoint + MultiLineString multiLineString + MultiPolygon multiPolygon + GeometryCollection geometryCollection + + static mapping = { + point geoIndex: '2dsphere' + } +} + +class PlaceWithoutSphereTest extends GrailsDataTckSpec<GrailsDataMongoTckManager> { + void setupSpec() { + manager.registerDomainClasses(PlaceNS) + } + + void "test place without sphere"() { + given: + def point = new Point(5, 10) + def poly = Polygon.valueOf([[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]) + def line = LineString.valueOf([[100.0, 0.0], [101.0, 1.0]]) + def box = Box.valueOf([[0, 0], [10, 10]]) + def circle = Circle.valueOf([[5, 5], 3]) + + when: + def p = new PlaceNS(point: point, polygon: poly, lineString: line, box: box, circle: circle) + p.save(flush: true, validate: false) + manager.session.clear() + p = PlaceNS.get(p.id) + + then: + p != null + p.point == point + } +} diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/SimpleHasManySpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/SimpleHasManySpec.groovy index e47a9d0f68..90a6319718 100644 --- a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/SimpleHasManySpec.groovy +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/SimpleHasManySpec.groovy @@ -107,4 +107,3 @@ class Chapter implements Serializable { String title } - diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/api/MongoTenantContextProfilingSpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/api/MongoTenantContextProfilingSpec.groovy new file mode 100644 index 0000000000..210b623945 --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/api/MongoTenantContextProfilingSpec.groovy @@ -0,0 +1,151 @@ +/* + * 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.mongo.api + +import grails.gorm.MultiTenant +import grails.gorm.multitenancy.Tenants +import org.grails.datastore.gorm.GormRegistry +import org.grails.datastore.gorm.DatastoreResolver +import org.grails.datastore.gorm.multitenancy.TenantDelegatingGormOperations +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.multitenancy.MultiTenantCapableDatastore +import org.grails.datastore.mapping.multitenancy.MultiTenancySettings +import org.grails.datastore.mapping.model.MappingContext +import org.bson.Document +import spock.lang.Specification + +class MongoTenantContextProfilingSpec extends Specification { + + void setup() { + GormRegistry.instance.reset() + } + + void cleanup() { + GormRegistry.instance.reset() + } + + void "profile mongo tenant wrapping overhead"() { + given: + def mappingContext = Stub(MappingContext) + def datastore = Stub(MultiTenantCapableDatastore) { + getMultiTenancyMode() >> MultiTenancySettings.MultiTenancyMode.DATABASE + getMappingContext() >> mappingContext + getDatastoreForTenantId(_) >> { return it[0] == null ? delegate : delegate } + } + + def registry = GormRegistry.instance + registry.registerDatastore("default", datastore) + + def persistentEntity = Stub(org.grails.datastore.mapping.model.PersistentEntity) { + isMultiTenant() >> true + getTenantId() >> Stub(org.grails.datastore.mapping.model.PersistentProperty) { + getName() >> "tenantId" + } + } + + def staticApi = new DummyMongoStaticApi(TenantEntity, mappingContext, datastore, persistentEntity) + def ops = new TenantDelegatingGormOperations<TenantEntity>((Datastore) datastore, "tenant1", staticApi) + def qualifiedApi = staticApi.forQualifier("tenant1") + + int iterations = 1000 + + when: "Calling operations repeatedly via TenantDelegatingGormOperations (wrapped every time)" + long startWrapped = System.currentTimeMillis() + for (int i = 0; i < iterations; i++) { + ops.exists(1L) + } + long endWrapped = System.currentTimeMillis() + + and: "Calling operations via qualified API (unwrapped, but pre-bound)" + long startQualified = System.currentTimeMillis() + for (int i = 0; i < iterations; i++) { + qualifiedApi.exists(1L) + } + long endQualified = System.currentTimeMillis() + + and: "Calling operations via closure block (wrapped once)" + long startBlock = System.currentTimeMillis() + Tenants.withId((MultiTenantCapableDatastore) datastore, "tenant1") { + for (int i = 0; i < iterations; i++) { + staticApi.exists(1L) + } + } + long endBlock = System.currentTimeMillis() + + and: "Calling internal wrapping logic directly (wrapped vs pre-bound)" + def filter = new Document() + long startInternalWrapped = System.currentTimeMillis() + Tenants.withId((MultiTenantCapableDatastore) datastore, "tenant1") { + for (int i = 0; i < iterations; i++) { + staticApi.wrapFilterWithMultiTenancy(filter) + } + } + long endInternalWrapped = System.currentTimeMillis() + + long startInternalPrebound = System.currentTimeMillis() + for (int i = 0; i < iterations; i++) { + qualifiedApi.wrapFilterWithMultiTenancy(filter) + } + long endInternalPrebound = System.currentTimeMillis() + + then: + println "Mongo Single block wrapped operations: ${endBlock - startBlock} ms" + println "Mongo Qualified API operations: ${endQualified - startQualified} ms" + println "Mongo Per-method wrapped operations: ${endWrapped - startWrapped} ms" + println "Mongo Internal wrapFilter (wrapped): ${endInternalWrapped - startInternalWrapped} ms" + println "Mongo Internal wrapFilter (pre-bound): ${endInternalPrebound - startInternalPrebound} ms" + + true + } + + static class TenantEntity implements MultiTenant<TenantEntity> { + Long id + } + + static class DummyMongoStaticApi extends MongoStaticApi<TenantEntity> { + private final org.grails.datastore.mapping.model.PersistentEntity persistentEntityStub + + DummyMongoStaticApi(Class<TenantEntity> persistentClass, MappingContext mappingContext, MultiTenantCapableDatastore datastore, org.grails.datastore.mapping.model.PersistentEntity persistentEntityStub, String qualifier = "default") { + super(persistentClass, mappingContext, [], new org.grails.datastore.gorm.DatastoreResolver() { + @Override org.grails.datastore.mapping.core.Datastore resolve() { return (Datastore) datastore } + }, qualifier) + this.persistentEntityStub = persistentEntityStub + } + + @Override + boolean exists(Serializable id) { + return true + } + + @Override + org.grails.datastore.gorm.GormStaticApi<TenantEntity> forQualifier(String qualifier) { + return new DummyMongoStaticApi(persistentClass, mappingContext, (MultiTenantCapableDatastore)datastore, persistentEntityStub, qualifier) + } + + @Override + public org.bson.conversions.Bson wrapFilterWithMultiTenancy(org.bson.conversions.Bson filter) { + return super.wrapFilterWithMultiTenancy(filter) + } + + @Override + org.grails.datastore.mapping.model.PersistentEntity getGormPersistentEntity() { + return persistentEntityStub + } + } +} diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy index 429c575374..6525099533 100644 --- a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/MultiTenancySpec.groovy @@ -41,16 +41,15 @@ import static com.mongodb.client.model.Filters.* @RestoreSystemProperties class MultiTenancySpec extends AutoStartedMongoSpec { - @AutoCleanup MongoDatastore datastore + @Shared @AutoCleanup MongoDatastore datastore @Override boolean shouldInitializeDatastore() { false } - void setup() { - // Ensure tenant property is cleared before each test for test isolation - System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME) + void setupSpec() { + org.grails.datastore.gorm.GormRegistry.reset() Map config = [ "grails.gorm.multiTenancy.mode" :"DISCRIMINATOR", "grails.gorm.multiTenancy.tenantResolverClass": MyResolver, @@ -59,6 +58,11 @@ class MultiTenancySpec extends AutoStartedMongoSpec { this.datastore = new MongoDatastore(config, getDomainClasses() as Class[]) } + void setup() { + // Ensure tenant property is cleared before each test for test isolation + System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME) + } + void "Test persist and retrieve entities with multi tenancy"() { setup: diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy index 406b967d7d..6420f24d85 100644 --- a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/connections/SchemaBasedMultiTenancySpec.groovy @@ -35,16 +35,15 @@ import spock.lang.Shared @RestoreSystemProperties class SchemaBasedMultiTenancySpec extends AutoStartedMongoSpec { - @AutoCleanup MongoDatastore datastore + @Shared @AutoCleanup MongoDatastore datastore @Override boolean shouldInitializeDatastore() { false } - void setup() { - // Ensure tenant property is cleared before each test for test isolation - System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME) + void setupSpec() { + org.grails.datastore.gorm.GormRegistry.reset() Map config = [ (MongoSettings.SETTING_URL): "mongodb://${mongoHost}:${mongoPort}/defaultDb" as String, "grails.gorm.multiTenancy.mode" :"SCHEMA", @@ -53,6 +52,11 @@ class SchemaBasedMultiTenancySpec extends AutoStartedMongoSpec { this.datastore = new MongoDatastore(config, getDomainClasses() as Class[]) } + void setup() { + // Ensure tenant property is cleared before each test for test isolation + System.clearProperty(SystemPropertyTenantResolver.PROPERTY_NAME) + } + void "Test no tenant id"() { when: CompanyB.DB diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplateSpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplateSpec.groovy new file mode 100644 index 0000000000..e898671362 --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoGormTransactionTemplateSpec.groovy @@ -0,0 +1,116 @@ +/* + * 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.mongo.transactions + +import org.grails.datastore.mapping.mongo.MongoDatastore +import org.grails.datastore.mapping.mongo.config.MongoMappingContext +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.interceptor.DefaultTransactionAttribute +import spock.lang.Specification + +/** + * Specification for MongoGormTransactionTemplate + */ +class MongoGormTransactionTemplateSpec extends Specification { + void "MongoTransactionContext enables and restores rollback-aware marker"() { + expect: + !MongoTransactionContext.isRollbackAwareActive() + + when: + def inside = MongoTransactionContext.withRollbackAware { + MongoTransactionContext.isRollbackAwareActive() + } + + then: + inside + !MongoTransactionContext.isRollbackAwareActive() + } + + void "MongoTransactionContext supports nested scopes"() { + when: + def result = MongoTransactionContext.withRollbackAware { + def nested = MongoTransactionContext.withRollbackAware { + MongoTransactionContext.isRollbackAwareActive() + } + [MongoTransactionContext.isRollbackAwareActive(), nested] + } + + then: + result[0] + result[1] + !MongoTransactionContext.isRollbackAwareActive() + } + + void "MongoGormTransactionTemplate can be instantiated with TransactionManager"() { + given: "a mock datastore and transaction manager" + def datastore = new MongoDatastore(new MongoMappingContext('TxEntity')) + def mockTxManager = Mock(PlatformTransactionManager) + + when: "creating MongoGormTransactionTemplate" + def template = new MongoGormTransactionTemplate(datastore, mockTxManager) + + then: "instance is created successfully" + template != null + template instanceof MongoGormTransactionTemplate + + cleanup: + datastore.close() + } + + void "MongoGormTransactionTemplate can be instantiated with TransactionDefinition"() { + given: "mock objects" + def datastore = new MongoDatastore(new MongoMappingContext('TxEntity')) + def mockTxManager = Mock(PlatformTransactionManager) + def mockDefinition = Mock(TransactionDefinition) { + getIsolationLevel() >> TransactionDefinition.ISOLATION_DEFAULT + getPropagationBehavior() >> TransactionDefinition.PROPAGATION_REQUIRED + getTimeout() >> -1 + isReadOnly() >> false + } + + when: "creating MongoGormTransactionTemplate with definition" + def template = new MongoGormTransactionTemplate(datastore, mockTxManager, mockDefinition) + + then: "instance is created successfully" + template != null + template instanceof MongoGormTransactionTemplate + + cleanup: + datastore.close() + } + + void "MongoGormTransactionTemplate can be instantiated with TransactionAttribute"() { + given: "mock objects" + def datastore = new MongoDatastore(new MongoMappingContext('TxEntity')) + def mockTxManager = Mock(PlatformTransactionManager) + def attribute = new DefaultTransactionAttribute() + + when: "creating MongoGormTransactionTemplate with attribute" + def template = new MongoGormTransactionTemplate(datastore, mockTxManager, attribute) + + then: "instance is created successfully" + template != null + template instanceof MongoGormTransactionTemplate + + cleanup: + datastore.close() + } +} diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactorySpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactorySpec.groovy new file mode 100644 index 0000000000..daf7497a67 --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/transactions/MongoTransactionTemplateFactorySpec.groovy @@ -0,0 +1,112 @@ +/* + * 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.mongo.transactions + +import grails.gorm.transactions.GrailsTransactionTemplate +import org.grails.datastore.mapping.mongo.MongoDatastore +import org.grails.datastore.mapping.mongo.config.MongoMappingContext +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionDefinition +import org.springframework.transaction.interceptor.DefaultTransactionAttribute +import spock.lang.Specification + +/** + * Specification for MongoTransactionTemplateFactory + */ +class MongoTransactionTemplateFactorySpec extends Specification { + + void "MongoTransactionTemplateFactory creates MongoGormTransactionTemplate with default settings"() { + given: "a mock datastore and transaction manager" + def datastore = new MongoDatastore(new MongoMappingContext('TxEntity')) + def mockTxManager = Mock(PlatformTransactionManager) + def factory = new MongoTransactionTemplateFactory(datastore) + + when: "creating transaction template" + def template = factory.createTransactionTemplate(mockTxManager) + + then: "MongoGormTransactionTemplate is returned" + template != null + template instanceof MongoGormTransactionTemplate + template instanceof GrailsTransactionTemplate + + cleanup: + datastore.close() + } + + void "MongoTransactionTemplateFactory creates MongoGormTransactionTemplate with TransactionDefinition"() { + given: "mock objects" + def datastore = new MongoDatastore(new MongoMappingContext('TxEntity')) + def mockTxManager = Mock(PlatformTransactionManager) + def mockDefinition = Mock(TransactionDefinition) { + getIsolationLevel() >> TransactionDefinition.ISOLATION_DEFAULT + getPropagationBehavior() >> TransactionDefinition.PROPAGATION_REQUIRED + getTimeout() >> -1 + isReadOnly() >> false + } + def factory = new MongoTransactionTemplateFactory(datastore) + + when: "creating transaction template with definition" + def template = factory.createTransactionTemplate(mockTxManager, mockDefinition) + + then: "MongoGormTransactionTemplate is returned" + template != null + template instanceof MongoGormTransactionTemplate + + cleanup: + datastore.close() + } + + void "MongoTransactionTemplateFactory creates MongoGormTransactionTemplate with TransactionAttribute"() { + given: "mock objects" + def datastore = new MongoDatastore(new MongoMappingContext('TxEntity')) + def mockTxManager = Mock(PlatformTransactionManager) + def attribute = new DefaultTransactionAttribute() + def factory = new MongoTransactionTemplateFactory(datastore) + + when: "creating transaction template with attribute" + def template = factory.createTransactionTemplate(mockTxManager, attribute) + + then: "MongoGormTransactionTemplate is returned" + template != null + template instanceof MongoGormTransactionTemplate + + cleanup: + datastore.close() + } + + void "MongoTransactionTemplateFactory is consistent across calls"() { + given: "a factory and transaction manager" + def datastore = new MongoDatastore(new MongoMappingContext('TxEntity')) + def mockTxManager = Mock(PlatformTransactionManager) + def factory = new MongoTransactionTemplateFactory(datastore) + + when: "creating multiple templates" + def template1 = factory.createTransactionTemplate(mockTxManager) + def template2 = factory.createTransactionTemplate(mockTxManager) + + then: "both are MongoGormTransactionTemplate instances" + template1 instanceof MongoGormTransactionTemplate + template2 instanceof MongoGormTransactionTemplate + template1.class == template2.class + + cleanup: + datastore.close() + } +} diff --git a/grails-data-mongodb/docs/build.gradle b/grails-data-mongodb/docs/build.gradle index a4c2f4b59b..3696616249 100644 --- a/grails-data-mongodb/docs/build.gradle +++ b/grails-data-mongodb/docs/build.gradle @@ -51,7 +51,7 @@ tasks.register('resolveMongodbVersion').configure { Task docTask -> dependencies { documentation platform(project(':grails-bom')) documentation 'org.fusesource.jansi:jansi' - documentation 'jline:jline' + documentation 'jline:jline:2.14.6' documentation 'org.apache.groovy:groovy' documentation 'org.apache.groovy:groovy-ant' documentation 'org.apache.groovy:groovy-groovydoc' diff --git a/grails-data-mongodb/ext/src/main/groovy/org/grails/datastore/gorm/mongo/extensions/MongoExtensions.groovy b/grails-data-mongodb/ext/src/main/groovy/org/grails/datastore/gorm/mongo/extensions/MongoExtensions.groovy index c1b4b9e00c..8abea4748c 100644 --- a/grails-data-mongodb/ext/src/main/groovy/org/grails/datastore/gorm/mongo/extensions/MongoExtensions.groovy +++ b/grails-data-mongodb/ext/src/main/groovy/org/grails/datastore/gorm/mongo/extensions/MongoExtensions.groovy @@ -49,7 +49,7 @@ import org.bson.Document import org.bson.conversions.Bson import org.bson.types.ObjectId -import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormRegistry import org.grails.datastore.mapping.mongo.AbstractMongoSession import org.grails.datastore.mapping.mongo.MongoConstants import org.grails.datastore.mapping.mongo.engine.AbstractMongoObectEntityPersister @@ -74,7 +74,7 @@ class MongoExtensions { return (T) document } else { - def datastore = GormEnhancer.findDatastore(cls) + def datastore = GormRegistry.instance.apiResolver.findDatastore(cls) AbstractMongoSession session = (AbstractMongoSession) datastore.currentSession if (session != null) { return session.decode(cls, document) @@ -93,7 +93,7 @@ class MongoExtensions { return (T) iterable } else { - def datastore = GormEnhancer.findDatastore(cls) + def datastore = GormRegistry.instance.apiResolver.findDatastore(cls) AbstractMongoSession session = (AbstractMongoSession) datastore.currentSession if (session != null) { @@ -106,7 +106,7 @@ class MongoExtensions { } static <T> List<T> toList(FindIterable iterable, Class<T> cls) { - def datastore = GormEnhancer.findDatastore(cls) + def datastore = GormRegistry.instance.apiResolver.findDatastore(cls) AbstractMongoSession session = (AbstractMongoSession) datastore.currentSession MongoEntityPersister p = (MongoEntityPersister) session.getPersister(cls) @@ -620,4 +620,3 @@ class MongoExtensions { } } -
