You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/09/09 23:41:14 UTC
(grails-core) 03/14: Convert MongoQuery to Groovy
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch 9.0.x-java-to-groovy
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 8654cfcf1f299406efb7b0e1795a5e206b66804e
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Tue Sep 8 17:12:34 2026 -0500
Convert MongoQuery to Groovy
Sixth grails-data-mongodb batch: MongoQuery, with its static handler
registrations, the geo criterion classes, AggregatedResultList,
MongoResultList and AggregatePipeline.
Two runtime-relevant adjustments. In the unique-result path the Java code
cast the iterable to FindIterable<Document> and read first() into an
Object; that cast is erased in Java, but the Groovy static compiler turns
it into a real check against Document, and with the codec persister the
collection was re-typed to the entity class so first() returns an entity
(117 suite failures). The cast is raw there so first() stays Object. The
WithinBox/WithinPolygon/WithinCircle setValue(List) overloads assign the
inherited field as this.@value: property syntax would route the
assignment back into the setter.
Otherwise: BsonQuery's nested handler interfaces and Query's nested
criterion/projection classes are imported explicitly; the private static
helpers used from the anonymous handlers are @PackageScope; the "in"
parameter is renamed; AggregatedResultList's members used by its
anonymous iterator are @PackageScope; createAssociationMap uses Groovy's
withDefault directly instead of DefaultGroovyMethods with hand-built
Closure subclasses.
Verified: compileGroovy, codenarc and checkstyle clean; full core suite
identical to baseline (669 tests, 0 failures, 45 skipped).
Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5dsQNQNLuSdFph84RRQdr
---
.../mapping/mongo/query/MongoQuery.groovy | 1658 ++++++++++++++++++++
.../datastore/mapping/mongo/query/MongoQuery.java | 1633 -------------------
2 files changed, 1658 insertions(+), 1633 deletions(-)
diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.groovy
new file mode 100644
index 0000000000..19ec924b14
--- /dev/null
+++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.groovy
@@ -0,0 +1,1658 @@
+/* Copyright (C) 2010-2025 the original author or authors.
+ *
+ * Licensed 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.mapping.mongo.query
+
+import com.mongodb.ReadConcern
+import com.mongodb.client.AggregateIterable
+import com.mongodb.client.FindIterable
+import com.mongodb.client.MongoCursor
+import com.mongodb.client.MongoIterable
+import groovy.transform.CompileStatic
+import groovy.transform.PackageScope
+import org.bson.BsonDocument
+import org.bson.BsonDocumentReader
+import org.bson.BsonDocumentWriter
+import org.bson.Document
+import org.bson.codecs.configuration.CodecRegistry
+import org.springframework.dao.InvalidDataAccessResourceUsageException
+import org.springframework.transaction.support.TransactionSynchronizationManager
+
+import grails.mongodb.geo.Box
+import grails.mongodb.geo.Circle
+import grails.mongodb.geo.Distance
+import grails.mongodb.geo.GeoJSON
+import grails.mongodb.geo.Point
+import grails.mongodb.geo.Polygon
+import grails.mongodb.geo.Shape
+import grails.mongodb.geo.Sphere
+import org.grails.datastore.bson.codecs.CodecCustomTypeMarshaller
+import org.grails.datastore.bson.query.BsonQuery
+import org.grails.datastore.bson.query.BsonQuery.ProjectionHandler
+import org.grails.datastore.bson.query.BsonQuery.QueryHandler
+import org.grails.datastore.bson.query.EmbeddedQueryEncoder
+import org.grails.datastore.gorm.mongo.geo.GeoJSONType
+import org.grails.datastore.gorm.query.AbstractResultList
+import org.grails.datastore.mapping.core.SessionImplementor
+import org.grails.datastore.mapping.engine.EntityAccess
+import org.grails.datastore.mapping.engine.EntityPersister
+import org.grails.datastore.mapping.engine.types.CustomTypeMarshaller
+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.Custom
+import org.grails.datastore.mapping.model.types.Embedded
+import org.grails.datastore.mapping.model.types.EmbeddedCollection
+import org.grails.datastore.mapping.model.types.ToOne
+import org.grails.datastore.mapping.mongo.AbstractMongoSession
+import org.grails.datastore.mapping.mongo.MongoCodecSession
+import org.grails.datastore.mapping.mongo.MongoDatastore
+import org.grails.datastore.mapping.mongo.config.MongoCollection
+import org.grails.datastore.mapping.mongo.engine.MongoCodecEntityPersister
+import org.grails.datastore.mapping.mongo.engine.MongoEntityPersister
+import org.grails.datastore.mapping.mongo.engine.MongoIdCoercion
+import org.grails.datastore.mapping.mongo.engine.codecs.PersistentEntityCodec
+import org.grails.datastore.mapping.query.AssociationQuery
+import org.grails.datastore.mapping.query.Query
+import org.grails.datastore.mapping.query.Query.AvgProjection
+import org.grails.datastore.mapping.query.Query.Conjunction
+import org.grails.datastore.mapping.query.Query.CountDistinctProjection
+import org.grails.datastore.mapping.query.Query.CountProjection
+import org.grails.datastore.mapping.query.Query.Criterion
+import org.grails.datastore.mapping.query.Query.Disjunction
+import org.grails.datastore.mapping.query.Query.DistinctProjection
+import org.grails.datastore.mapping.query.Query.DistinctPropertyProjection
+import org.grails.datastore.mapping.query.Query.IdEquals
+import org.grails.datastore.mapping.query.Query.IdProjection
+import org.grails.datastore.mapping.query.Query.In
+import org.grails.datastore.mapping.query.Query.Junction
+import org.grails.datastore.mapping.query.Query.MaxProjection
+import org.grails.datastore.mapping.query.Query.MinProjection
+import org.grails.datastore.mapping.query.Query.Order
+import org.grails.datastore.mapping.query.Query.Projection
+import org.grails.datastore.mapping.query.Query.PropertyCriterion
+import org.grails.datastore.mapping.query.Query.PropertyProjection
+import org.grails.datastore.mapping.query.Query.SumProjection
+import org.grails.datastore.mapping.query.api.QueryArgumentsAware
+import org.grails.datastore.mapping.query.projections.ManualProjections
+
+/**
+ * A {@link org.grails.datastore.mapping.query.Query} implementation for the Mongo document store.
+ *
+ * @author Graeme Rocher
+ * @since 1.0
+ */
+@CompileStatic
+@SuppressWarnings('rawtypes')
+class MongoQuery extends BsonQuery implements QueryArgumentsAware {
+
+ public static final String MONGO_IN_OPERATOR = IN_OPERATOR
+ public static final String MONGO_OR_OPERATOR = OR_OPERATOR
+ public static final String MONGO_AND_OPERATOR = AND_OPERATOR
+ public static final String MONGO_GTE_OPERATOR = GTE_OPERATOR
+ public static final String MONGO_LTE_OPERATOR = LTE_OPERATOR
+ public static final String MONGO_GT_OPERATOR = GT_OPERATOR
+ public static final String MONGO_LT_OPERATOR = LT_OPERATOR
+ public static final String MONGO_NE_OPERATOR = NE_OPERATOR
+ public static final String MONGO_NIN_OPERATOR = NIN_OPERATOR
+ public static final String MONGO_REGEX_OPERATOR = REGEX_OPERATOR
+
+ public static final String MONGO_WHERE_OPERATOR = WHERE_OPERATOR
+
+ public static final String HINT_ARGUMENT = 'hint'
+ public static final String READ_CONCERN_ARGUMENT = 'readConcern'
+
+ private Map queryArguments = Collections.emptyMap()
+
+ public static final String NEAR_OPERATOR = '$near'
+
+ public static final String BOX_OPERATOR = '$box'
+
+ public static final String POLYGON_OPERATOR = '$polygon'
+
+ public static final String WITHIN_OPERATOR = '$within'
+
+ public static final String CENTER_OPERATOR = '$center'
+
+ public static final String GEO_WITHIN_OPERATOR = '$geoWithin'
+
+ public static final String GEOMETRY_OPERATOR = '$geometry'
+
+ public static final String CENTER_SPHERE_OPERATOR = '$centerSphere'
+
+ public static final String GEO_INTERSECTS_OPERATOR = '$geoIntersects'
+
+ public static final String MAX_DISTANCE_OPERATOR = '$maxDistance'
+
+ public static final String NEAR_SPHERE_OPERATOR = '$nearSphere'
+
+ static {
+ queryHandlers.put(IdEquals, new QueryHandler<IdEquals>() {
+ // Exercised end-to-end by StringIdWithObjectIdStorageSpec:
+ // - "with storedAs ObjectId, point lookup by hex string works" (happy path)
+ // - "with storedAs ObjectId, point lookup of a non-hex id matches the
+ // BSON String the encoder wrote" (null-return fallback for natural keys)
+ // - "with storedAs ObjectId, updates persist (no phantom OptimisticLockingException)"
+ // and related update/delete specs (which also hit IdEquals via the session filter)
+ void handle(EmbeddedQueryEncoder queryEncoder, IdEquals criterion, Document query, PersistentEntity entity) {
+ Object value = criterion.getValue()
+ // Prefer the configured storage type ('storedAs' on the id mapping) so query BSON
+ // matches what's actually on disk. Falls back to the declared Java type otherwise.
+ Class<?> storedAs = MongoIdCoercion.resolveStoredAs(entity)
+ Class<?> targetType = storedAs != null ? storedAs : entity.getIdentity().getType()
+ Object converted = entity.getMappingContext().getConversionService().convert(value, targetType)
+ // Symmetry with IdentityEncoder's non-hex fallback: if the converter returns
+ // null for a non-null input (e.g. a natural-key String being converted to
+ // ObjectId), keep the original value so the query targets what the encoder
+ // actually wrote rather than {_id: null}.
+ if (converted == null && value != null) {
+ converted = value
+ }
+ query.put(MongoEntityPersister.MONGO_ID_FIELD, converted)
+ }
+ })
+
+ // Override the In handler so that criteria targeting the identity (findAllByIdInList,
+ // Domain.createCriteria().list { 'in'('id', [...]) }, etc.) honor 'storedAs' the same way
+ // IdEquals does. Without this, a domain declaring storedAs: ObjectId would send BSON Strings
+ // in {_id: {$in: [...]}} and miss all stored ObjectId documents.
+ //
+ // Exercised end-to-end by StringIdWithObjectIdStorageSpec:
+ // - "with storedAs ObjectId, findAllByIdInList resolves all ids" (happy path via dynamic finder)
+ // - "with storedAs ObjectId, criteria in('id', [...]) resolves all ids" (happy path via createCriteria)
+ // - "with storedAs ObjectId, batch getAll with non-hex ids falls back to BSON String in the in-list"
+ // (null-return fallback for natural keys)
+ queryHandlers.put(In, new QueryHandler<In>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, In inCriterion, Document query, PersistentEntity entity) {
+ Document inQuery = new Document()
+ List<Object> values = getInListQueryValues(entity, inCriterion)
+
+ PersistentProperty identityProp = entity.getIdentity()
+ boolean isIdInList = identityProp != null && identityProp.getName() == inCriterion.getProperty()
+ if (isIdInList && MongoIdCoercion.resolveStoredAs(entity) != null) {
+ List<Object> coerced = new ArrayList<Object>(values.size())
+ for (Object v : values) {
+ coerced.add(MongoIdCoercion.coerceIdToStoredType(v, entity))
+ }
+ values = coerced
+ }
+
+ inQuery.put(IN_OPERATOR, values)
+ String propertyName = getPropertyName(entity, inCriterion)
+ query.put(propertyName, inQuery)
+ }
+ })
+
+ queryHandlers.put(AssociationQuery, new QueryHandler<AssociationQuery>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, AssociationQuery criterion, Document query, PersistentEntity entity) {
+ Association<?> association = criterion.getAssociation()
+ PersistentEntity associatedEntity = association.getAssociatedEntity()
+ if (association instanceof EmbeddedCollection) {
+ Document associationCollectionQuery = new Document()
+ populateMongoQuery(queryEncoder, associationCollectionQuery, criterion.getCriteria(), associatedEntity)
+ Document collectionQuery = new Document('$elemMatch', associationCollectionQuery)
+ String propertyKey = getPropertyName(entity, association.getName())
+ query.put(propertyKey, collectionQuery)
+ } else if (associatedEntity instanceof EmbeddedPersistentEntity || association instanceof Embedded) {
+ Document associatedEntityQuery = new Document()
+ populateMongoQuery(queryEncoder, associatedEntityQuery, criterion.getCriteria(), associatedEntity)
+ String propertyKey = getPropertyName(entity, association.getName())
+ prefixEmbeddedQuery(propertyKey, associatedEntityQuery, query)
+ } else {
+ throw new UnsupportedOperationException('Join queries are not supported by MongoDB')
+ }
+ }
+ })
+
+ queryHandlers.put(WithinBox, new QueryHandler<WithinBox>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, WithinBox withinBox, Document query, PersistentEntity entity) {
+ Document nearQuery = new Document()
+ Document box = new Document()
+ MongoEntityPersister.setDBObjectValue(box, BOX_OPERATOR, withinBox.getValues(), entity.getMappingContext())
+ nearQuery.put(WITHIN_OPERATOR, box)
+ String propertyName = getPropertyName(entity, withinBox)
+ query.put(propertyName, nearQuery)
+ }
+ })
+
+ queryHandlers.put(WithinPolygon, new QueryHandler<WithinPolygon>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, WithinPolygon withinPolygon, Document query, PersistentEntity entity) {
+ Document nearQuery = new Document()
+ Document box = new Document()
+ MongoEntityPersister.setDBObjectValue(box, POLYGON_OPERATOR, withinPolygon.getValues(), entity.getMappingContext())
+ nearQuery.put(WITHIN_OPERATOR, box)
+ String propertyName = getPropertyName(entity, withinPolygon)
+ query.put(propertyName, nearQuery)
+ }
+ })
+
+ queryHandlers.put(WithinCircle, new QueryHandler<WithinCircle>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, WithinCircle withinCentre, Document query, PersistentEntity entity) {
+ Document nearQuery = new Document()
+ Document center = new Document()
+ MongoEntityPersister.setDBObjectValue(center, CENTER_OPERATOR, withinCentre.getValues(), entity.getMappingContext())
+ nearQuery.put(WITHIN_OPERATOR, center)
+ String propertyName = getPropertyName(entity, withinCentre)
+ query.put(propertyName, nearQuery)
+ }
+ })
+
+ QueryHandler<Near> nearHandler = new QueryHandler<Near>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, Near near, Document query, PersistentEntity entity) {
+ Document nearQuery = new Document()
+ Object value = near.getValue()
+ String nearOperator = near instanceof NearSphere ? NEAR_SPHERE_OPERATOR : NEAR_OPERATOR
+ if ((value instanceof List) || (value instanceof Map)) {
+ MongoEntityPersister.setDBObjectValue(nearQuery, nearOperator, value, entity.getMappingContext())
+ } else if (value instanceof Point) {
+ Document geoJson = GeoJSONType.convertToGeoDocument((Point) value)
+ Document geometry = new Document()
+ geometry.put(GEOMETRY_OPERATOR, geoJson)
+ if (near.maxDistance != null) {
+ geometry.put(MAX_DISTANCE_OPERATOR, near.maxDistance.getValue())
+ }
+ nearQuery.put(nearOperator, geometry)
+ }
+
+ String propertyName = getPropertyName(entity, near)
+ query.put(propertyName, nearQuery)
+ }
+ }
+ queryHandlers.put(Near, nearHandler)
+ queryHandlers.put(NearSphere, nearHandler)
+
+ queryHandlers.put(GeoWithin, new QueryHandler<GeoWithin>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, GeoWithin geoWithin, Document query, PersistentEntity entity) {
+ Document queryRoot = new Document()
+ Document queryGeoWithin = new Document()
+ queryRoot.put(GEO_WITHIN_OPERATOR, queryGeoWithin)
+ String targetProperty = getPropertyName(entity, geoWithin)
+ Object value = geoWithin.getValue()
+ if (value instanceof Shape) {
+ Shape shape = (Shape) value
+ if (shape instanceof Polygon) {
+ Polygon p = (Polygon) shape
+ Document geoJson = GeoJSONType.convertToGeoDocument(p)
+ queryGeoWithin.put(GEOMETRY_OPERATOR, geoJson)
+ } else if (shape instanceof Box) {
+ queryGeoWithin.put(BOX_OPERATOR, shape.asList())
+ } else if (shape instanceof Circle) {
+ queryGeoWithin.put(CENTER_OPERATOR, shape.asList())
+ } else if (shape instanceof Sphere) {
+ queryGeoWithin.put(CENTER_SPHERE_OPERATOR, shape.asList())
+ }
+ } else if (value instanceof Map) {
+ queryGeoWithin.putAll((Map) value)
+ }
+
+ query.put(targetProperty, queryRoot)
+ }
+ })
+
+ queryHandlers.put(GeoIntersects, new QueryHandler<GeoIntersects>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, GeoIntersects geoIntersects, Document query, PersistentEntity entity) {
+ Document queryRoot = new Document()
+ Document queryGeoWithin = new Document()
+ queryRoot.put(GEO_INTERSECTS_OPERATOR, queryGeoWithin)
+ String targetProperty = getPropertyName(entity, geoIntersects)
+ Object value = geoIntersects.getValue()
+ if (value instanceof GeoJSON) {
+ Shape shape = (Shape) value
+ Document geoJson = GeoJSONType.convertToGeoDocument(shape)
+ queryGeoWithin.put(GEOMETRY_OPERATOR, geoJson)
+ } else if (value instanceof Map) {
+ queryGeoWithin.putAll((Map) value)
+ }
+
+ query.put(targetProperty, queryRoot)
+ }
+ })
+ queryHandlers.put(Conjunction, new QueryHandler<Conjunction>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, Conjunction criterion, Document query, PersistentEntity entity) {
+ populateMongoQuery(queryEncoder, query, criterion, entity)
+ }
+ })
+
+ queryHandlers.put(Disjunction, new QueryHandler<Disjunction>() {
+ void handle(EmbeddedQueryEncoder queryEncoder, Disjunction criterion, Document query, PersistentEntity entity) {
+ populateMongoQuery(queryEncoder, query, criterion, entity)
+ }
+ })
+
+ groupByProjectionHandlers.put(AvgProjection, new ProjectionHandler<AvgProjection>() {
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, AvgProjection projection) {
+ return addProjectionToGroupBy(projectObject, groupBy, projection, AVERAGE_OPERATOR, 'avg_')
+ }
+ })
+ groupByProjectionHandlers.put(CountProjection, new ProjectionHandler<CountProjection>() {
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, CountProjection projection) {
+ projectObject.put(MongoEntityPersister.MONGO_ID_FIELD, 1)
+ String projectionKey = 'count'
+ groupBy.put(projectionKey, new Document(SUM_OPERATOR, 1))
+ return projectionKey
+ }
+ })
+ groupByProjectionHandlers.put(CountDistinctProjection, new ProjectionHandler<CountDistinctProjection>() {
+ // equivalent of "select count (distinct fieldName) from someTable". Example:
+ // db.someCollection.aggregate([{ $group: { _id: "$fieldName"} },{ $group: { _id: 1, count: { $sum: 1 } } } ])
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, CountDistinctProjection projection) {
+ projectObject.put(projection.getPropertyName(), 1)
+ String property = projection.getPropertyName()
+ String projectionValueKey = 'countDistinct_' + property
+ Document id = getIdObjectForGroupBy(groupBy)
+ id.put(projectionValueKey, '$' + property)
+ return projectionValueKey
+ }
+ })
+
+ groupByProjectionHandlers.put(MinProjection, new ProjectionHandler<MinProjection>() {
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, MinProjection projection) {
+ return addProjectionToGroupBy(projectObject, groupBy, projection, MIN_OPERATOR, 'min_')
+ }
+ })
+ groupByProjectionHandlers.put(MaxProjection, new ProjectionHandler<MaxProjection>() {
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, MaxProjection projection) {
+ return addProjectionToGroupBy(projectObject, groupBy, projection, MAX_OPERATOR, 'max_')
+ }
+ })
+ groupByProjectionHandlers.put(SumProjection, new ProjectionHandler<SumProjection>() {
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, SumProjection projection) {
+ return addProjectionToGroupBy(projectObject, groupBy, projection, SUM_OPERATOR, 'sum_')
+ }
+ })
+
+ projectProjectionHandlers.put(DistinctPropertyProjection, new ProjectionHandler<DistinctPropertyProjection>() {
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, DistinctPropertyProjection projection) {
+ String property = projection.getPropertyName()
+ projectObject.put(property, 1)
+ Document id = getIdObjectForGroupBy(groupBy)
+ String projectedValueKey = property.replace('.', '_')
+ id.put(projectedValueKey, '$' + property)
+ return projectedValueKey
+ }
+ })
+
+ projectProjectionHandlers.put(PropertyProjection, new ProjectionHandler<PropertyProjection>() {
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, PropertyProjection projection) {
+ String property = projection.getPropertyName()
+ projectObject.put(property, 1)
+ Document id = getIdObjectForGroupBy(groupBy)
+ String projectedValueKey = property.replace('.', '_')
+ id.put(projectedValueKey, '$' + property)
+ // we add the id to the grouping to make it not distinct
+ id.put(MongoEntityPersister.MONGO_ID_FIELD, '$' + MongoEntityPersister.MONGO_ID_FIELD)
+ return projectedValueKey
+ }
+ })
+
+ projectProjectionHandlers.put(IdProjection, new ProjectionHandler<IdProjection>() {
+ @Override
+ String handle(PersistentEntity entity, Document projectObject, Document groupBy, IdProjection projection) {
+ projectObject.put(MongoEntityPersister.MONGO_ID_FIELD, 1)
+ Document id = getIdObjectForGroupBy(groupBy)
+ id.put(MongoEntityPersister.MONGO_ID_FIELD, '$_id')
+
+ return MongoEntityPersister.MONGO_ID_FIELD
+ }
+ })
+ }
+
+ @PackageScope
+ static Document getIdObjectForGroupBy(Document groupBy) {
+ Object value = groupBy.get(MongoEntityPersister.MONGO_ID_FIELD)
+ Document id
+ if (value instanceof Document) {
+ id = (Document) value
+ } else {
+ id = new Document()
+ groupBy.put(MongoEntityPersister.MONGO_ID_FIELD, id)
+ }
+ return id
+ }
+
+ @PackageScope
+ static String addProjectionToGroupBy(Document projectObject, Document groupBy, PropertyProjection projection, String operator, String prefix) {
+ projectObject.put(projection.getPropertyName(), 1)
+ String property = projection.getPropertyName()
+ String projectionValueKey = prefix + property.replace('.', '_')
+ Document averageProjection = new Document(operator, '$' + property)
+ groupBy.put(projectionValueKey, averageProjection)
+ return projectionValueKey
+ }
+
+ private final AbstractMongoSession mongoSession
+ private final EntityPersister mongoEntityPersister
+ private final ManualProjections manualProjections
+ private boolean isCodecPersister = false
+
+ MongoQuery(AbstractMongoSession session, PersistentEntity entity) {
+ super(session, entity)
+ this.mongoSession = session
+ this.manualProjections = new ManualProjections(entity)
+ if (session != null) {
+ this.mongoEntityPersister = (EntityPersister) session.getPersister(entity)
+ if (this.mongoEntityPersister instanceof MongoCodecEntityPersister) {
+ this.isCodecPersister = true
+ }
+ } else {
+ mongoEntityPersister = null
+ }
+ }
+
+ @Override
+ protected void flushBeforeQuery() {
+ // with Mongo we only flush the session if a transaction is not active to allow for session-managed transactions
+ if (!TransactionSynchronizationManager.isSynchronizationActive()) {
+ super.flushBeforeQuery()
+ }
+ }
+
+ /**
+ * Gets the Mongo query for this query instance
+ *
+ * @return The Mongo query
+ */
+ Document getMongoQuery() {
+ Document query = createQueryObject(entity)
+ populateMongoQuery((AbstractMongoSession) getSession(), query, criteria, entity)
+ return query
+ }
+
+ @Override
+ protected List executeQuery(final PersistentEntity entity, final Junction criteria) {
+ final AbstractMongoSession mongoSession = this.mongoSession
+ com.mongodb.client.MongoCollection<Document> collection = mongoSession.getCollection(entity)
+
+ final List<Projection> projectionList = projections().getProjectionList()
+ boolean hasOnlyDistinct = projectionList.size() == 1 && (projectionList.get(0) instanceof DistinctProjection)
+ if (uniqueResult && (projectionList.isEmpty() || hasOnlyDistinct)) {
+ if (isCodecPersister) {
+ collection = (com.mongodb.client.MongoCollection<Document>) (com.mongodb.client.MongoCollection) collection
+ .withDocumentClass(entity.getJavaClass())
+ }
+ Object dbObject
+ if (criteria.isEmpty()) {
+ FindIterable<Document> cursor = mongoSession.find(collection, createQueryObject(entity))
+
+ // Raw on purpose: with the codec persister the collection was re-typed to the entity
+ // class above, so first() yields an entity, and a FindIterable<Document> here would
+ // make the static compiler insert a cast to Document that fails at runtime.
+ dbObject = ((FindIterable) setHint(cursor)).limit(1)
+ .first()
+ } else {
+ FindIterable<Document> cursor = mongoSession.find(collection, getMongoQuery())
+
+ // Raw on purpose: with the codec persister the collection was re-typed to the entity
+ // class above, so first() yields an entity, and a FindIterable<Document> here would
+ // make the static compiler insert a cast to Document that fails at runtime.
+ dbObject = ((FindIterable) setHint(cursor)).limit(1)
+ .first()
+ }
+ if (dbObject == null) {
+ return wrapObjectResultInList(dbObject)
+ }
+ if (isCodecPersister) {
+ if (!mongoSession.contains(dbObject)) {
+ final EntityAccess entityAccess = mongoSession.createEntityAccess(entity, dbObject)
+ mongoEntityPersister.firePostLoadEvent(entity, entityAccess)
+ mongoSession.cacheInstance(dbObject.getClass(), (Serializable) entityAccess.getIdentifier(), dbObject)
+ }
+ return wrapObjectResultInList(dbObject)
+ }
+ return wrapObjectResultInList(createObjectFromDBObject((Document) dbObject))
+ }
+
+ MongoCursor<Document> cursor
+ Document query = createQueryObject(entity)
+
+ if (projectionList.isEmpty() || hasOnlyDistinct) {
+ if (isCodecPersister) {
+ collection = (com.mongodb.client.MongoCollection<Document>) (com.mongodb.client.MongoCollection) collection
+ .withDocumentClass(entity.getJavaClass())
+ .withCodecRegistry(mongoSession.getDatastore().getCodecRegistry())
+ }
+ cursor = executeQuery(entity, criteria, collection, query)
+ return new MongoResultList(cursor, offset, mongoEntityPersister)
+ }
+
+ populateMongoQuery((AbstractMongoSession) session, query, criteria, entity)
+ AggregatePipeline aggregatePipeline = buildAggregatePipeline(entity, query, projectionList)
+ List<Document> aggregationPipeline = aggregatePipeline.getAggregationPipeline()
+ boolean singleResult = aggregatePipeline.isSingleResult()
+ List<ProjectedProperty> projectedKeys = aggregatePipeline.getProjectedKeys()
+ List projectedResults = new ArrayList()
+
+ AggregateIterable<Document> aggregatedResults = mongoSession.aggregate(collection, aggregationPipeline)
+ aggregatedResults = (AggregateIterable<Document>) setHint(aggregatedResults)
+ final MongoCursor<Document> aggregateCursor = aggregatedResults.iterator()
+
+ if (singleResult && aggregateCursor.hasNext()) {
+ Document dbo = aggregateCursor.next()
+ for (ProjectedProperty projectedProperty : projectedKeys) {
+ Object value = dbo.get(projectedProperty.projectionKey)
+ PersistentProperty property = projectedProperty.property
+ if (value != null) {
+ if (property instanceof ToOne) {
+ projectedResults.add(session.retrieve(property.getType(), (Serializable) value))
+ } else {
+ projectedResults.add(value)
+ }
+ } else {
+ if (projectedProperty.projection instanceof CountProjection) {
+ projectedResults.add(0)
+ }
+ }
+ }
+ } else {
+ return new AggregatedResultList((AbstractMongoSession) getSession(), aggregateCursor, projectedKeys)
+ }
+
+ return projectedResults
+ }
+
+ protected AggregatePipeline buildAggregatePipeline(PersistentEntity entity, Document query, List<Projection> projectionList) {
+ return new AggregatePipeline(this, entity, query, projectionList).build()
+ }
+
+ protected MongoCursor<Document> executeQuery(final PersistentEntity entity,
+ final Junction criteria,
+ final com.mongodb.client.MongoCollection<Document> collection,
+ final Document query) {
+ FindIterable<Document> cursor
+ if (criteria.isEmpty()) {
+ cursor = executeQueryAndApplyPagination(collection, query)
+ } else {
+ populateMongoQuery((AbstractMongoSession) session, query, criteria, entity)
+ cursor = executeQueryAndApplyPagination(collection, query)
+ }
+
+ cursor = (FindIterable<Document>) setHint(cursor)
+
+ return cursor.iterator()
+ }
+
+ private MongoIterable<Document> setHint(MongoIterable<Document> cursor) {
+ MongoIterable<Document> result = cursor
+
+ if (queryArguments != null) {
+ if (queryArguments.containsKey(HINT_ARGUMENT)) {
+ Object hint = queryArguments.get(HINT_ARGUMENT)
+ if (hint instanceof Map) {
+ if (cursor instanceof FindIterable) {
+ result = ((FindIterable<Document>) cursor).hint(new Document((Map<String, Object>) hint))
+ } else if (cursor instanceof AggregateIterable) {
+ result = ((AggregateIterable<Document>) cursor).hint(new Document((Map<String, Object>) hint))
+ }
+ } else {
+ if (cursor instanceof FindIterable) {
+ result = ((FindIterable<Document>) cursor).hintString(hint.toString())
+ } else if (cursor instanceof AggregateIterable) {
+ result = ((AggregateIterable<Document>) cursor).hintString(hint.toString())
+ }
+ }
+ }
+ }
+
+ return result
+ }
+
+ protected FindIterable<Document> executeQueryAndApplyPagination(com.mongodb.client.MongoCollection<Document> collection, Document query) {
+ com.mongodb.client.MongoCollection<Document> target = collection
+ Object readConcernObject = queryArguments.get(READ_CONCERN_ARGUMENT)
+ if (readConcernObject instanceof ReadConcern) {
+ target = target.withReadConcern(
+ (ReadConcern) readConcernObject
+ )
+ }
+
+ final FindIterable<Document> iterable = mongoSession.find(target, query)
+ if (offset != null && offset > 0) {
+ iterable.skip(offset)
+ }
+ if (max != null && max > -1) {
+ iterable.limit(max)
+ }
+ if (uniqueResult) {
+ iterable.limit(1)
+ }
+
+ if (!orderBy.isEmpty()) {
+ Document orderObject = new Document()
+ for (Order order : orderBy) {
+ String property = order.getProperty()
+ property = getPropertyName(entity, property)
+ orderObject.put(property, order.getDirection() == Order.Direction.DESC ? -1 : 1)
+ }
+ iterable.sort(orderObject)
+ } else {
+ MongoCollection coll = (MongoCollection) entity.getMapping().getMappedForm()
+ if (coll != null && coll.getSort() != null) {
+ Document orderObject = new Document()
+ Order order = (Order) coll.getSort()
+ String property = order.getProperty()
+ property = getPropertyName(entity, property)
+ orderObject.put(property, order.getDirection() == Order.Direction.DESC ? -1 : 1)
+ iterable.sort(orderObject)
+ }
+ }
+
+ return iterable
+ }
+
+ private Document getClassFieldDocument(final PersistentEntity entity) {
+ Object classFieldValue
+ Collection<PersistentEntity> childEntities = entity.getMappingContext().getChildEntities(entity)
+ if (childEntities.size() > 0) {
+ HashMap classValue = new HashMap()
+ ArrayList classes = new ArrayList()
+ classes.add(entity.getDiscriminator())
+ for (PersistentEntity childEntity : childEntities) {
+ classes.add(childEntity.getDiscriminator())
+ }
+ classValue.put(MONGO_IN_OPERATOR, classes)
+ classFieldValue = classValue
+ } else {
+ classFieldValue = entity.getDiscriminator()
+ }
+ return new Document(MongoEntityPersister.MONGO_CLASS_FIELD, classFieldValue)
+ }
+
+ protected Document createQueryObject(PersistentEntity persistentEntity) {
+ Document query
+ if (persistentEntity.isRoot()) {
+ query = new Document()
+ } else {
+ query = getClassFieldDocument(persistentEntity)
+ }
+ return query
+ }
+
+ static void populateMongoQuery(final AbstractMongoSession session, Document query, Junction criteria, final PersistentEntity entity) {
+ EmbeddedQueryEncoder queryEncoder
+ if (session instanceof MongoCodecSession) {
+ final MongoDatastore datastore = session.getDatastore()
+ final CodecRegistry codecRegistry = datastore.getCodecRegistry()
+ queryEncoder = new EmbeddedQueryEncoder() {
+ @Override
+ Object encode(Embedded embedded, Object instance) {
+ final PersistentEntityCodec codec = (PersistentEntityCodec) codecRegistry.get(embedded.getType())
+ final BsonDocument doc = new BsonDocument()
+ codec.encode(new BsonDocumentWriter(doc), instance, ENCODER_CONTEXT, false)
+ return doc
+ }
+ }
+ } else {
+ queryEncoder = new EmbeddedQueryEncoder() {
+ @Override
+ Object encode(Embedded embedded, Object instance) {
+ MongoEntityPersister persister = (MongoEntityPersister) session.getPersister(entity.getJavaClass())
+ return persister.createNativeObjectForEmbedded(embedded, instance)
+ }
+ }
+ }
+
+ populateMongoQuery(queryEncoder, query, criteria, entity)
+ }
+
+ @SuppressWarnings('unchecked')
+ static void populateMongoQuery(final EmbeddedQueryEncoder queryEncoder, Document query, Junction criteria, final PersistentEntity entity) {
+ List subList = null
+ // if a query combines more than 1 item, wrap the items in individual $and or $or arguments
+ // so that property names can't clash (e.g. for an $and containing two $ors)
+ if (criteria.getCriteria().size() > 1) {
+ if (criteria instanceof Disjunction) {
+ subList = new ArrayList()
+ query.put(OR_OPERATOR, subList)
+ } else if (criteria instanceof Conjunction) {
+ subList = new ArrayList()
+ query.put(AND_OPERATOR, subList)
+ }
+ }
+ for (Criterion criterion : criteria.getCriteria()) {
+ final QueryHandler queryHandler = queryHandlers.get(criterion.getClass())
+ if (queryHandler != null) {
+ Document dbo = query
+ if (subList != null) {
+ dbo = new Document()
+ subList.add(dbo)
+ }
+
+ if (criterion instanceof PropertyCriterion && !(criterion instanceof GeoCriterion)) {
+ PropertyCriterion pc = (PropertyCriterion) criterion
+ PersistentProperty property = entity.getPropertyByName(pc.getProperty())
+ if (property instanceof Custom) {
+ CustomTypeMarshaller<Object, Object, Document> customTypeMarshaller = (CustomTypeMarshaller<Object, Object, Document>) ((Custom) property).getCustomTypeMarshaller()
+ if (!(customTypeMarshaller instanceof CodecCustomTypeMarshaller)) {
+ customTypeMarshaller.query(property, pc, query)
+ continue
+ }
+ }
+ }
+ queryHandler.handle(queryEncoder, criterion, dbo, entity)
+ } else {
+ throw new InvalidDataAccessResourceUsageException('Queries of type ' + criterion.getClass().getSimpleName() + ' are not supported by this implementation')
+ }
+ }
+ }
+
+ /**
+ * Rewrites a query built against an embedded entity so it applies to the owning document,
+ * qualifying each property name with the embedded property's path. Logical operators such as
+ * {@code $and} and {@code $or} must stay at the current level (a key like {@code extRef1.$and}
+ * matches nothing), so their nested documents are rewritten recursively instead.
+ */
+ @PackageScope
+ static void prefixEmbeddedQuery(String prefix, Document source, Document target) {
+ for (String key : source.keySet()) {
+ Object value = source.get(key)
+ if (key.charAt(0) == ('$' as char)) {
+ if (!(value instanceof List)) {
+ // A top-level operator whose value is not a rewritable list of clauses
+ // (e.g. $where from a property-to-property comparison, or $text) cannot be
+ // qualified with the embedded path - prefixing it would silently match nothing.
+ throw new UnsupportedOperationException('Criterion [' + key +
+ '] is not supported inside an embedded association query')
+ }
+ List<Object> rewritten = new ArrayList<>()
+ for (Object element : (List) value) {
+ if (element instanceof Document) {
+ Document rewrittenElement = new Document()
+ prefixEmbeddedQuery(prefix, (Document) element, rewrittenElement)
+ rewritten.add(rewrittenElement)
+ } else {
+ rewritten.add(element)
+ }
+ }
+ target.put(key, rewritten)
+ } else {
+ target.put(prefix + '.' + key, value)
+ }
+ }
+ }
+
+ private Object createObjectFromDBObject(Document dbObject) {
+ // we always use the session cached version where available.
+ final Object id = dbObject.get(MongoEntityPersister.MONGO_ID_FIELD)
+ Class type = mongoEntityPersister.getPersistentEntity().getJavaClass()
+ Object instance = mongoSession.getCachedInstance(type, (Serializable) id)
+ if (instance == null) {
+ instance = ((MongoEntityPersister) mongoEntityPersister).createObjectFromNativeEntry(
+ mongoEntityPersister.getPersistentEntity(), (Serializable) id, dbObject)
+ mongoSession.cacheInstance(type, (Serializable) id, instance)
+ }
+ // note cached instances may be stale, but user can call 'refresh' to fix that.
+ return instance
+ }
+
+ private List wrapObjectResultInList(Object object) {
+ List result = new ArrayList()
+ result.add(object)
+ return result
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query near(String property, List value) {
+ add(new Near(property, value))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query near(String property, Point value) {
+ add(new Near(property, value))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query near(String property, List value, Distance maxDistance) {
+ add(new Near(property, value, maxDistance))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query near(String property, Point value, Distance maxDistance) {
+ add(new Near(property, value, maxDistance))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query near(String property, List value, Number maxDistance) {
+ add(new Near(property, value, maxDistance))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query near(String property, Point value, Number maxDistance) {
+ add(new Near(property, value, maxDistance))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query nearSphere(String property, List value) {
+ add(new NearSphere(property, value))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query nearSphere(String property, Point value) {
+ add(new NearSphere(property, value))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query nearSphere(String property, List value, Distance maxDistance) {
+ add(new NearSphere(property, value, maxDistance))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query nearSphere(String property, Point value, Distance maxDistance) {
+ add(new NearSphere(property, value, maxDistance))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query nearSphere(String property, List value, Number maxDistance) {
+ add(new NearSphere(property, value, maxDistance))
+ return this
+ }
+
+ /**
+ * Geospacial query for values near the given two dimensional list
+ *
+ * @param property The property
+ * @param value A two dimensional list of values
+ * @return this
+ */
+ Query nearSphere(String property, Point value, Number maxDistance) {
+ add(new NearSphere(property, value, maxDistance))
+ return this
+ }
+
+ /**
+ * Geospacial query for values within a given box. A box is defined as a multi-dimensional list in the form
+ * [[40.73083, -73.99756], [40.741404, -73.988135]]
+ *
+ * @param property The property
+ * @param value A multi-dimensional list of values
+ * @return This query
+ */
+ Query withinBox(String property, List value) {
+ add(new WithinBox(property, value))
+ return this
+ }
+
+ /**
+ * Geospacial query for values within the given shape
+ *
+ * @param property The property
+ * @param shape The shape
+ * @return The query instance
+ */
+ Query geoWithin(String property, Shape shape) {
+ add(new GeoWithin(property, shape))
+ return this
+ }
+
+ /**
+ * Geospacial query for values within the given shape
+ *
+ * @param property The property
+ * @param shape The shape
+ * @return The query instance
+ */
+ Query geoIntersects(String property, GeoJSON shape) {
+ add(new GeoIntersects(property, shape))
+ return this
+ }
+
+ /**
+ * Geospacial query for values within a given polygon. A polygon is defined as a multi-dimensional list in the form
+ * [[0, 0], [3, 6], [6, 0]]
+ *
+ * @param property The property
+ * @param value A multi-dimensional list of values
+ * @return This query
+ */
+ Query withinPolygon(String property, List value) {
+ add(new WithinPolygon(property, value))
+ return this
+ }
+
+ /**
+ * Geospacial query for values within a given circle. A circle is defined as a multi-dimensial list containing the position of the center and the radius:
+ * [[50, 50], 10]
+ *
+ * @param property The property
+ * @param value A multi-dimensional list of values
+ * @return This query
+ */
+ Query withinCircle(String property, List value) {
+ add(new WithinBox(property, value))
+ return this
+ }
+
+ /**
+ * @param arguments The query arguments
+ */
+ void setArguments(Map arguments) {
+ this.queryArguments = arguments
+ }
+
+ /**
+ * Used for Geospacial querying
+ *
+ * @author Graeme Rocher
+ * @since 1.0
+ */
+ static class Near extends GeoCriterion {
+
+ Distance maxDistance = null
+
+ Near(String name, Object value) {
+ super(name, value)
+ }
+
+ Near(String name, Object value, Distance maxDistance) {
+ super(name, value)
+ this.maxDistance = maxDistance
+ }
+
+ Near(String name, Object value, Number maxDistance) {
+ super(name, value)
+ this.maxDistance = Distance.valueOf(maxDistance.doubleValue())
+ }
+
+ void setMaxDistance(Distance maxDistance) {
+ this.maxDistance = maxDistance
+ }
+
+ }
+
+ /**
+ * Used for Geospacial querying with the $nearSphere operator
+ *
+ * @author Graeme Rocher
+ * @since 1.0
+ */
+ static class NearSphere extends Near {
+
+ NearSphere(String name, Object value) {
+ super(name, value)
+ }
+
+ NearSphere(String name, Object value, Distance maxDistance) {
+ super(name, value, maxDistance)
+ }
+
+ NearSphere(String name, Object value, Number maxDistance) {
+ super(name, value, maxDistance)
+ }
+
+ }
+
+ /**
+ * Used for Geospacial querying of boxes
+ *
+ * @author Graeme Rocher
+ * @since 1.0
+ */
+ static class WithinBox extends PropertyCriterion {
+
+ WithinBox(String name, List value) {
+ super(name, value)
+ }
+
+ List getValues() {
+ return (List) getValue()
+ }
+
+ void setValue(List matrix) {
+ // Direct field access: through property syntax Groovy would route the assignment back
+ // into this setter.
+ this.@value = matrix
+ }
+
+ }
+
+ /**
+ * Used for Geospacial querying of polygons
+ */
+ static class WithinPolygon extends PropertyCriterion {
+
+ WithinPolygon(String name, List value) {
+ super(name, value)
+ }
+
+ List getValues() {
+ return (List) getValue()
+ }
+
+ void setValue(List value) {
+ // Direct field access: through property syntax Groovy would route the assignment back
+ // into this setter.
+ this.@value = value
+ }
+
+ }
+
+ /**
+ * Used for Geospacial querying of circles
+ *
+ * @author Graeme Rocher
+ * @since 1.0
+ */
+ static class WithinCircle extends PropertyCriterion {
+
+ WithinCircle(String name, List value) {
+ super(name, value)
+ }
+
+ List getValues() {
+ return (List) getValue()
+ }
+
+ void setValue(List matrix) {
+ // Direct field access: through property syntax Groovy would route the assignment back
+ // into this setter.
+ this.@value = matrix
+ }
+
+ }
+
+ /**
+ * Used for all GeoSpacial queries using 2dsphere indexes
+ */
+ static class GeoCriterion extends PropertyCriterion {
+
+ GeoCriterion(String name, Object value) {
+ super(name, value)
+ }
+
+ }
+
+ static class GeoWithin extends GeoCriterion {
+
+ GeoWithin(String name, Object value) {
+ super(name, value)
+ }
+
+ }
+
+ static class GeoIntersects extends GeoCriterion {
+
+ GeoIntersects(String name, Object value) {
+ super(name, value)
+ }
+
+ }
+
+ static class AggregatedResultList extends AbstractList implements Closeable {
+
+ @PackageScope MongoCursor cursor
+ private List<ProjectedProperty> projectedProperties
+ private List initializedObjects = new ArrayList()
+ private int internalIndex = 0
+ @PackageScope boolean initialized = false
+ private boolean containsAssociations = false
+ private AbstractMongoSession session
+
+ AggregatedResultList(AbstractMongoSession session, MongoCursor<Document> cursor, List<ProjectedProperty> projectedProperties) {
+ this.cursor = cursor
+ this.projectedProperties = projectedProperties
+ this.session = session
+ for (ProjectedProperty projectedProperty : projectedProperties) {
+ if (projectedProperty.property instanceof Association) {
+ this.containsAssociations = true
+ break
+ }
+ }
+ }
+
+ @Override
+ String toString() {
+ return initializedObjects.toString()
+ }
+
+ @Override
+ Object get(int index) {
+ if (containsAssociations) {
+ initializeFully()
+ }
+ if (initializedObjects.size() > index) {
+ return initializedObjects.get(index)
+ } else if (!initialized) {
+ boolean hasResults = false
+ while (cursor.hasNext()) {
+ hasResults = true
+ Document dbo = (Document) cursor.next()
+ Object projected = addInitializedObject(dbo)
+ if (index == internalIndex) {
+ return projected
+ }
+ }
+ if (!hasResults) {
+ handleNoResults()
+ }
+ initialized = true
+ }
+ throw new ArrayIndexOutOfBoundsException('Index value ' + index + ' exceeds size of aggregate list')
+ }
+
+ @Override
+ Object set(int index, Object element) {
+ initializeFully()
+ return initializedObjects.set(index, element)
+ }
+
+ @Override
+ ListIterator listIterator() {
+ return listIterator(0)
+ }
+
+ @Override
+ ListIterator listIterator(int index) {
+ initializeFully()
+ return initializedObjects.listIterator(index)
+ }
+
+ protected void initializeFully() {
+ if (initialized) {
+ return
+ }
+ if (containsAssociations) {
+ if (projectedProperties.size() == 1) {
+ ProjectedProperty projectedProperty = projectedProperties.get(0)
+ PersistentProperty property = projectedProperty.property
+ List<Serializable> identifiers = new ArrayList<>()
+ boolean hasResults = false
+ while (cursor.hasNext()) {
+ hasResults = true
+ Document dbo = (Document) cursor.next()
+ Object id = getProjectedValue(dbo, projectedProperty.projectionKey)
+ identifiers.add((Serializable) id)
+ }
+ if (!hasResults) {
+ handleNoResults()
+ } else if (property instanceof Embedded) {
+ Embedded embedded = (Embedded) property
+ List embeddedList = new ArrayList()
+ CodecRegistry codecRegistry = session.getDatastore().getCodecRegistry()
+ PersistentEntityCodec codec = new PersistentEntityCodec(codecRegistry, embedded.getAssociatedEntity())
+
+ for (Serializable embeddedDoc : identifiers) {
+ if (embeddedDoc instanceof Document) {
+ Document documentObject = (Document) embeddedDoc
+
+ Object decoded = codec.decode(new BsonDocumentReader(documentObject.toBsonDocument(Document, codecRegistry)))
+ embeddedList.add(
+ decoded
+ )
+ }
+ }
+
+ this.initializedObjects = embeddedList
+ } else {
+ this.initializedObjects = session.retrieveAll(property.getType(), identifiers)
+ }
+ } else {
+ Map<Integer, Map<Class, List<Serializable>>> associationMap = createAssociationMap()
+
+ boolean hasResults = false
+ while (cursor.hasNext()) {
+ hasResults = true
+ Document dbo = (Document) cursor.next()
+ List<Object> projectedResult = new ArrayList<>()
+ int index = 0
+ for (ProjectedProperty projectedProperty : projectedProperties) {
+ PersistentProperty property = projectedProperty.property
+ Object value = getProjectedValue(dbo, projectedProperty.projectionKey)
+ if (property instanceof Association) {
+ if ((!(property instanceof Embedded) && !(property instanceof EmbeddedCollection) && !(property instanceof Basic))) {
+ Map<Class, List<Serializable>> identifierMap = associationMap.get(index)
+ Class type = ((Association) property).getAssociatedEntity().getJavaClass()
+ identifierMap.get(type).add((Serializable) value)
+ }
+ }
+ projectedResult.add(value)
+ index++
+ }
+
+ initializedObjects.add(projectedResult)
+ }
+
+ if (!hasResults) {
+ handleNoResults()
+ return
+ }
+
+ Map<Integer, List> finalResults = new HashMap<>()
+ for (Integer index : associationMap.keySet()) {
+ Map<Class, List<Serializable>> associatedEntityIdentifiers = associationMap.get(index)
+ for (Class associationClass : associatedEntityIdentifiers.keySet()) {
+ List<Serializable> identifiers = associatedEntityIdentifiers.get(associationClass)
+ finalResults.put(index, session.retrieveAll(associationClass, identifiers))
+ }
+ }
+
+ for (Object initializedObject : initializedObjects) {
+ List projected = (List) initializedObject
+ for (Integer index : finalResults.keySet()) {
+ List resultsByIndex = finalResults.get(index)
+ if (index < resultsByIndex.size()) {
+ projected.set(index, resultsByIndex.get(index))
+ } else {
+ projected.set(index, null)
+ }
+ }
+ }
+ }
+ } else {
+ boolean hasResults = false
+ while (cursor.hasNext()) {
+ hasResults = true
+ Document dbo = (Document) cursor.next()
+ addInitializedObject(dbo)
+ }
+ if (!hasResults) {
+ handleNoResults()
+ }
+ }
+ initialized = true
+ }
+
+ protected void handleNoResults() {
+ ProjectedProperty projectedProperty = projectedProperties.get(0)
+ if (projectedProperty.projection instanceof CountProjection) {
+ initializedObjects.add(0)
+ }
+ }
+
+ private Map<Integer, Map<Class, List<Serializable>>> createAssociationMap() {
+ Map<Integer, Map<Class, List<Serializable>>> associationMap = new HashMap<>()
+ associationMap = associationMap.withDefault { Object o ->
+ Map<Class, List<Serializable>> subMap = new HashMap<>()
+ subMap = subMap.withDefault { Object k ->
+ return new ArrayList<Serializable>()
+ }
+ return subMap
+ }
+ return associationMap
+ }
+
+ @Override
+ Iterator iterator() {
+ if (initialized || containsAssociations || internalIndex > 0) {
+ initializeFully()
+ return initializedObjects.iterator()
+ }
+
+ if (!cursor.hasNext()) {
+ handleNoResults()
+ return initializedObjects.iterator()
+ }
+
+ return new Iterator() {
+ @Override
+ boolean hasNext() {
+ boolean hasMore = cursor.hasNext()
+ if (!hasMore) {
+ initialized = true
+ }
+ return hasMore
+ }
+
+ @Override
+ Object next() {
+ Document dbo = (Document) cursor.next()
+ return addInitializedObject(dbo)
+ }
+
+ @Override
+ void remove() {
+ throw new UnsupportedOperationException('Aggregate result list cannot be mutated.')
+ }
+ }
+ }
+
+ @PackageScope
+ Object addInitializedObject(Document dbo) {
+ if (projectedProperties.size() > 1) {
+ List<Object> projected = new ArrayList<>()
+ for (ProjectedProperty projectedProperty : projectedProperties) {
+ Object value
+ value = getProjectedValue(dbo, projectedProperty.projectionKey)
+ projected.add(value)
+ }
+ initializedObjects.add(internalIndex, projected)
+ internalIndex++
+ return projected
+ }
+ ProjectedProperty projectedProperty = projectedProperties.get(0)
+ Object projected = getProjectedValue(dbo, projectedProperty.projectionKey)
+ initializedObjects.add(internalIndex, projected)
+ internalIndex++
+ return projected
+ }
+
+ private Object getProjectedValue(Document dbo, String projectionKey) {
+ Object value
+ if (projectionKey.startsWith('id.')) {
+ String key = projectionKey.substring(3)
+ Document id = (Document) dbo.get(MongoEntityPersister.MONGO_ID_FIELD)
+ value = id.get(key)
+ } else {
+ value = dbo.get(projectionKey)
+ }
+ return value
+ }
+
+ @Override
+ int size() {
+ initializeFully()
+ return initializedObjects.size()
+ }
+
+ @Override
+ void close() throws IOException {
+ cursor.close()
+ }
+
+ }
+
+ @SuppressWarnings('serial')
+ static class MongoResultList extends AbstractResultList {
+
+ private EntityPersister mongoEntityPersister
+ private MongoCursor cursor
+ private boolean isCodecPersister
+
+ MongoResultList(MongoCursor cursor, Integer offset, EntityPersister mongoEntityPersister) {
+ super(offset == null ? 0 : offset, cursor)
+ this.cursor = cursor
+ this.mongoEntityPersister = mongoEntityPersister
+ this.isCodecPersister = mongoEntityPersister instanceof MongoCodecEntityPersister
+ }
+
+ @Override
+ void close() throws IOException {
+ cursor.close()
+ }
+
+ @Override
+ String toString() {
+ initializeFully()
+ return initializedObjects.toString()
+ }
+
+ /**
+ * @return The underlying MongoDB cursor instance
+ */
+ MongoCursor getCursor() {
+ return cursor
+ }
+
+ @Override
+ protected Object nextDecoded() {
+ final Object o = cursor.next()
+ if (isCodecPersister) {
+ final AbstractMongoSession session = (AbstractMongoSession) mongoEntityPersister.getSession()
+ if (!session.contains(o)) {
+ final PersistentEntity entity = mongoEntityPersister.getPersistentEntity()
+ final EntityAccess entityAccess = session.createEntityAccess(entity, o)
+ final Object id = entityAccess.getIdentifier()
+ if (id != null) {
+ session.cacheInstance(entity.getJavaClass(), (Serializable) id, o)
+ }
+ mongoEntityPersister.firePostLoadEvent(entity, entityAccess)
+ }
+ }
+ return o
+ }
+
+ @Override
+ protected Object convertObject(Object object) {
+ return isCodecPersister ? object : convertDBObject(object)
+ }
+
+ protected Object convertDBObject(Object object) {
+ if (mongoEntityPersister instanceof MongoCodecEntityPersister) {
+ return object
+ }
+ final Document dbObject = (Document) object
+ Object id = dbObject.get(MongoEntityPersister.MONGO_ID_FIELD)
+ SessionImplementor session = (SessionImplementor) mongoEntityPersister.getSession()
+ Class type = mongoEntityPersister.getPersistentEntity().getJavaClass()
+ Object instance = session.getCachedInstance(type, (Serializable) id)
+ if (instance == null) {
+ final MongoEntityPersister mep = (MongoEntityPersister) this.mongoEntityPersister
+ instance = mep.createObjectFromNativeEntry(
+ this.mongoEntityPersister.getPersistentEntity(), (Serializable) id, dbObject)
+ session.cacheInstance(type, (Serializable) id, instance)
+ }
+ return instance
+ }
+
+ }
+
+ static class ProjectedProperty {
+
+ public Projection projection
+ public String projectionKey
+ public PersistentProperty property
+
+ }
+
+ protected static class AggregatePipeline {
+
+ private PersistentEntity entity
+ private Document query
+ private List<Projection> projectionList
+ private List<Document> aggregationPipeline
+ private List<ProjectedProperty> projectedKeys
+ private boolean singleResult
+ private final MongoQuery mongoQuery
+
+ AggregatePipeline(MongoQuery mongoQuery, PersistentEntity entity, Document queryObject, List<Projection> projectionList) {
+ this.mongoQuery = mongoQuery
+ this.entity = entity
+ this.query = queryObject
+ this.projectionList = projectionList
+ }
+
+ List<Document> getAggregationPipeline() {
+ return aggregationPipeline
+ }
+
+ List<ProjectedProperty> getProjectedKeys() {
+ return projectedKeys
+ }
+
+ boolean isSingleResult() {
+ return singleResult
+ }
+
+ AggregatePipeline build() {
+ aggregationPipeline = new ArrayList<Document>()
+
+ if (!query.keySet().isEmpty()) {
+ aggregationPipeline.add(new Document(MATCH_OPERATOR, query))
+ }
+
+ projectedKeys = new ArrayList<ProjectedProperty>()
+ singleResult = true
+
+ Document projectObject = new Document()
+
+ Document groupByObject = new Document()
+ groupByObject.put(MongoEntityPersister.MONGO_ID_FIELD, 0)
+ Document additionalGroupBy = null
+
+ for (Projection projection : projectionList) {
+ ProjectionHandler projectionHandler = projectProjectionHandlers.get(projection.getClass())
+ ProjectedProperty projectedProperty = new ProjectedProperty()
+ projectedProperty.projection = projection
+ if (projection instanceof PropertyProjection) {
+ PropertyProjection propertyProjection = (PropertyProjection) projection
+ String propertyName = propertyProjection.getPropertyName()
+
+ PersistentProperty property = entity.getPropertyByName(propertyName)
+ if (property != null) {
+ projectedProperty.property = property
+ } else if (!propertyName.contains('.')) {
+ throw new InvalidDataAccessResourceUsageException('Attempt to project on a non-existent project [' + propertyName + ']')
+ }
+ }
+ if (projectionHandler != null) {
+ singleResult = false
+
+ String aggregationKey = projectionHandler.handle(entity, projectObject, groupByObject, projection)
+ aggregationKey = 'id.' + aggregationKey
+ projectedProperty.projectionKey = aggregationKey
+ projectedKeys.add(projectedProperty)
+ } else {
+ projectionHandler = groupByProjectionHandlers.get(projection.getClass())
+ if (projectionHandler != null) {
+ projectedProperty.projectionKey = projectionHandler.handle(entity, projectObject, groupByObject, projection)
+ projectedKeys.add(projectedProperty)
+
+ if (projection instanceof CountDistinctProjection) {
+ Document finalCount = new Document(MongoEntityPersister.MONGO_ID_FIELD, 1)
+ finalCount.put(projectedProperty.projectionKey, new Document(SUM_OPERATOR, 1))
+ additionalGroupBy = new Document(GROUP_OPERATOR, finalCount)
+ }
+ }
+ }
+ }
+
+ if (!projectObject.isEmpty()) {
+ aggregationPipeline.add(new Document(PROJECT_OPERATOR, projectObject))
+ }
+
+ aggregationPipeline.add(new Document(GROUP_OPERATOR, groupByObject))
+
+ if (additionalGroupBy != null) {
+ aggregationPipeline.add(additionalGroupBy)
+ }
+
+ List<Order> orderBy = mongoQuery.getOrderBy()
+ if (!orderBy.isEmpty()) {
+ Document sortBy = new Document()
+ for (Order order : orderBy) {
+ String prop = order.getProperty()
+ String sortKey = prop
+ for (ProjectedProperty pp : projectedKeys) {
+ if (pp.property != null && pp.property.getName() == prop) {
+ sortKey = pp.projectionKey
+ if (sortKey.startsWith('id.')) {
+ sortKey = MongoEntityPersister.MONGO_ID_FIELD + '.' + sortKey.substring(3)
+ }
+ break
+ }
+ }
+ sortBy.put(sortKey, order.getDirection() == Order.Direction.ASC ? 1 : -1)
+ }
+ aggregationPipeline.add(new Document(SORT_OPERATOR, sortBy))
+ }
+
+ int max = mongoQuery.getMax() != null ? mongoQuery.getMax() : -1
+ if (max > 0) {
+ aggregationPipeline.add(new Document('$limit', max))
+ }
+ int offset = mongoQuery.getOffset() != null ? mongoQuery.getOffset() : 0
+ if (offset > 0) {
+ aggregationPipeline.add(new Document('$skip', offset))
+ }
+
+ return this
+ }
+
+ }
+
+}
diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.java b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.java
deleted file mode 100644
index d00231eb66..0000000000
--- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.java
+++ /dev/null
@@ -1,1633 +0,0 @@
-/* Copyright (C) 2010-2025 the original author or authors.
- *
- * Licensed 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.mapping.mongo.query;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.Serializable;
-import java.util.AbstractList;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-
-import groovy.lang.Closure;
-import org.codehaus.groovy.runtime.DefaultGroovyMethods;
-
-import com.mongodb.ReadConcern;
-import com.mongodb.client.AggregateIterable;
-import com.mongodb.client.FindIterable;
-import com.mongodb.client.MongoCursor;
-import com.mongodb.client.MongoIterable;
-import org.bson.BsonDocument;
-import org.bson.BsonDocumentReader;
-import org.bson.BsonDocumentWriter;
-import org.bson.Document;
-import org.bson.codecs.configuration.CodecRegistry;
-
-import org.springframework.dao.InvalidDataAccessResourceUsageException;
-import org.springframework.transaction.support.TransactionSynchronizationManager;
-
-import grails.mongodb.geo.Box;
-import grails.mongodb.geo.Circle;
-import grails.mongodb.geo.Distance;
-import grails.mongodb.geo.GeoJSON;
-import grails.mongodb.geo.Point;
-import grails.mongodb.geo.Polygon;
-import grails.mongodb.geo.Shape;
-import grails.mongodb.geo.Sphere;
-import org.grails.datastore.bson.codecs.CodecCustomTypeMarshaller;
-import org.grails.datastore.bson.query.BsonQuery;
-import org.grails.datastore.bson.query.EmbeddedQueryEncoder;
-import org.grails.datastore.gorm.mongo.geo.GeoJSONType;
-import org.grails.datastore.gorm.query.AbstractResultList;
-import org.grails.datastore.mapping.core.SessionImplementor;
-import org.grails.datastore.mapping.engine.EntityAccess;
-import org.grails.datastore.mapping.engine.EntityPersister;
-import org.grails.datastore.mapping.engine.types.CustomTypeMarshaller;
-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.Custom;
-import org.grails.datastore.mapping.model.types.Embedded;
-import org.grails.datastore.mapping.model.types.EmbeddedCollection;
-import org.grails.datastore.mapping.model.types.ToOne;
-import org.grails.datastore.mapping.mongo.AbstractMongoSession;
-import org.grails.datastore.mapping.mongo.MongoCodecSession;
-import org.grails.datastore.mapping.mongo.MongoDatastore;
-import org.grails.datastore.mapping.mongo.config.MongoCollection;
-import org.grails.datastore.mapping.mongo.engine.MongoCodecEntityPersister;
-import org.grails.datastore.mapping.mongo.engine.MongoEntityPersister;
-import org.grails.datastore.mapping.mongo.engine.MongoIdCoercion;
-import org.grails.datastore.mapping.mongo.engine.codecs.PersistentEntityCodec;
-import org.grails.datastore.mapping.query.AssociationQuery;
-import org.grails.datastore.mapping.query.Query;
-import org.grails.datastore.mapping.query.api.QueryArgumentsAware;
-import org.grails.datastore.mapping.query.projections.ManualProjections;
-
-/**
- * A {@link org.grails.datastore.mapping.query.Query} implementation for the Mongo document store.
- *
- * @author Graeme Rocher
- * @since 1.0
- */
-@SuppressWarnings("rawtypes")
-public class MongoQuery extends BsonQuery implements QueryArgumentsAware {
-
- public static final String MONGO_IN_OPERATOR = IN_OPERATOR;
- public static final String MONGO_OR_OPERATOR = OR_OPERATOR;
- public static final String MONGO_AND_OPERATOR = AND_OPERATOR;
- public static final String MONGO_GTE_OPERATOR = GTE_OPERATOR;
- public static final String MONGO_LTE_OPERATOR = LTE_OPERATOR;
- public static final String MONGO_GT_OPERATOR = GT_OPERATOR;
- public static final String MONGO_LT_OPERATOR = LT_OPERATOR;
- public static final String MONGO_NE_OPERATOR = NE_OPERATOR;
- public static final String MONGO_NIN_OPERATOR = NIN_OPERATOR;
- public static final String MONGO_REGEX_OPERATOR = REGEX_OPERATOR;
-
- public static final String MONGO_WHERE_OPERATOR = WHERE_OPERATOR;
-
- public static final String HINT_ARGUMENT = "hint";
- public static final String READ_CONCERN_ARGUMENT = "readConcern";
-
- private Map queryArguments = Collections.emptyMap();
-
- public static final String NEAR_OPERATOR = "$near";
-
- public static final String BOX_OPERATOR = "$box";
-
- public static final String POLYGON_OPERATOR = "$polygon";
-
- public static final String WITHIN_OPERATOR = "$within";
-
- public static final String CENTER_OPERATOR = "$center";
-
- public static final String GEO_WITHIN_OPERATOR = "$geoWithin";
-
- public static final String GEOMETRY_OPERATOR = "$geometry";
-
- public static final String CENTER_SPHERE_OPERATOR = "$centerSphere";
-
- public static final String GEO_INTERSECTS_OPERATOR = "$geoIntersects";
-
- public static final String MAX_DISTANCE_OPERATOR = "$maxDistance";
-
- public static final String NEAR_SPHERE_OPERATOR = "$nearSphere";
-
- static {
- queryHandlers.put(IdEquals.class, new QueryHandler<IdEquals>() {
- // Exercised end-to-end by StringIdWithObjectIdStorageSpec:
- // - "with storedAs ObjectId, point lookup by hex string works" (happy path)
- // - "with storedAs ObjectId, point lookup of a non-hex id matches the
- // BSON String the encoder wrote" (null-return fallback for natural keys)
- // - "with storedAs ObjectId, updates persist (no phantom OptimisticLockingException)"
- // and related update/delete specs (which also hit IdEquals via the session filter)
- public void handle(EmbeddedQueryEncoder queryEncoder, IdEquals criterion, Document query, PersistentEntity entity) {
- Object value = criterion.getValue();
- // Prefer the configured storage type ('storedAs' on the id mapping) so query BSON
- // matches what's actually on disk. Falls back to the declared Java type otherwise.
- Class<?> storedAs = MongoIdCoercion.resolveStoredAs(entity);
- Class<?> targetType = storedAs != null ? storedAs : entity.getIdentity().getType();
- Object converted = entity.getMappingContext().getConversionService().convert(value, targetType);
- // Symmetry with IdentityEncoder's non-hex fallback: if the converter returns
- // null for a non-null input (e.g. a natural-key String being converted to
- // ObjectId), keep the original value so the query targets what the encoder
- // actually wrote rather than {_id: null}.
- if (converted == null && value != null) {
- converted = value;
- }
- query.put(MongoEntityPersister.MONGO_ID_FIELD, converted);
- }
- });
-
- // Override the In handler so that criteria targeting the identity (findAllByIdInList,
- // Domain.createCriteria().list { 'in'('id', [...]) }, etc.) honor 'storedAs' the same way
- // IdEquals does. Without this, a domain declaring storedAs: ObjectId would send BSON Strings
- // in {_id: {$in: [...]}} and miss all stored ObjectId documents.
- //
- // Exercised end-to-end by StringIdWithObjectIdStorageSpec:
- // - "with storedAs ObjectId, findAllByIdInList resolves all ids" (happy path via dynamic finder)
- // - "with storedAs ObjectId, criteria in('id', [...]) resolves all ids" (happy path via createCriteria)
- // - "with storedAs ObjectId, batch getAll with non-hex ids falls back to BSON String in the in-list"
- // (null-return fallback for natural keys)
- queryHandlers.put(In.class, new QueryHandler<In>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, In in, Document query, PersistentEntity entity) {
- Document inQuery = new Document();
- List<Object> values = getInListQueryValues(entity, in);
-
- PersistentProperty identityProp = entity.getIdentity();
- boolean isIdInList = identityProp != null && identityProp.getName().equals(in.getProperty());
- if (isIdInList && MongoIdCoercion.resolveStoredAs(entity) != null) {
- List<Object> coerced = new ArrayList<>(values.size());
- for (Object v : values) {
- coerced.add(MongoIdCoercion.coerceIdToStoredType(v, entity));
- }
- values = coerced;
- }
-
- inQuery.put(IN_OPERATOR, values);
- String propertyName = getPropertyName(entity, in);
- query.put(propertyName, inQuery);
- }
- });
-
- queryHandlers.put(AssociationQuery.class, new QueryHandler<AssociationQuery>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, AssociationQuery criterion, Document query, PersistentEntity entity) {
- Association<?> association = criterion.getAssociation();
- PersistentEntity associatedEntity = association.getAssociatedEntity();
- if (association instanceof EmbeddedCollection) {
- Document associationCollectionQuery = new Document();
- populateMongoQuery(queryEncoder, associationCollectionQuery, criterion.getCriteria(), associatedEntity);
- Document collectionQuery = new Document("$elemMatch", associationCollectionQuery);
- String propertyKey = getPropertyName(entity, association.getName());
- query.put(propertyKey, collectionQuery);
- } else if (associatedEntity instanceof EmbeddedPersistentEntity || association instanceof Embedded) {
- Document associatedEntityQuery = new Document();
- populateMongoQuery(queryEncoder, associatedEntityQuery, criterion.getCriteria(), associatedEntity);
- String propertyKey = getPropertyName(entity, association.getName());
- prefixEmbeddedQuery(propertyKey, associatedEntityQuery, query);
- } else {
- throw new UnsupportedOperationException("Join queries are not supported by MongoDB");
- }
- }
- });
-
- queryHandlers.put(WithinBox.class, new QueryHandler<WithinBox>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, WithinBox withinBox, Document query, PersistentEntity entity) {
- Document nearQuery = new Document();
- Document box = new Document();
- MongoEntityPersister.setDBObjectValue(box, BOX_OPERATOR, withinBox.getValues(), entity.getMappingContext());
- nearQuery.put(WITHIN_OPERATOR, box);
- String propertyName = getPropertyName(entity, withinBox);
- query.put(propertyName, nearQuery);
- }
- });
-
- queryHandlers.put(WithinPolygon.class, new QueryHandler<WithinPolygon>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, WithinPolygon withinPolygon, Document query, PersistentEntity entity) {
- Document nearQuery = new Document();
- Document box = new Document();
- MongoEntityPersister.setDBObjectValue(box, POLYGON_OPERATOR, withinPolygon.getValues(), entity.getMappingContext());
- nearQuery.put(WITHIN_OPERATOR, box);
- String propertyName = getPropertyName(entity, withinPolygon);
- query.put(propertyName, nearQuery);
- }
- });
-
- queryHandlers.put(WithinCircle.class, new QueryHandler<WithinCircle>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, WithinCircle withinCentre, Document query, PersistentEntity entity) {
- Document nearQuery = new Document();
- Document center = new Document();
- MongoEntityPersister.setDBObjectValue(center, CENTER_OPERATOR, withinCentre.getValues(), entity.getMappingContext());
- nearQuery.put(WITHIN_OPERATOR, center);
- String propertyName = getPropertyName(entity, withinCentre);
- query.put(propertyName, nearQuery);
- }
- });
-
- QueryHandler<Near> nearHandler = new QueryHandler<>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, Near near, Document query, PersistentEntity entity) {
- Document nearQuery = new Document();
- Object value = near.getValue();
- String nearOperator = near instanceof NearSphere ? NEAR_SPHERE_OPERATOR : NEAR_OPERATOR;
- if ((value instanceof List) || (value instanceof Map)) {
- MongoEntityPersister.setDBObjectValue(nearQuery, nearOperator, value, entity.getMappingContext());
- } else if (value instanceof Point) {
- Document geoJson = GeoJSONType.convertToGeoDocument((Point) value);
- Document geometry = new Document();
- geometry.put(GEOMETRY_OPERATOR, geoJson);
- if (near.maxDistance != null) {
- geometry.put(MAX_DISTANCE_OPERATOR, near.maxDistance.getValue());
- }
- nearQuery.put(nearOperator, geometry);
- }
-
- String propertyName = getPropertyName(entity, near);
- query.put(propertyName, nearQuery);
- }
- };
- queryHandlers.put(Near.class, nearHandler);
- queryHandlers.put(NearSphere.class, nearHandler);
-
- queryHandlers.put(GeoWithin.class, new QueryHandler<GeoWithin>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, GeoWithin geoWithin, Document query, PersistentEntity entity) {
- Document queryRoot = new Document();
- Document queryGeoWithin = new Document();
- queryRoot.put(GEO_WITHIN_OPERATOR, queryGeoWithin);
- String targetProperty = getPropertyName(entity, geoWithin);
- Object value = geoWithin.getValue();
- if (value instanceof Shape) {
- Shape shape = (Shape) value;
- if (shape instanceof Polygon) {
- Polygon p = (Polygon) shape;
- Document geoJson = GeoJSONType.convertToGeoDocument(p);
- queryGeoWithin.put(GEOMETRY_OPERATOR, geoJson);
- } else if (shape instanceof Box) {
- queryGeoWithin.put(BOX_OPERATOR, shape.asList());
- } else if (shape instanceof Circle) {
- queryGeoWithin.put(CENTER_OPERATOR, shape.asList());
- } else if (shape instanceof Sphere) {
- queryGeoWithin.put(CENTER_SPHERE_OPERATOR, shape.asList());
- }
- } else if (value instanceof Map) {
- queryGeoWithin.putAll((Map) value);
- }
-
- query.put(targetProperty, queryRoot);
- }
- });
-
- queryHandlers.put(GeoIntersects.class, new QueryHandler<GeoIntersects>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, GeoIntersects geoIntersects, Document query, PersistentEntity entity) {
- Document queryRoot = new Document();
- Document queryGeoWithin = new Document();
- queryRoot.put(GEO_INTERSECTS_OPERATOR, queryGeoWithin);
- String targetProperty = getPropertyName(entity, geoIntersects);
- Object value = geoIntersects.getValue();
- if (value instanceof GeoJSON) {
- Shape shape = (Shape) value;
- Document geoJson = GeoJSONType.convertToGeoDocument(shape);
- queryGeoWithin.put(GEOMETRY_OPERATOR, geoJson);
- } else if (value instanceof Map) {
- queryGeoWithin.putAll((Map) value);
- }
-
- query.put(targetProperty, queryRoot);
- }
- });
- queryHandlers.put(Conjunction.class, new QueryHandler<Conjunction>() {
- public void handle(EmbeddedQueryEncoder queryEncoder, Conjunction criterion, Document query, PersistentEntity entity) {
- populateMongoQuery(queryEncoder, query, criterion, entity);
- }
- });
-
- queryHandlers.put(Disjunction.class, new QueryHandler<Disjunction>() {
- @SuppressWarnings("unchecked")
- public void handle(EmbeddedQueryEncoder queryEncoder, Disjunction criterion, Document query, PersistentEntity entity) {
- populateMongoQuery(queryEncoder, query, criterion, entity);
- }
- });
-
- groupByProjectionHandlers.put(AvgProjection.class, new ProjectionHandler<AvgProjection>() {
- @Override
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, AvgProjection projection) {
- return addProjectionToGroupBy(projectObject, groupBy, projection, AVERAGE_OPERATOR, "avg_");
- }
- });
- groupByProjectionHandlers.put(CountProjection.class, new ProjectionHandler<CountProjection>() {
- @Override
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, CountProjection projection) {
- projectObject.put(MongoEntityPersister.MONGO_ID_FIELD, 1);
- String projectionKey = "count";
- groupBy.put(projectionKey, new Document(SUM_OPERATOR, 1));
- return projectionKey;
- }
- });
- groupByProjectionHandlers.put(CountDistinctProjection.class, new ProjectionHandler<CountDistinctProjection>() {
- @Override
- // equivalent of "select count (distinct fieldName) from someTable". Example:
- // db.someCollection.aggregate([{ $group: { _id: "$fieldName"} },{ $group: { _id: 1, count: { $sum: 1 } } } ])
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, CountDistinctProjection projection) {
- projectObject.put(projection.getPropertyName(), 1);
- String property = projection.getPropertyName();
- String projectionValueKey = "countDistinct_" + property;
- Document id = getIdObjectForGroupBy(groupBy);
- id.put(projectionValueKey, "$" + property);
- return projectionValueKey;
- }
- });
-
- groupByProjectionHandlers.put(MinProjection.class, new ProjectionHandler<MinProjection>() {
- @Override
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, MinProjection projection) {
- return addProjectionToGroupBy(projectObject, groupBy, projection, MIN_OPERATOR, "min_");
- }
- });
- groupByProjectionHandlers.put(MaxProjection.class, new ProjectionHandler<MaxProjection>() {
- @Override
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, MaxProjection projection) {
- return addProjectionToGroupBy(projectObject, groupBy, projection, MAX_OPERATOR, "max_");
- }
- });
- groupByProjectionHandlers.put(SumProjection.class, new ProjectionHandler<SumProjection>() {
- @Override
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, SumProjection projection) {
- return addProjectionToGroupBy(projectObject, groupBy, projection, SUM_OPERATOR, "sum_");
- }
- });
-
- projectProjectionHandlers.put(DistinctPropertyProjection.class, new ProjectionHandler<DistinctPropertyProjection>() {
- @Override
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, DistinctPropertyProjection projection) {
- String property = projection.getPropertyName();
- projectObject.put(property, 1);
- Document id = getIdObjectForGroupBy(groupBy);
- String projectedValueKey = property.replace('.', '_');
- id.put(projectedValueKey, "$" + property);
- return projectedValueKey;
- }
- });
-
- projectProjectionHandlers.put(PropertyProjection.class, new ProjectionHandler<PropertyProjection>() {
- @Override
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, PropertyProjection projection) {
- String property = projection.getPropertyName();
- projectObject.put(property, 1);
- Document id = getIdObjectForGroupBy(groupBy);
- String projectedValueKey = property.replace('.', '_');
- id.put(projectedValueKey, "$" + property);
- // we add the id to the grouping to make it not distinct
- id.put(MongoEntityPersister.MONGO_ID_FIELD, "$" + MongoEntityPersister.MONGO_ID_FIELD);
- return projectedValueKey;
- }
- });
-
- projectProjectionHandlers.put(IdProjection.class, new ProjectionHandler<IdProjection>() {
- @Override
- public String handle(PersistentEntity entity, Document projectObject, Document groupBy, IdProjection projection) {
- projectObject.put(MongoEntityPersister.MONGO_ID_FIELD, 1);
- Document id = getIdObjectForGroupBy(groupBy);
- id.put(MongoEntityPersister.MONGO_ID_FIELD, "$_id");
-
- return MongoEntityPersister.MONGO_ID_FIELD;
- }
- });
-
- }
-
- private static Document getIdObjectForGroupBy(Document groupBy) {
- Object value = groupBy.get(MongoEntityPersister.MONGO_ID_FIELD);
- Document id;
- if (value instanceof Document) {
- id = (Document) value;
- } else {
- id = new Document();
- groupBy.put(MongoEntityPersister.MONGO_ID_FIELD, id);
- }
- return id;
- }
-
- private static String addProjectionToGroupBy(Document projectObject, Document groupBy, PropertyProjection projection, String operator, String prefix) {
- projectObject.put(projection.getPropertyName(), 1);
- String property = projection.getPropertyName();
- String projectionValueKey = prefix + property.replace('.', '_');
- Document averageProjection = new Document(operator, "$" + property);
- groupBy.put(projectionValueKey, averageProjection);
- return projectionValueKey;
- }
-
- private final AbstractMongoSession mongoSession;
- private final EntityPersister mongoEntityPersister;
- private final ManualProjections manualProjections;
- private boolean isCodecPersister = false;
-
- public MongoQuery(AbstractMongoSession session, PersistentEntity entity) {
- super(session, entity);
- this.mongoSession = session;
- this.manualProjections = new ManualProjections(entity);
- if (session != null) {
-
- this.mongoEntityPersister = (EntityPersister) session.getPersister(entity);
- if (this.mongoEntityPersister instanceof MongoCodecEntityPersister) {
- this.isCodecPersister = true;
- }
- }
- else {
- mongoEntityPersister = null;
- }
- }
-
- @Override
- protected void flushBeforeQuery() {
- // with Mongo we only flush the session if a transaction is not active to allow for session-managed transactions
- if (!TransactionSynchronizationManager.isSynchronizationActive()) {
- super.flushBeforeQuery();
- }
- }
-
- /**
- * Gets the Mongo query for this query instance
- *
- * @return The Mongo query
- */
- public Document getMongoQuery() {
- Document query = createQueryObject(entity);
- populateMongoQuery((AbstractMongoSession) getSession(), query, criteria, entity);
- return query;
- }
-
- @Override
- protected List executeQuery(final PersistentEntity entity, final Junction criteria) {
- final AbstractMongoSession mongoSession = this.mongoSession;
- com.mongodb.client.MongoCollection<Document> collection = mongoSession.getCollection(entity);
-
- final List<Projection> projectionList = projections().getProjectionList();
- boolean hasOnlyDistinct = projectionList.size() == 1 && (projectionList.get(0) instanceof DistinctProjection);
- if (uniqueResult && (projectionList.isEmpty() || hasOnlyDistinct)) {
- if (isCodecPersister) {
- collection = (com.mongodb.client.MongoCollection<Document>) (com.mongodb.client.MongoCollection) collection
- .withDocumentClass(entity.getJavaClass());
- }
- final Object dbObject;
- if (criteria.isEmpty()) {
- FindIterable<Document> cursor = mongoSession.find(collection, createQueryObject(entity));
-
- dbObject = ((FindIterable<Document>) setHint(cursor)).limit(1)
- .first();
- } else {
- FindIterable<Document> cursor = mongoSession.find(collection, getMongoQuery());
-
- dbObject = ((FindIterable<Document>) setHint(cursor)).limit(1)
- .first();
- }
- if (dbObject == null) {
- return wrapObjectResultInList(dbObject);
- }
- if (isCodecPersister) {
- if (!mongoSession.contains(dbObject)) {
- final EntityAccess entityAccess = mongoSession.createEntityAccess(entity, dbObject);
- mongoEntityPersister.firePostLoadEvent(entity, entityAccess);
- mongoSession.cacheInstance(dbObject.getClass(), (Serializable) entityAccess.getIdentifier(), dbObject);
- }
- return wrapObjectResultInList(dbObject);
- }
- else {
- return wrapObjectResultInList(createObjectFromDBObject((Document) dbObject));
- }
- }
-
- MongoCursor<Document> cursor;
- Document query = createQueryObject(entity);
-
- if (projectionList.isEmpty() || hasOnlyDistinct) {
- if (isCodecPersister) {
- collection = (com.mongodb.client.MongoCollection<Document>) (com.mongodb.client.MongoCollection) collection
- .withDocumentClass(entity.getJavaClass())
- .withCodecRegistry(mongoSession.getDatastore().getCodecRegistry());
- }
- cursor = executeQuery(entity, criteria, collection, query);
- return new MongoResultList(cursor, offset, mongoEntityPersister);
- }
-
- populateMongoQuery((AbstractMongoSession) session, query, criteria, entity);
- AggregatePipeline aggregatePipeline = buildAggregatePipeline(entity, query, projectionList);
- List<Document> aggregationPipeline = aggregatePipeline.getAggregationPipeline();
- boolean singleResult = aggregatePipeline.isSingleResult();
- List<ProjectedProperty> projectedKeys = aggregatePipeline.getProjectedKeys();
- List projectedResults = new ArrayList();
-
- AggregateIterable<Document> aggregatedResults = mongoSession.aggregate(collection, aggregationPipeline);
- aggregatedResults = (AggregateIterable<Document>) setHint(aggregatedResults);
- final MongoCursor<Document> aggregateCursor = aggregatedResults.iterator();
-
- if (singleResult && aggregateCursor.hasNext()) {
- Document dbo = aggregateCursor.next();
- for (ProjectedProperty projectedProperty : projectedKeys) {
- Object value = dbo.get(projectedProperty.projectionKey);
- PersistentProperty property = projectedProperty.property;
- if (value != null) {
- if (property instanceof ToOne) {
- projectedResults.add(session.retrieve(property.getType(), (Serializable) value));
- } else {
- projectedResults.add(value);
- }
- } else {
- if (projectedProperty.projection instanceof CountProjection) {
- projectedResults.add(0);
- }
- }
- }
- } else {
- return new AggregatedResultList((AbstractMongoSession) getSession(), aggregateCursor, projectedKeys);
- }
-
- return projectedResults;
-
- }
-
- protected AggregatePipeline buildAggregatePipeline(PersistentEntity entity, Document query, List<Projection> projectionList) {
- return new AggregatePipeline(this, entity, query, projectionList).build();
- }
-
- protected MongoCursor<Document> executeQuery(final PersistentEntity entity,
- final Junction criteria,
- final com.mongodb.client.MongoCollection<Document> collection,
- final Document query) {
- FindIterable<Document> cursor;
- if (criteria.isEmpty()) {
- cursor = executeQueryAndApplyPagination(collection, query);
- } else {
- populateMongoQuery((AbstractMongoSession) session, query, criteria, entity);
- cursor = executeQueryAndApplyPagination(collection, query);
- }
-
- cursor = (FindIterable<Document>) setHint(cursor);
-
- return cursor.iterator();
- }
-
- private MongoIterable<Document> setHint(MongoIterable<Document> cursor) {
- MongoIterable<Document> result = cursor;
-
- if (queryArguments != null) {
- if (queryArguments.containsKey(HINT_ARGUMENT)) {
- Object hint = queryArguments.get(HINT_ARGUMENT);
- if (hint instanceof Map) {
- if (cursor instanceof FindIterable) {
- result = ((FindIterable) cursor).hint(new Document((Map<String, Object>) hint));
- } else if (cursor instanceof AggregateIterable) {
- result = ((AggregateIterable) cursor).hint(new Document((Map<String, Object>) hint));
- }
- } else {
- if (cursor instanceof FindIterable) {
- result = ((FindIterable) cursor).hintString(hint.toString());
- } else if (cursor instanceof AggregateIterable) {
- result = ((AggregateIterable) cursor).hintString(hint.toString());
- }
- }
- }
- }
-
- return result;
- }
-
- protected FindIterable<Document> executeQueryAndApplyPagination(com.mongodb.client.MongoCollection<Document> collection, Document query) {
- Object readConcernObject = queryArguments.get(READ_CONCERN_ARGUMENT);
- if (readConcernObject instanceof ReadConcern) {
- collection = collection.withReadConcern(
- (ReadConcern) readConcernObject
- );
- }
-
- final FindIterable<Document> iterable = mongoSession.find(collection, query);
- if (offset != null && offset > 0) {
- iterable.skip(offset);
- }
- if (max != null && max > -1) {
- iterable.limit(max);
- }
- if (uniqueResult) {
- iterable.limit(1);
- }
-
- if (!orderBy.isEmpty()) {
- Document orderObject = new Document();
- for (Order order : orderBy) {
- String property = order.getProperty();
- property = getPropertyName(entity, property);
- orderObject.put(property, order.getDirection() == Order.Direction.DESC ? -1 : 1);
- }
- iterable.sort(orderObject);
- } else {
- MongoCollection coll = (MongoCollection) entity.getMapping().getMappedForm();
- if (coll != null && coll.getSort() != null) {
- Document orderObject = new Document();
- Order order = coll.getSort();
- String property = order.getProperty();
- property = getPropertyName(entity, property);
- orderObject.put(property, order.getDirection() == Order.Direction.DESC ? -1 : 1);
- iterable.sort(orderObject);
- }
- }
-
- return iterable;
- }
-
- private Document getClassFieldDocument(final PersistentEntity entity) {
- Object classFieldValue;
- Collection<PersistentEntity> childEntities = entity.getMappingContext().getChildEntities(entity);
- if (childEntities.size() > 0) {
- HashMap classValue = new HashMap<>();
- ArrayList classes = new ArrayList<>();
- classes.add(entity.getDiscriminator());
- for (PersistentEntity childEntity: childEntities) {
- classes.add(childEntity.getDiscriminator());
- }
- classValue.put(MONGO_IN_OPERATOR, classes);
- classFieldValue = classValue;
- } else {
- classFieldValue = entity.getDiscriminator();
- }
- return new Document(MongoEntityPersister.MONGO_CLASS_FIELD, classFieldValue);
- }
-
- protected Document createQueryObject(PersistentEntity persistentEntity) {
- Document query;
- if (persistentEntity.isRoot()) {
- query = new Document();
- } else {
- query = getClassFieldDocument(persistentEntity);
- }
- return query;
- }
-
- public static void populateMongoQuery(final AbstractMongoSession session, Document query, Junction criteria, final PersistentEntity entity) {
- EmbeddedQueryEncoder queryEncoder;
- if (session instanceof MongoCodecSession) {
- final MongoDatastore datastore = session.getDatastore();
- final CodecRegistry codecRegistry = datastore.getCodecRegistry();
- queryEncoder = new EmbeddedQueryEncoder() {
- @Override
- public Object encode(Embedded embedded, Object instance) {
- final PersistentEntityCodec codec = (PersistentEntityCodec) codecRegistry.get(embedded.getType());
- final BsonDocument doc = new BsonDocument();
- codec.encode(new BsonDocumentWriter(doc), instance, ENCODER_CONTEXT, false);
- return doc;
- }
- };
- }
- else {
- queryEncoder = new EmbeddedQueryEncoder() {
- @Override
- public Object encode(Embedded embedded, Object instance) {
- MongoEntityPersister persister = (MongoEntityPersister) session.getPersister(entity.getJavaClass());
- return persister.createNativeObjectForEmbedded(embedded, instance);
-
- }
- };
- }
-
- populateMongoQuery(queryEncoder, query, criteria, entity);
- }
-
- @SuppressWarnings("unchecked")
- public static void populateMongoQuery(final EmbeddedQueryEncoder queryEncoder, Document query, Junction criteria, final PersistentEntity entity) {
- List subList = null;
- // if a query combines more than 1 item, wrap the items in individual $and or $or arguments
- // so that property names can't clash (e.g. for an $and containing two $ors)
- if (criteria.getCriteria().size() > 1) {
- if (criteria instanceof Disjunction) {
- subList = new ArrayList();
- query.put(OR_OPERATOR, subList);
- } else if (criteria instanceof Conjunction) {
- subList = new ArrayList();
- query.put(AND_OPERATOR, subList);
- }
- }
- for (Criterion criterion : criteria.getCriteria()) {
- final QueryHandler queryHandler = queryHandlers.get(criterion.getClass());
- if (queryHandler != null) {
- Document dbo = query;
- if (subList != null) {
- dbo = new Document();
- subList.add(dbo);
- }
-
- if (criterion instanceof PropertyCriterion && !(criterion instanceof GeoCriterion)) {
- PropertyCriterion pc = (PropertyCriterion) criterion;
- PersistentProperty property = entity.getPropertyByName(pc.getProperty());
- if (property instanceof Custom) {
- CustomTypeMarshaller customTypeMarshaller = ((Custom) property).getCustomTypeMarshaller();
- if (!(customTypeMarshaller instanceof CodecCustomTypeMarshaller)) {
- customTypeMarshaller.query(property, pc, query);
- continue;
- }
- }
- }
- queryHandler.handle(queryEncoder, criterion, dbo, entity);
- } else {
- throw new InvalidDataAccessResourceUsageException("Queries of type " + criterion.getClass().getSimpleName() + " are not supported by this implementation");
- }
- }
- }
-
- /**
- * Rewrites a query built against an embedded entity so it applies to the owning document,
- * qualifying each property name with the embedded property's path. Logical operators such as
- * {@code $and} and {@code $or} must stay at the current level (a key like {@code extRef1.$and}
- * matches nothing), so their nested documents are rewritten recursively instead.
- */
- private static void prefixEmbeddedQuery(String prefix, Document source, Document target) {
- for (String key : source.keySet()) {
- Object value = source.get(key);
- if (key.charAt(0) == '$') {
- if (!(value instanceof List)) {
- // A top-level operator whose value is not a rewritable list of clauses
- // (e.g. $where from a property-to-property comparison, or $text) cannot be
- // qualified with the embedded path - prefixing it would silently match nothing.
- throw new UnsupportedOperationException("Criterion [" + key +
- "] is not supported inside an embedded association query");
- }
- List<Object> rewritten = new ArrayList<>();
- for (Object element : (List<?>) value) {
- if (element instanceof Document) {
- Document rewrittenElement = new Document();
- prefixEmbeddedQuery(prefix, (Document) element, rewrittenElement);
- rewritten.add(rewrittenElement);
- } else {
- rewritten.add(element);
- }
- }
- target.put(key, rewritten);
- } else {
- target.put(prefix + '.' + key, value);
- }
- }
- }
-
- private Object createObjectFromDBObject(Document dbObject) {
- // we always use the session cached version where available.
- final Object id = dbObject.get(MongoEntityPersister.MONGO_ID_FIELD);
- Class type = mongoEntityPersister.getPersistentEntity().getJavaClass();
- Object instance = mongoSession.getCachedInstance(type, (Serializable) id);
- if (instance == null) {
- instance = ((MongoEntityPersister) mongoEntityPersister).createObjectFromNativeEntry(
- mongoEntityPersister.getPersistentEntity(), (Serializable) id, dbObject);
- mongoSession.cacheInstance(type, (Serializable) id, instance);
- }
- // note cached instances may be stale, but user can call 'refresh' to fix that.
- return instance;
- }
-
- @SuppressWarnings("unchecked")
- private List wrapObjectResultInList(Object object) {
- List result = new ArrayList();
- result.add(object);
- return result;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query near(String property, List value) {
- add(new Near(property, value));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query near(String property, Point value) {
- add(new Near(property, value));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query near(String property, List value, Distance maxDistance) {
- add(new Near(property, value, maxDistance));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query near(String property, Point value, Distance maxDistance) {
- add(new Near(property, value, maxDistance));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query near(String property, List value, Number maxDistance) {
- add(new Near(property, value, maxDistance));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query near(String property, Point value, Number maxDistance) {
- add(new Near(property, value, maxDistance));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query nearSphere(String property, List value) {
- add(new NearSphere(property, value));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query nearSphere(String property, Point value) {
- add(new NearSphere(property, value));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query nearSphere(String property, List value, Distance maxDistance) {
- add(new NearSphere(property, value, maxDistance));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query nearSphere(String property, Point value, Distance maxDistance) {
- add(new NearSphere(property, value, maxDistance));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query nearSphere(String property, List value, Number maxDistance) {
- add(new NearSphere(property, value, maxDistance));
- return this;
- }
-
- /**
- * Geospacial query for values near the given two dimensional list
- *
- * @param property The property
- * @param value A two dimensional list of values
- * @return this
- */
- public Query nearSphere(String property, Point value, Number maxDistance) {
- add(new NearSphere(property, value, maxDistance));
- return this;
- }
-
- /**
- * Geospacial query for values within a given box. A box is defined as a multi-dimensional list in the form
- * [[40.73083, -73.99756], [40.741404, -73.988135]]
- *
- * @param property The property
- * @param value A multi-dimensional list of values
- * @return This query
- */
- public Query withinBox(String property, List value) {
- add(new WithinBox(property, value));
- return this;
- }
-
- /**
- * Geospacial query for values within the given shape
- *
- * @param property The property
- * @param shape The shape
- * @return The query instance
- */
- public Query geoWithin(String property, Shape shape) {
- add(new GeoWithin(property, shape));
- return this;
- }
-
- /**
- * Geospacial query for values within the given shape
- *
- * @param property The property
- * @param shape The shape
- * @return The query instance
- */
- public Query geoIntersects(String property, GeoJSON shape) {
- add(new GeoIntersects(property, shape));
- return this;
- }
-
- /**
- * Geospacial query for values within a given polygon. A polygon is defined as a multi-dimensional list in the form
- * [[0, 0], [3, 6], [6, 0]]
- *
- * @param property The property
- * @param value A multi-dimensional list of values
- * @return This query
- */
- public Query withinPolygon(String property, List value) {
- add(new WithinPolygon(property, value));
- return this;
- }
-
- /**
- * Geospacial query for values within a given circle. A circle is defined as a multi-dimensial list containing the position of the center and the radius:
- * [[50, 50], 10]
- *
- * @param property The property
- * @param value A multi-dimensional list of values
- * @return This query
- */
- public Query withinCircle(String property, List value) {
- add(new WithinBox(property, value));
- return this;
- }
-
- /**
- * @param arguments The query arguments
- */
- public void setArguments(Map arguments) {
- this.queryArguments = arguments;
- }
-
- /**
- * Used for Geospacial querying
- *
- * @author Graeme Rocher
- * @since 1.0
- */
- public static class Near extends GeoCriterion {
-
- Distance maxDistance = null;
-
- public Near(String name, Object value) {
- super(name, value);
- }
-
- public Near(String name, Object value, Distance maxDistance) {
- super(name, value);
- this.maxDistance = maxDistance;
- }
-
- public Near(String name, Object value, Number maxDistance) {
- super(name, value);
- this.maxDistance = Distance.valueOf(maxDistance.doubleValue());
- }
-
- public void setMaxDistance(Distance maxDistance) {
- this.maxDistance = maxDistance;
- }
- }
-
- /**
- * Used for Geospacial querying with the $nearSphere operator
- *
- * @author Graeme Rocher
- * @since 1.0
- */
- public static class NearSphere extends Near {
- public NearSphere(String name, Object value) {
- super(name, value);
- }
-
- public NearSphere(String name, Object value, Distance maxDistance) {
- super(name, value, maxDistance);
- }
-
- public NearSphere(String name, Object value, Number maxDistance) {
- super(name, value, maxDistance);
- }
- }
-
- /**
- * Used for Geospacial querying of boxes
- *
- * @author Graeme Rocher
- * @since 1.0
- */
- public static class WithinBox extends PropertyCriterion {
-
- public WithinBox(String name, List value) {
- super(name, value);
- }
-
- public List getValues() {
- return (List) getValue();
- }
-
- public void setValue(List matrix) {
- this.value = matrix;
- }
- }
-
- /**
- * Used for Geospacial querying of polygons
- */
- public static class WithinPolygon extends PropertyCriterion {
-
- public WithinPolygon(String name, List value) {
- super(name, value);
- }
-
- public List getValues() {
- return (List) getValue();
- }
-
- public void setValue(List value) {
- this.value = value;
- }
- }
-
- /**
- * Used for Geospacial querying of circles
- *
- * @author Graeme Rocher
- * @since 1.0
- */
- public static class WithinCircle extends PropertyCriterion {
-
- public WithinCircle(String name, List value) {
- super(name, value);
- }
-
- public List getValues() {
- return (List) getValue();
- }
-
- public void setValue(List matrix) {
- this.value = matrix;
- }
- }
-
- /**
- * Used for all GeoSpacial queries using 2dsphere indexes
- */
- public static class GeoCriterion extends PropertyCriterion {
-
- public GeoCriterion(String name, Object value) {
- super(name, value);
- }
- }
-
- public static class GeoWithin extends GeoCriterion {
- public GeoWithin(String name, Object value) {
- super(name, value);
- }
- }
-
- public static class GeoIntersects extends GeoCriterion {
- public GeoIntersects(String name, Object value) {
- super(name, value);
- }
- }
-
- public static class AggregatedResultList extends AbstractList implements Closeable {
-
- private MongoCursor cursor;
- private List<ProjectedProperty> projectedProperties;
- private List initializedObjects = new ArrayList();
- private int internalIndex = 0;
- private boolean initialized = false;
- private boolean containsAssociations = false;
- private AbstractMongoSession session;
-
- public AggregatedResultList(AbstractMongoSession session, MongoCursor<Document> cursor, List<ProjectedProperty> projectedProperties) {
- this.cursor = cursor;
- this.projectedProperties = projectedProperties;
- this.session = session;
- for (ProjectedProperty projectedProperty : projectedProperties) {
- if (projectedProperty.property instanceof Association) {
- this.containsAssociations = true;
- break;
- }
- }
- }
-
- @Override
- public String toString() {
- return initializedObjects.toString();
- }
-
- @Override
- public Object get(int index) {
- if (containsAssociations) initializeFully();
- if (initializedObjects.size() > index) {
- return initializedObjects.get(index);
- } else if (!initialized) {
- boolean hasResults = false;
- while (cursor.hasNext()) {
- hasResults = true;
- Document dbo = (Document) cursor.next();
- Object projected = addInitializedObject(dbo);
- if (index == internalIndex) {
- return projected;
- }
- }
- if (!hasResults) handleNoResults();
- initialized = true;
- }
- throw new ArrayIndexOutOfBoundsException("Index value " + index + " exceeds size of aggregate list");
- }
-
- @Override
- public Object set(int index, Object element) {
- initializeFully();
- return initializedObjects.set(index, element);
- }
-
- @Override
- public ListIterator listIterator() {
- return listIterator(0);
- }
-
- @Override
- public ListIterator listIterator(int index) {
- initializeFully();
- return initializedObjects.listIterator(index);
- }
-
- protected void initializeFully() {
- if (initialized) return;
- if (containsAssociations) {
- if (projectedProperties.size() == 1) {
- ProjectedProperty projectedProperty = projectedProperties.get(0);
- PersistentProperty property = projectedProperty.property;
- List<Serializable> identifiers = new ArrayList<>();
- boolean hasResults = false;
- while (cursor.hasNext()) {
- hasResults = true;
- Document dbo = (Document) cursor.next();
- Object id = getProjectedValue(dbo, projectedProperty.projectionKey);
- identifiers.add((Serializable) id);
- }
- if (!hasResults) {
- handleNoResults();
- }
- else if (property instanceof Embedded) {
- Embedded embedded = (Embedded) property;
- List embeddedList = new ArrayList();
- CodecRegistry codecRegistry = session.getDatastore().getCodecRegistry();
- PersistentEntityCodec codec = new PersistentEntityCodec(codecRegistry, embedded.getAssociatedEntity());
-
- for (Serializable embeddedDoc : identifiers) {
- if (embeddedDoc instanceof Document) {
- Document documentObject = (Document) embeddedDoc;
-
- Object decoded = codec.decode(new BsonDocumentReader(documentObject.toBsonDocument(Document.class, codecRegistry)));
- embeddedList.add(
- decoded
- );
- }
- }
-
- this.initializedObjects = embeddedList;
- }
- else {
- this.initializedObjects = session.retrieveAll(property.getType(), identifiers);
- }
- } else {
- Map<Integer, Map<Class, List<Serializable>>> associationMap = createAssociationMap();
-
- boolean hasResults = false;
- while (cursor.hasNext()) {
- hasResults = true;
- Document dbo = (Document) cursor.next();
- List<Object> projectedResult = new ArrayList<>();
- int index = 0;
- for (ProjectedProperty projectedProperty : projectedProperties) {
- PersistentProperty property = projectedProperty.property;
- Object value = getProjectedValue(dbo, projectedProperty.projectionKey);
- if (property instanceof Association) {
- if ((!(property instanceof Embedded) && !(property instanceof EmbeddedCollection) && !(property instanceof Basic))) {
- Map<Class, List<Serializable>> identifierMap = associationMap.get(index);
- Class type = ((Association) property).getAssociatedEntity().getJavaClass();
- identifierMap.get(type).add((Serializable) value);
- }
- }
- projectedResult.add(value);
- index++;
- }
-
- initializedObjects.add(projectedResult);
- }
-
- if (!hasResults) {
- handleNoResults();
- return;
- }
-
- Map<Integer, List> finalResults = new HashMap<>();
- for (Integer index : associationMap.keySet()) {
- Map<Class, List<Serializable>> associatedEntityIdentifiers = associationMap.get(index);
- for (Class associationClass : associatedEntityIdentifiers.keySet()) {
- List<Serializable> identifiers = associatedEntityIdentifiers.get(associationClass);
- finalResults.put(index, session.retrieveAll(associationClass, identifiers));
- }
- }
-
- for (Object initializedObject : initializedObjects) {
- List projected = (List) initializedObject;
- for (Integer index : finalResults.keySet()) {
- List resultsByIndex = finalResults.get(index);
- if (index < resultsByIndex.size()) {
- projected.set(index, resultsByIndex.get(index));
- } else {
- projected.set(index, null);
- }
- }
-
- }
- }
- } else {
- boolean hasResults = false;
- while (cursor.hasNext()) {
- hasResults = true;
- Document dbo = (Document) cursor.next();
- addInitializedObject(dbo);
- }
- if (!hasResults) {
- handleNoResults();
- }
- }
- initialized = true;
- }
-
- protected void handleNoResults() {
- ProjectedProperty projectedProperty = projectedProperties.get(0);
- if (projectedProperty.projection instanceof CountProjection) {
- initializedObjects.add(0);
- }
- }
-
- private Map<Integer, Map<Class, List<Serializable>>> createAssociationMap() {
- Map<Integer, Map<Class, List<Serializable>>> associationMap = new HashMap<>();
- associationMap = DefaultGroovyMethods.withDefault(associationMap, new Closure(this) {
- public Object doCall(Object o) {
- Map<Class, List<Serializable>> subMap = new HashMap<>();
- subMap = DefaultGroovyMethods.withDefault(subMap, new Closure(this) {
- public Object doCall(Object o) {
- return new ArrayList<Serializable>();
- }
- });
- return subMap;
- }
- });
- return associationMap;
- }
-
- @Override
- public Iterator iterator() {
- if (initialized || containsAssociations || internalIndex > 0) {
- initializeFully();
- return initializedObjects.iterator();
- }
-
- if (!cursor.hasNext()) {
- handleNoResults();
- return initializedObjects.iterator();
- }
-
- return new Iterator() {
- @Override
- public boolean hasNext() {
- boolean hasMore = cursor.hasNext();
- if (!hasMore) initialized = true;
- return hasMore;
- }
-
- @Override
- public Object next() {
- Document dbo = (Document) cursor.next();
- return addInitializedObject(dbo);
- }
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException("Aggregate result list cannot be mutated.");
- }
- };
- }
-
- private Object addInitializedObject(Document dbo) {
- if (projectedProperties.size() > 1) {
-
- List<Object> projected = new ArrayList<>();
- for (ProjectedProperty projectedProperty : projectedProperties) {
- Object value;
- value = getProjectedValue(dbo, projectedProperty.projectionKey);
- projected.add(value);
- }
- initializedObjects.add(internalIndex, projected);
- internalIndex++;
- return projected;
- } else {
- ProjectedProperty projectedProperty = projectedProperties.get(0);
- Object projected = getProjectedValue(dbo, projectedProperty.projectionKey);
- initializedObjects.add(internalIndex, projected);
- internalIndex++;
- return projected;
-
- }
- }
-
- private Object getProjectedValue(Document dbo, String projectionKey) {
- Object value;
- if (projectionKey.startsWith("id.")) {
- projectionKey = projectionKey.substring(3);
- Document id = (Document) dbo.get(MongoEntityPersister.MONGO_ID_FIELD);
- value = id.get(projectionKey);
- } else {
- value = dbo.get(projectionKey);
- }
- return value;
- }
-
- @Override
- public int size() {
- initializeFully();
- return initializedObjects.size();
- }
-
- @Override
- public void close() throws IOException {
- cursor.close();
- }
- }
-
- @SuppressWarnings("serial")
- public static class MongoResultList extends AbstractResultList {
-
- private EntityPersister mongoEntityPersister;
- private MongoCursor cursor;
- private boolean isCodecPersister;
-
- @SuppressWarnings("unchecked")
- public MongoResultList(MongoCursor cursor, Integer offset, EntityPersister mongoEntityPersister) {
- super(offset == null ? 0 : offset, cursor);
- this.cursor = cursor;
- this.mongoEntityPersister = mongoEntityPersister;
- this.isCodecPersister = mongoEntityPersister instanceof MongoCodecEntityPersister;
- }
-
- @Override
- public void close() throws IOException {
- cursor.close();
- }
-
- @Override
- public String toString() {
- initializeFully();
- return initializedObjects.toString();
- }
-
- /**
- * @return The underlying MongoDB cursor instance
- */
- public MongoCursor getCursor() {
- return cursor;
- }
-
- @Override
- protected Object nextDecoded() {
- final Object o = cursor.next();
- if (isCodecPersister) {
- final AbstractMongoSession session = (AbstractMongoSession) mongoEntityPersister.getSession();
- if (!session.contains(o)) {
- final PersistentEntity entity = mongoEntityPersister.getPersistentEntity();
- final EntityAccess entityAccess = session.createEntityAccess(entity, o);
- final Object id = entityAccess.getIdentifier();
- if (id != null) {
- session.cacheInstance(entity.getJavaClass(), (Serializable) id, o);
- }
- mongoEntityPersister.firePostLoadEvent(entity, entityAccess);
- }
- }
- return o;
- }
-
- @Override
- protected Object convertObject(Object object) {
- return isCodecPersister ? object : convertDBObject(object);
- }
-
- protected Object convertDBObject(Object object) {
- if (mongoEntityPersister instanceof MongoCodecEntityPersister) {
- return object;
- } else {
- final Document dbObject = (Document) object;
- Object id = dbObject.get(MongoEntityPersister.MONGO_ID_FIELD);
- SessionImplementor session = (SessionImplementor) mongoEntityPersister.getSession();
- Class type = mongoEntityPersister.getPersistentEntity().getJavaClass();
- Object instance = session.getCachedInstance(type, (Serializable) id);
- if (instance == null) {
- final MongoEntityPersister mep = (MongoEntityPersister) this.mongoEntityPersister;
- instance = mep.createObjectFromNativeEntry(
- this.mongoEntityPersister.getPersistentEntity(), (Serializable) id, dbObject);
- session.cacheInstance(type, (Serializable) id, instance);
- }
- return instance;
- }
- }
-
- }
-
- public static class ProjectedProperty {
- public Projection projection;
- public String projectionKey;
- public PersistentProperty property;
- }
-
- protected static class AggregatePipeline {
- private PersistentEntity entity;
- private Document query;
- private List<Projection> projectionList;
- private List<Document> aggregationPipeline;
- private List<ProjectedProperty> projectedKeys;
- private boolean singleResult;
- private final MongoQuery mongoQuery;
-
- public AggregatePipeline(MongoQuery mongoQuery, PersistentEntity entity, Document queryObject, List<Projection> projectionList) {
- this.mongoQuery = mongoQuery;
- this.entity = entity;
- this.query = queryObject;
- this.projectionList = projectionList;
- }
-
- public List<Document> getAggregationPipeline() {
- return aggregationPipeline;
- }
-
- public List<ProjectedProperty> getProjectedKeys() {
- return projectedKeys;
- }
-
- public boolean isSingleResult() {
- return singleResult;
- }
-
- public AggregatePipeline build() {
- aggregationPipeline = new ArrayList<>();
-
- if (!query.keySet().isEmpty()) {
- aggregationPipeline.add(new Document(MATCH_OPERATOR, query));
- }
-
- projectedKeys = new ArrayList<>();
- singleResult = true;
-
- Document projectObject = new Document();
-
- Document groupByObject = new Document();
- groupByObject.put(MongoEntityPersister.MONGO_ID_FIELD, 0);
- Document additionalGroupBy = null;
-
- for (Projection projection : projectionList) {
- ProjectionHandler projectionHandler = projectProjectionHandlers.get(projection.getClass());
- ProjectedProperty projectedProperty = new ProjectedProperty();
- projectedProperty.projection = projection;
- if (projection instanceof PropertyProjection) {
- PropertyProjection propertyProjection = (PropertyProjection) projection;
- String propertyName = propertyProjection.getPropertyName();
-
- PersistentProperty property = entity.getPropertyByName(propertyName);
- if (property != null) {
- projectedProperty.property = property;
- } else if (!propertyName.contains(".")) {
- throw new InvalidDataAccessResourceUsageException("Attempt to project on a non-existent project [" + propertyName + "]");
- }
- }
- if (projectionHandler != null) {
- singleResult = false;
-
- String aggregationKey = projectionHandler.handle(entity, projectObject, groupByObject, projection);
- aggregationKey = "id." + aggregationKey;
- projectedProperty.projectionKey = aggregationKey;
- projectedKeys.add(projectedProperty);
- } else {
-
- projectionHandler = groupByProjectionHandlers.get(projection.getClass());
- if (projectionHandler != null) {
- projectedProperty.projectionKey = projectionHandler.handle(entity, projectObject, groupByObject, projection);
- projectedKeys.add(projectedProperty);
-
- if (projection instanceof CountDistinctProjection) {
- Document finalCount = new Document(MongoEntityPersister.MONGO_ID_FIELD, 1);
- finalCount.put(projectedProperty.projectionKey, new Document(SUM_OPERATOR, 1));
- additionalGroupBy = new Document(GROUP_OPERATOR, finalCount);
- }
- }
-
- }
- }
-
- if (!projectObject.isEmpty()) {
- aggregationPipeline.add(new Document(PROJECT_OPERATOR, projectObject));
- }
-
- aggregationPipeline.add(new Document(GROUP_OPERATOR, groupByObject));
-
- if (additionalGroupBy != null) {
- aggregationPipeline.add(additionalGroupBy);
- }
-
- List<Order> orderBy = mongoQuery.getOrderBy();
- if (!orderBy.isEmpty()) {
- Document sortBy = new Document();
- for (Order order : orderBy) {
- String prop = order.getProperty();
- String sortKey = prop;
- for (ProjectedProperty pp : projectedKeys) {
- if (pp.property != null && pp.property.getName().equals(prop)) {
- sortKey = pp.projectionKey;
- if (sortKey.startsWith("id.")) {
- sortKey = MongoEntityPersister.MONGO_ID_FIELD + "." + sortKey.substring(3);
- }
- break;
- }
- }
- sortBy.put(sortKey, order.getDirection() == Order.Direction.ASC ? 1 : -1);
- }
- aggregationPipeline.add(new Document(SORT_OPERATOR, sortBy));
- }
-
- int max = mongoQuery.max != null ? mongoQuery.max : -1;
- if (max > 0) {
- aggregationPipeline.add(new Document("$limit", max));
- }
- int offset = mongoQuery.offset != null ? mongoQuery.offset : 0;
- if (offset > 0) {
- aggregationPipeline.add(new Document("$skip", offset));
- }
-
- return this;
- }
- }
-}