All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.data.mongodb.operations.DefaultReactiveMongoRepositoryOperations Maven / Gradle / Ivy

There is a newer version: 4.10.5
Show newest version
/*
 * Copyright 2017-2022 original 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 io.micronaut.data.mongodb.operations;

import com.mongodb.CursorType;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.DeleteOneModel;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.ReplaceOneModel;
import com.mongodb.client.model.UpdateOneModel;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.reactivestreams.client.AggregatePublisher;
import com.mongodb.reactivestreams.client.ClientSession;
import com.mongodb.reactivestreams.client.FindPublisher;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.EachBean;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.data.connection.reactive.ReactorConnectionOperations;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.PersistentEntity;
import io.micronaut.data.model.PersistentProperty;
import io.micronaut.data.model.runtime.AttributeConverterRegistry;
import io.micronaut.data.model.runtime.DeleteBatchOperation;
import io.micronaut.data.model.runtime.DeleteOperation;
import io.micronaut.data.model.runtime.InsertBatchOperation;
import io.micronaut.data.model.runtime.InsertOperation;
import io.micronaut.data.model.runtime.PagedQuery;
import io.micronaut.data.model.runtime.PreparedQuery;
import io.micronaut.data.model.runtime.RuntimeAssociation;
import io.micronaut.data.model.runtime.RuntimeEntityRegistry;
import io.micronaut.data.model.runtime.RuntimePersistentEntity;
import io.micronaut.data.model.runtime.RuntimePersistentProperty;
import io.micronaut.data.model.runtime.StoredQuery;
import io.micronaut.data.model.runtime.UpdateBatchOperation;
import io.micronaut.data.model.runtime.UpdateOperation;
import io.micronaut.data.mongodb.conf.RequiresReactiveMongo;
import io.micronaut.data.mongodb.operations.options.MongoAggregationOptions;
import io.micronaut.data.mongodb.operations.options.MongoFindOptions;
import io.micronaut.data.operations.reactive.ReactorReactiveRepositoryOperations;
import io.micronaut.data.runtime.config.DataSettings;
import io.micronaut.data.runtime.convert.DataConversionService;
import io.micronaut.data.runtime.date.DateTimeProvider;
import io.micronaut.data.runtime.operations.internal.AbstractReactiveEntitiesOperations;
import io.micronaut.data.runtime.operations.internal.AbstractReactiveEntityOperations;
import io.micronaut.data.runtime.operations.internal.OperationContext;
import io.micronaut.data.runtime.operations.internal.ReactiveCascadeOperations;
import io.micronaut.inject.qualifiers.Qualifiers;
import org.bson.BsonDocument;
import org.bson.BsonDocumentWrapper;
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;
import org.slf4j.Logger;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * The reactive MongoDB repository operations implementation.
 *
 * @author Denis Stepanov
 * @since 3.3
 */
@RequiresReactiveMongo
@EachBean(MongoClient.class)
@Internal
public final class DefaultReactiveMongoRepositoryOperations extends AbstractMongoRepositoryOperations
    implements MongoReactorRepositoryOperations,
    ReactorReactiveRepositoryOperations,
    ReactiveCascadeOperations.ReactiveCascadeOperationsHelper {

    private static final Logger QUERY_LOG = DataSettings.QUERY_LOG;
    private final MongoClient mongoClient;
    private final ReactiveCascadeOperations cascadeOperations;
    private final ReactorConnectionOperations connectionOperations;

    /**
     * Default constructor.
     *
     * @param serverName                 The server name
     * @param beanContext                The bean context
     * @param dateTimeProvider           The date time provider
     * @param runtimeEntityRegistry      The entity registry
     * @param conversionService          The conversion service
     * @param attributeConverterRegistry The attribute converter registry
     * @param mongoClient                The reactive mongo client
     * @param collectionNameProvider     The collection name provider
     * @param connectionOperations       The connection operations
     */
    DefaultReactiveMongoRepositoryOperations(@Parameter String serverName,
                                             BeanContext beanContext,
                                             DateTimeProvider dateTimeProvider,
                                             RuntimeEntityRegistry runtimeEntityRegistry,
                                             DataConversionService conversionService,
                                             AttributeConverterRegistry attributeConverterRegistry,
                                             MongoClient mongoClient,
                                             MongoCollectionNameProvider collectionNameProvider,
                                             @Parameter ReactorConnectionOperations connectionOperations) {
        super(dateTimeProvider, runtimeEntityRegistry, conversionService, attributeConverterRegistry, collectionNameProvider,
            beanContext.getBean(MongoDatabaseNameProvider.class, "Primary".equals(serverName) ? null : Qualifiers.byName(serverName))
        );
        this.mongoClient = mongoClient;
        this.cascadeOperations = new ReactiveCascadeOperations<>(conversionService, this);
        this.connectionOperations = connectionOperations;
    }

    @Override
    public  Mono findOne(Class type, Object id) {
        return withClientSession(clientSession -> {
            RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(type);
            MongoDatabase database = getDatabase(persistentEntity, null);
            MongoCollection collection = getCollection(database, persistentEntity, type);
            Bson filter = MongoUtils.filterById(conversionService, persistentEntity, id, collection.getCodecRegistry());
            if (QUERY_LOG.isDebugEnabled()) {
                QUERY_LOG.debug("Executing Mongo 'find' with filter: {}", filter.toBsonDocument().toJson());
            }
            return Mono.from(collection.find(clientSession, filter, type).first());
        });
    }

    @Override
    public  Mono findOne(PreparedQuery preparedQuery) {
        return withClientSession(clientSession -> {
            MongoPreparedQuery mongoPreparedQuery = getMongoPreparedQuery(preparedQuery);
            if (mongoPreparedQuery.isCount()) {
                return getCount(clientSession, mongoPreparedQuery);
            }
            if (mongoPreparedQuery.isAggregate()) {
                return findOneAggregated(clientSession, mongoPreparedQuery);
            } else {
                return findOneFiltered(clientSession, mongoPreparedQuery);
            }
        });
    }

    @Override
    public  Mono exists(PreparedQuery preparedQuery) {
        return withClientSession(clientSession -> {
            MongoPreparedQuery mongoPreparedQuery = getMongoPreparedQuery(preparedQuery);
            if (mongoPreparedQuery.isAggregate()) {
                return Flux.from(aggregate(clientSession, mongoPreparedQuery, BsonDocument.class)).hasElements();
            } else {
                return Flux.from(find(clientSession, mongoPreparedQuery, BsonDocument.class).limit(1)).hasElements();
            }
        });
    }

    @Override
    public  Flux findAll(PagedQuery query) {
        throw new DataAccessException("Not supported!");
    }

    @Override
    public  Mono count(PagedQuery pagedQuery) {
        throw new DataAccessException("Not supported!");
    }

    @Override
    public  Flux findAll(PreparedQuery preparedQuery) {
        return withClientSessionMany(clientSession -> findAll(clientSession, getMongoPreparedQuery(preparedQuery)));
    }

    @Override
    public  Mono findOptional(Class type, Object id) {
        return findOne(type, id);
    }

    @Override
    public  Mono findOptional(PreparedQuery preparedQuery) {
        return findOne(preparedQuery);
    }

    @Override
    public  Mono> findPage(PagedQuery pagedQuery) {
        throw new DataAccessException("Not supported!");
    }

    @Override
    public  Mono persist(InsertOperation operation) {
        return withClientSession(clientSession -> {
            MongoOperationContext ctx = new MongoOperationContext(clientSession, operation.getRepositoryType(), operation.getAnnotationMetadata());
            return persistOne(ctx, operation.getEntity(), runtimeEntityRegistry.getEntity(operation.getRootEntity()));
        });
    }

    @Override
    public  Flux persistAll(InsertBatchOperation operation) {
        return withClientSessionMany(clientSession -> {
            MongoOperationContext ctx = new MongoOperationContext(clientSession, operation.getRepositoryType(), operation.getAnnotationMetadata());
            return persistBatch(ctx, operation, runtimeEntityRegistry.getEntity(operation.getRootEntity()), null);
        });
    }

    @Override
    public  Mono update(UpdateOperation operation) {
        return withClientSession(clientSession -> {
            MongoOperationContext ctx = new MongoOperationContext(clientSession, operation.getRepositoryType(), operation.getAnnotationMetadata());
            StoredQuery storedQuery = operation.getStoredQuery();
            if (storedQuery != null) {
                MongoStoredQuery mongoStoredQuery = getMongoStoredQuery(storedQuery);
                MongoReactiveEntitiesOperation op = createMongoUpdateOneInBulkOperation(ctx, mongoStoredQuery.getRuntimePersistentEntity(),
                    Collections.singletonList(operation.getEntity()), mongoStoredQuery);
                op.update();
                return op.getEntities().next();
            }
            return updateOne(ctx, operation.getEntity(), runtimeEntityRegistry.getEntity(operation.getRootEntity()));
        });
    }

    @Override
    public  Flux updateAll(UpdateBatchOperation operation) {
        return withClientSessionMany(clientSession -> {
            MongoOperationContext ctx = new MongoOperationContext(clientSession, operation.getRepositoryType(), operation.getAnnotationMetadata());
            StoredQuery storedQuery = operation.getStoredQuery();
            if (storedQuery != null) {
                MongoStoredQuery mongoStoredQuery = getMongoStoredQuery(storedQuery);
                MongoReactiveEntitiesOperation op = createMongoUpdateOneInBulkOperation(ctx, mongoStoredQuery.getRuntimePersistentEntity(), operation, mongoStoredQuery);
                op.update();
                return op.getEntities();
            }
            return updateBatch(ctx, operation, runtimeEntityRegistry.getEntity(operation.getRootEntity()));
        });
    }

    @Override
    public  Mono delete(DeleteOperation operation) {
        return withClientSession(clientSession -> {
            MongoOperationContext ctx = new MongoOperationContext(clientSession, operation.getRepositoryType(), operation.getAnnotationMetadata());
            StoredQuery storedQuery = operation.getStoredQuery();
            if (storedQuery != null) {
                MongoStoredQuery mongoStoredQuery = (MongoStoredQuery) getMongoStoredQuery(storedQuery);
                MongoReactiveEntitiesOperation op = createMongoDeleteOneInBulkOperation(ctx, mongoStoredQuery.getRuntimePersistentEntity(),
                    Collections.singletonList(operation.getEntity()), mongoStoredQuery);
                op.update();
                return op.getRowsUpdated();
            }
            RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(operation.getRootEntity());
            MongoReactiveEntityOperation op = createMongoDeleteOneOperation(ctx, persistentEntity, operation.getEntity());
            op.delete();
            return op.getRowsUpdated();
        });
    }

    @Override
    public  Mono deleteAll(DeleteBatchOperation operation) {
        return withClientSession(clientSession -> {
            MongoOperationContext ctx = new MongoOperationContext(clientSession, operation.getRepositoryType(), operation.getAnnotationMetadata());
            StoredQuery storedQuery = operation.getStoredQuery();
            if (storedQuery != null) {
                MongoStoredQuery mongoStoredQuery = (MongoStoredQuery) getMongoStoredQuery(storedQuery);
                MongoReactiveEntitiesOperation op = createMongoDeleteOneInBulkOperation(ctx, mongoStoredQuery.getRuntimePersistentEntity(), operation, mongoStoredQuery);
                op.update();
                return op.getRowsUpdated();
            }
            RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(operation.getRootEntity());
            if (operation.all()) {
                MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
                return Mono.from(getCollection(mongoDatabase, persistentEntity, persistentEntity.getIntrospection().getBeanType()).deleteMany(EMPTY)).map(DeleteResult::getDeletedCount);
            }
            MongoReactiveEntitiesOperation op = createMongoDeleteManyOperation(ctx, persistentEntity, operation);
            op.delete();
            return op.getRowsUpdated();
        });
    }

    @Override
    public Mono executeUpdate(PreparedQuery preparedQuery) {
        return withClientSession(clientSession -> {
            MongoPreparedQuery mongoPreparedQuery = getMongoPreparedQuery(preparedQuery);
            MongoUpdate updateMany = mongoPreparedQuery.getUpdateMany();
            if (QUERY_LOG.isDebugEnabled()) {
                QUERY_LOG.debug("Executing Mongo 'updateMany' with filter: {} and update: {}", updateMany.getFilter().toBsonDocument().toJson(), updateMany.getUpdate().toBsonDocument().toJson());
            }
            return Mono.from(getCollection(mongoPreparedQuery)
                .updateMany(clientSession, updateMany.getFilter(), updateMany.getUpdate(), updateMany.getOptions())).map(updateResult -> {
                if (mongoPreparedQuery.isOptimisticLock()) {
                    checkOptimisticLocking(1, (int) updateResult.getModifiedCount());
                }
                return updateResult.getModifiedCount();
            });
        });
    }

    @Override
    public Mono executeDelete(PreparedQuery preparedQuery) {
        return withClientSession(clientSession -> {
            MongoPreparedQuery mongoPreparedQuery = getMongoPreparedQuery(preparedQuery);
            MongoDelete deleteMany = mongoPreparedQuery.getDeleteMany();
            if (QUERY_LOG.isDebugEnabled()) {
                QUERY_LOG.debug("Executing Mongo 'deleteMany' with filter: {}", deleteMany.getFilter().toBsonDocument().toJson());
            }
            return Mono.from(getCollection(mongoPreparedQuery).
                deleteMany(clientSession, deleteMany.getFilter(), deleteMany.getOptions())).map(deleteResult -> {
                if (mongoPreparedQuery.isOptimisticLock()) {
                    checkOptimisticLocking(1, (int) deleteResult.getDeletedCount());
                }
                return deleteResult.getDeletedCount();
            });
        });
    }

    private  Flux findAll(ClientSession clientSession, MongoPreparedQuery preparedQuery) {
        if (preparedQuery.isCount()) {
            return getCount(clientSession, preparedQuery).flux();
        }
        if (preparedQuery.isAggregate()) {
            return findAllAggregated(clientSession, preparedQuery, preparedQuery.isDtoProjection());
        }
        return Flux.from(find(clientSession, preparedQuery));
    }

    private  Mono getCount(ClientSession clientSession, MongoPreparedQuery preparedQuery) {
        Class resultType = preparedQuery.getResultType();
        MongoDatabase database = getDatabase(preparedQuery);
        RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity();
        if (preparedQuery.isAggregate()) {
            MongoAggregation aggregation = preparedQuery.getAggregation();
            if (QUERY_LOG.isDebugEnabled()) {
                QUERY_LOG.debug("Executing Mongo 'aggregate' with pipeline: {}", aggregation.getPipeline().stream().map(e -> e.toBsonDocument().toJson()).toList());
            }
            return Mono.from(aggregate(clientSession, preparedQuery, BsonDocument.class).first())
                .map(bsonDocument -> convertResult(database.getCodecRegistry(), resultType, bsonDocument, false))
                .switchIfEmpty(Mono.defer(() -> Mono.just(conversionService.convertRequired(0, resultType))));
        } else {
            MongoFind find = preparedQuery.getFind();
            MongoFindOptions options = find.getOptions();
            Bson filter = options == null ? null : options.getFilter();
            filter = filter == null ? new BsonDocument() : filter;
            if (QUERY_LOG.isDebugEnabled()) {
                QUERY_LOG.debug("Executing Mongo 'countDocuments' with filter: {}", filter.toBsonDocument().toJson());
            }
            return Mono.from(getCollection(database, persistentEntity, BsonDocument.class)
                    .countDocuments(clientSession, filter))
                .map(count -> conversionService.convertRequired(count, resultType));
        }
    }

    private  Mono findOneFiltered(ClientSession clientSession, MongoPreparedQuery preparedQuery) {
        return Mono.from(find(clientSession, preparedQuery).limit(1).first())
            .map(r -> {
                Class type = preparedQuery.getRootEntity();
                RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity();
                if (type.isInstance(r)) {
                    return (R) triggerPostLoad(preparedQuery.getAnnotationMetadata(), persistentEntity, type.cast(r));
                }
                return r;
            });
    }

    private  Mono findOneAggregated(ClientSession clientSession, MongoPreparedQuery preparedQuery) {
        Class resultType = preparedQuery.getResultType();
        Class type = preparedQuery.getRootEntity();
        if (!resultType.isAssignableFrom(type)) {
            MongoDatabase database = getDatabase(preparedQuery);
            return Mono.from(aggregate(clientSession, preparedQuery, BsonDocument.class).first())
                .map(bsonDocument -> convertResult(database.getCodecRegistry(), resultType, bsonDocument, preparedQuery.isDtoProjection()));
        }
        return Mono.from(aggregate(clientSession, preparedQuery).first())
            .map(r -> {
                RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity();
                if (type.isInstance(r)) {
                    return (R) triggerPostLoad(preparedQuery.getAnnotationMetadata(), persistentEntity, type.cast(r));
                }
                return r;
            });
    }

    private  Flux findAllAggregated(ClientSession clientSession, MongoPreparedQuery preparedQuery, boolean isDtoProjection) {
        Class type = preparedQuery.getRootEntity();
        Class resultType = preparedQuery.getResultType();
        Flux aggregate;
        if (!resultType.isAssignableFrom(type)) {
            MongoDatabase database = getDatabase(preparedQuery);
            aggregate = Flux.from(aggregate(clientSession, preparedQuery, BsonDocument.class))
                .map(result -> convertResult(database.getCodecRegistry(), resultType, result, isDtoProjection));
        } else {
            aggregate = Flux.from(aggregate(clientSession, preparedQuery));
        }
        return aggregate;
    }

    private  FindPublisher find(ClientSession clientSession, MongoPreparedQuery preparedQuery) {
        return find(clientSession, preparedQuery, preparedQuery.getResultType());
    }

    private  FindPublisher find(ClientSession clientSession,
                                              MongoPreparedQuery preparedQuery,
                                              Class resultType) {
        MongoFind find = preparedQuery.getFind();
        if (QUERY_LOG.isDebugEnabled()) {
            logFind(find);
        }
        MongoDatabase database = getDatabase(preparedQuery);
        MongoCollection collection = getCollection(database, preparedQuery.getPersistentEntity(), resultType);
        FindPublisher findIterable = collection.find(clientSession, resultType);
        return applyFindOptions(find.getOptions(), findIterable);
    }

    private  FindPublisher applyFindOptions(@Nullable MongoFindOptions findOptions, FindPublisher findIterable) {
        if (findOptions == null) {
            return findIterable;
        }
        Bson filter = findOptions.getFilter();
        if (filter != null) {
            findIterable = findIterable.filter(filter);
        }
        Collation collation = findOptions.getCollation();
        if (collation != null) {
            findIterable = findIterable.collation(collation);
        }
        Integer skip = findOptions.getSkip();
        if (skip != null) {
            findIterable = findIterable.skip(skip);
        }
        Integer limit = findOptions.getLimit();
        if (limit != null) {
            findIterable = findIterable.limit(Math.max(limit, 0));
        }
        Bson sort = findOptions.getSort();
        if (sort != null) {
            findIterable = findIterable.sort(sort);
        }
        Bson projection = findOptions.getProjection();
        if (projection != null) {
            findIterable = findIterable.projection(projection);
        }
        Integer batchSize = findOptions.getBatchSize();
        if (batchSize != null) {
            findIterable = findIterable.batchSize(batchSize);
        }
        Boolean allowDiskUse = findOptions.getAllowDiskUse();
        if (allowDiskUse != null) {
            findIterable = findIterable.allowDiskUse(allowDiskUse);
        }
        Long maxTimeMS = findOptions.getMaxTimeMS();
        if (maxTimeMS != null) {
            findIterable = findIterable.maxTime(maxTimeMS, TimeUnit.MILLISECONDS);
        }
        Long maxAwaitTimeMS = findOptions.getMaxAwaitTimeMS();
        if (maxAwaitTimeMS != null) {
            findIterable = findIterable.maxAwaitTime(maxAwaitTimeMS, TimeUnit.MILLISECONDS);
        }
        String comment = findOptions.getComment();
        if (comment != null) {
            findIterable = findIterable.comment(comment);
        }
        Bson hint = findOptions.getHint();
        if (hint != null) {
            findIterable = findIterable.hint(hint);
        }
        CursorType cursorType = findOptions.getCursorType();
        if (cursorType != null) {
            findIterable = findIterable.cursorType(cursorType);
        }
        Boolean noCursorTimeout = findOptions.getNoCursorTimeout();
        if (noCursorTimeout != null) {
            findIterable = findIterable.noCursorTimeout(noCursorTimeout);
        }
        Boolean partial = findOptions.getPartial();
        if (partial != null) {
            findIterable = findIterable.partial(partial);
        }
        Bson max = findOptions.getMax();
        if (max != null) {
            findIterable = findIterable.max(max);
        }
        Bson min = findOptions.getMin();
        if (min != null) {
            findIterable = findIterable.min(min);
        }
        Boolean returnKey = findOptions.getReturnKey();
        if (returnKey != null) {
            findIterable = findIterable.returnKey(returnKey);
        }
        Boolean showRecordId = findOptions.getShowRecordId();
        if (showRecordId != null) {
            findIterable = findIterable.showRecordId(showRecordId);
        }
        return findIterable;
    }

    private  AggregatePublisher aggregate(ClientSession clientSession,
                                                        MongoPreparedQuery preparedQuery,
                                                        Class resultType) {
        MongoDatabase database = getDatabase(preparedQuery);
        MongoCollection collection = getCollection(database, preparedQuery.getPersistentEntity(), resultType);
        MongoAggregation aggregation = preparedQuery.getAggregation();
        if (QUERY_LOG.isDebugEnabled()) {
            logAggregate(aggregation);
        }
        AggregatePublisher aggregateIterable = collection.aggregate(clientSession, aggregation.getPipeline(), resultType);
        return applyAggregateOptions(aggregation.getOptions(), aggregateIterable);
    }

    private  AggregatePublisher aggregate(ClientSession clientSession,
                                                   MongoPreparedQuery preparedQuery) {
        return aggregate(clientSession, preparedQuery, preparedQuery.getResultType());
    }

    private  AggregatePublisher applyAggregateOptions(@Nullable MongoAggregationOptions aggregateOptions, AggregatePublisher aggregateIterable) {
        if (aggregateOptions == null) {
            return aggregateIterable;
        }
        if (aggregateOptions.getCollation() != null) {
            aggregateIterable = aggregateIterable.collation(aggregateOptions.getCollation());
        }
        Boolean allowDiskUse = aggregateOptions.getAllowDiskUse();
        if (allowDiskUse != null) {
            aggregateIterable = aggregateIterable.allowDiskUse(allowDiskUse);
        }
        Long maxTimeMS = aggregateOptions.getMaxTimeMS();
        if (maxTimeMS != null) {
            aggregateIterable = aggregateIterable.maxTime(maxTimeMS, TimeUnit.MILLISECONDS);
        }
        Long maxAwaitTimeMS = aggregateOptions.getMaxAwaitTimeMS();
        if (maxTimeMS != null) {
            aggregateIterable = aggregateIterable.maxAwaitTime(maxAwaitTimeMS, TimeUnit.MILLISECONDS);
        }
        Boolean bypassDocumentValidation = aggregateOptions.getBypassDocumentValidation();
        if (bypassDocumentValidation != null) {
            aggregateIterable = aggregateIterable.bypassDocumentValidation(bypassDocumentValidation);
        }
        String comment = aggregateOptions.getComment();
        if (comment != null) {
            aggregateIterable = aggregateIterable.comment(comment);
        }
        Bson hint = aggregateOptions.getHint();
        if (hint != null) {
            aggregateIterable = aggregateIterable.hint(hint);
        }
        return aggregateIterable;
    }

    private  K triggerPostLoad(AnnotationMetadata annotationMetadata, RuntimePersistentEntity persistentEntity, K entity) {
        if (persistentEntity.hasPostLoadEventListeners()) {
            entity = triggerPostLoad(entity, persistentEntity, annotationMetadata);
        }
        for (PersistentProperty pp : persistentEntity.getPersistentProperties()) {
            if (pp instanceof RuntimeAssociation runtimeAssociation) {
                Object o = runtimeAssociation.getProperty().get(entity);
                if (o == null) {
                    continue;
                }
                RuntimePersistentEntity associatedEntity = runtimeAssociation.getAssociatedEntity();
                switch (runtimeAssociation.getKind()) {
                    case MANY_TO_MANY:
                    case ONE_TO_MANY:
                        if (o instanceof Iterable iterable) {
                            for (Object value : iterable) {
                                triggerPostLoad(value, associatedEntity, annotationMetadata);
                            }
                        }
                        continue;
                    case MANY_TO_ONE:
                    case ONE_TO_ONE:
                    case EMBEDDED:
                        triggerPostLoad(o, associatedEntity, annotationMetadata);
                        continue;
                    default:
                        throw new IllegalStateException("Unknown kind: " + runtimeAssociation.getKind());
                }
            }
        }
        return entity;
    }

    private  MongoCollection getCollection(RuntimePersistentEntity persistentEntity, Class repositoryClass) {
        return getDatabase(persistentEntity, repositoryClass).getCollection(persistentEntity.getPersistedName(), persistentEntity.getIntrospection().getBeanType());
    }

    @Override
    public  Mono persistOne(MongoOperationContext ctx, T value, RuntimePersistentEntity persistentEntity) {
        MongoReactiveEntityOperation op = createMongoInsertOneOperation(ctx, persistentEntity, value);
        op.persist();
        return op.getEntity();
    }

    @Override
    public  Flux persistBatch(MongoOperationContext ctx, Iterable values, RuntimePersistentEntity persistentEntity, Predicate predicate) {
        MongoReactiveEntitiesOperation op = createMongoInsertManyOperation(ctx, persistentEntity, values);
        if (predicate != null) {
            op.veto(predicate);
        }
        op.persist();
        return op.getEntities();
    }

    @Override
    public  Mono updateOne(MongoOperationContext ctx, T value, RuntimePersistentEntity persistentEntity) {
        MongoReactiveEntityOperation op = createMongoReplaceOneOperation(ctx, persistentEntity, value);
        op.update();
        return op.getEntity();
    }

    private  Flux updateBatch(MongoOperationContext ctx, Iterable values, RuntimePersistentEntity persistentEntity) {
        MongoReactiveEntitiesOperation op = createMongoReplaceOneInBulkOperation(ctx, persistentEntity, values);
        op.update();
        return op.getEntities();
    }

    @Override
    protected MongoDatabase getDatabase(PersistentEntity persistentEntity, Class repository) {
        return mongoClient.getDatabase(databaseNameProvider.provide(persistentEntity, repository));
    }

    private MongoDatabase getDatabase(MongoPreparedQuery preparedQuery) {
        return getDatabase(preparedQuery.getPersistentEntity(), preparedQuery.getRepositoryType());
    }

    private  MongoCollection getCollection(MongoPreparedQuery preparedQuery) {
        return getCollection(getDatabase(preparedQuery), preparedQuery.getPersistentEntity(), preparedQuery.getRootEntity());
    }

    private  MongoCollection getCollection(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity) {
        return getCollection(persistentEntity, ctx.repositoryType, persistentEntity.getIntrospection().getBeanType());
    }

    private  MongoCollection getCollection(MongoDatabase database, RuntimePersistentEntity persistentEntity, Class resultType) {
        return database.getCollection(collectionNameProvider.provide(persistentEntity), resultType);
    }

    private  MongoCollection getCollection(RuntimePersistentEntity persistentEntity, Class repositoryClass, Class resultType) {
        return getDatabase(persistentEntity, repositoryClass).getCollection(collectionNameProvider.provide(persistentEntity), resultType);
    }

    @Override
    protected CodecRegistry getCodecRegistry(MongoDatabase mongoDatabase) {
        return mongoDatabase.getCodecRegistry();
    }

    @Override
    public Mono persistManyAssociation(MongoOperationContext ctx, RuntimeAssociation runtimeAssociation, Object value, RuntimePersistentEntity persistentEntity, Object child, RuntimePersistentEntity childPersistentEntity) {
        String joinCollectionName = runtimeAssociation.getOwner().getNamingStrategy().mappedName(runtimeAssociation);
        MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
        MongoCollection collection = mongoDatabase.getCollection(joinCollectionName, BsonDocument.class);
        BsonDocument association = association(collection.getCodecRegistry(), value, persistentEntity, child, childPersistentEntity);
        if (QUERY_LOG.isDebugEnabled()) {
            QUERY_LOG.debug("Executing Mongo 'insertOne' for collection: {} with document: {}", collection.getNamespace().getFullName(), association);
        }
        return Mono.from(collection.insertOne(ctx.clientSession, association, getInsertOneOptions(ctx.annotationMetadata))).then();
    }

    @Override
    public Mono persistManyAssociationBatch(MongoOperationContext ctx, RuntimeAssociation runtimeAssociation, Object value, RuntimePersistentEntity persistentEntity, Iterable child, RuntimePersistentEntity childPersistentEntity, Predicate veto) {
        String joinCollectionName = runtimeAssociation.getOwner().getNamingStrategy().mappedName(runtimeAssociation);
        MongoCollection collection = getDatabase(persistentEntity, ctx.repositoryType).getCollection(joinCollectionName, BsonDocument.class);
        List associations = new ArrayList<>();
        for (Object c : child) {
            associations.add(association(collection.getCodecRegistry(), value, persistentEntity, c, childPersistentEntity));
        }
        if (QUERY_LOG.isDebugEnabled()) {
            QUERY_LOG.debug("Executing Mongo 'insertMany' for collection: {} with associations: {}", collection.getNamespace().getFullName(), associations);
        }
        return Mono.from(collection.insertMany(ctx.clientSession, associations, getInsertManyOptions(ctx.annotationMetadata))).then();
    }

    @Override
    public  Mono withClientSession(Function> function) {
        return connectionOperations.withConnectionMono(status -> function.apply(status.getConnection()));
    }

    @Override
    public  Flux withClientSessionMany(Function> function) {
        return connectionOperations.withConnectionFlux(status -> function.apply(status.getConnection()));
    }

    private  MongoReactiveEntityOperation createMongoInsertOneOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, T entity) {
        return new MongoReactiveEntityOperation<>(ctx, persistentEntity, entity, true) {

            @Override
            protected void execute() throws RuntimeException {
                MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
                MongoCollection collection = getCollection(mongoDatabase, persistentEntity, persistentEntity.getIntrospection().getBeanType());

                data = data.flatMap(d -> {
                    if (QUERY_LOG.isDebugEnabled()) {
                        QUERY_LOG.debug("Executing Mongo 'insertOne' with entity: {}", d.entity);
                    }
                    return Mono.from(collection.insertOne(ctx.clientSession, d.entity, getInsertOneOptions(ctx.annotationMetadata))).map(insertOneResult -> {
                        BsonValue insertedId = insertOneResult.getInsertedId();
                        BeanProperty property = persistentEntity.getIdentity().getProperty();
                        if (property.get(d.entity) == null) {
                            d.entity = updateEntityId(property, d.entity, insertedId);
                        }
                        return d;
                    });
                });
            }
        };
    }

    private  MongoReactiveEntityOperation createMongoReplaceOneOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, T entity) {
        return new MongoReactiveEntityOperation<>(ctx, persistentEntity, entity, false) {

            final MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
            final MongoCollection collection = getCollection(mongoDatabase, persistentEntity, BsonDocument.class);

            @Override
            protected void collectAutoPopulatedPreviousValues() {
                data = data.map(d -> {
                    d.filter = createFilterIdAndVersion(persistentEntity, d.entity, collection.getCodecRegistry());
                    return d;
                });
            }

            @Override
            protected void execute() throws RuntimeException {
                data = data.flatMap(d -> {
                    Bson filter = (Bson) d.filter;
                    if (QUERY_LOG.isDebugEnabled()) {
                        QUERY_LOG.debug("Executing Mongo 'replaceOne' with filter: {}", filter.toBsonDocument().toJson());
                    }
                    BsonDocument bsonDocument = BsonDocumentWrapper.asBsonDocument(d.entity, mongoDatabase.getCodecRegistry());
                    bsonDocument.remove("_id");
                    return Mono.from(collection.replaceOne(ctx.clientSession, filter, bsonDocument, getReplaceOptions(ctx.annotationMetadata))).map(updateResult -> {
                        d.rowsUpdated = updateResult.getModifiedCount();
                        if (persistentEntity.getVersion() != null) {
                            checkOptimisticLocking(1, (int) d.rowsUpdated);
                        }
                        return d;
                    });
                });

            }

        };
    }

    private  MongoReactiveEntitiesOperation createMongoReplaceOneInBulkOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities) {
        return new MongoReactiveEntitiesOperation<>(ctx, persistentEntity, entities, false) {

            final MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
            final MongoCollection collection = getCollection(mongoDatabase, persistentEntity, BsonDocument.class);

            @Override
            protected void collectAutoPopulatedPreviousValues() {
                entities = entities.map(list -> {
                    for (Data d : list) {
                        if (d.vetoed) {
                            continue;
                        }
                        d.filter = createFilterIdAndVersion(persistentEntity, d.entity, collection.getCodecRegistry());
                    }
                    return list;
                });
            }

            @Override
            protected void execute() throws RuntimeException {
                Mono, Long>> entitiesWithRowsUpdated = entities.flatMap(list -> {
                    List> replaces = new ArrayList<>(list.size());
                    for (Data d : list) {
                        if (d.vetoed) {
                            continue;
                        }
                        Bson filter = (Bson) d.filter;
                        if (QUERY_LOG.isDebugEnabled()) {
                            QUERY_LOG.debug("Executing Mongo 'replaceOne' with filter: {}", filter.toBsonDocument().toJson());
                        }
                        BsonDocument bsonDocument = BsonDocumentWrapper.asBsonDocument(d.entity, mongoDatabase.getCodecRegistry());
                        bsonDocument.remove("_id");
                        replaces.add(new ReplaceOneModel<>(filter, bsonDocument, getReplaceOptions(ctx.annotationMetadata)));
                    }
                    return Mono.from(collection.bulkWrite(ctx.clientSession, replaces)).map(bulkWriteResult -> {
                        if (persistentEntity.getVersion() != null) {
                            checkOptimisticLocking(replaces.size(), bulkWriteResult.getModifiedCount());
                        }
                        return Tuples.of(list, (long) bulkWriteResult.getModifiedCount());
                    });
                }).cache();
                entities = entitiesWithRowsUpdated.flatMap(t -> Mono.just(t.getT1()));
                rowsUpdated = entitiesWithRowsUpdated.map(Tuple2::getT2);
            }
        };
    }

    private  MongoReactiveEntitiesOperation createMongoUpdateOneInBulkOperation(MongoOperationContext ctx,
                                                                                      RuntimePersistentEntity persistentEntity,
                                                                                      Iterable entities,
                                                                                      MongoStoredQuery storedQuery) {
        return new MongoReactiveEntitiesOperation<>(ctx, persistentEntity, entities, false) {

            @Override
            protected void execute() throws RuntimeException {
                Mono, Long>> entitiesWithRowsUpdated = entities.flatMap(list -> {
                    List> updates = new ArrayList<>(list.size());
                    for (Data d : list) {
                        if (d.vetoed) {
                            continue;
                        }
                        MongoUpdate updateOne = storedQuery.getUpdateOne(d.entity);
                        updates.add(new UpdateOneModel<>(updateOne.getFilter(), updateOne.getUpdate(), updateOne.getOptions()));
                    }
                    Mono modifiedCount = Mono.from(getCollection(ctx, persistentEntity).bulkWrite(ctx.clientSession, updates)).map(result -> {
                        if (storedQuery.isOptimisticLock()) {
                            checkOptimisticLocking(updates.size(), result.getModifiedCount());
                        }
                        return (long) result.getModifiedCount();
                    });
                    return modifiedCount.map(count -> Tuples.of(list, count));
                }).cache();
                entities = entitiesWithRowsUpdated.flatMap(t -> Mono.just(t.getT1()));
                rowsUpdated = entitiesWithRowsUpdated.map(Tuple2::getT2);
            }
        };
    }

    private  MongoReactiveEntityOperation createMongoDeleteOneOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, T entity) {
        return new MongoReactiveEntityOperation<>(ctx, persistentEntity, entity, false) {

            final MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
            final MongoCollection collection = getCollection(mongoDatabase, persistentEntity, persistentEntity.getIntrospection().getBeanType());

            @Override
            protected void collectAutoPopulatedPreviousValues() {
                data = data.map(d -> {
                    if (d.vetoed) {
                        return d;
                    }
                    d.filter = createFilterIdAndVersion(persistentEntity, d.entity, collection.getCodecRegistry());
                    return d;
                });
            }

            @Override
            protected void execute() throws RuntimeException {
                data = data.flatMap(d -> {
                    Bson filter = (Bson) d.filter;
                    if (QUERY_LOG.isDebugEnabled()) {
                        QUERY_LOG.debug("Executing Mongo 'deleteOne' with filter: {}", filter.toBsonDocument().toJson());
                    }
                    return Mono.from(getCollection(persistentEntity, ctx.repositoryType).deleteOne(ctx.clientSession, filter, getDeleteOptions(ctx.annotationMetadata))).map(deleteResult -> {
                        d.rowsUpdated = (int) deleteResult.getDeletedCount();
                        if (persistentEntity.getVersion() != null) {
                            checkOptimisticLocking(1, d.rowsUpdated);
                        }
                        return d;
                    });
                });

            }

        };
    }

    private  MongoReactiveEntitiesOperation createMongoDeleteManyOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities) {
        return new MongoReactiveEntitiesOperation<>(ctx, persistentEntity, entities, false) {

            final MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
            final MongoCollection collection = getCollection(mongoDatabase, persistentEntity, persistentEntity.getIntrospection().getBeanType());

            @Override
            protected void collectAutoPopulatedPreviousValues() {
                entities = entities.map(list -> {
                    for (Data d : list) {
                        if (d.vetoed) {
                            continue;
                        }
                        d.filter = createFilterIdAndVersion(persistentEntity, d.entity, collection.getCodecRegistry());
                    }
                    return list;
                });
            }

            @Override
            protected void execute() throws RuntimeException {
                Mono, Long>> entitiesWithRowsUpdated = entities.flatMap(list -> {
                    List filters = list.stream().filter(d -> !d.vetoed).map(d -> ((Bson) d.filter)).toList();
                    Mono modifiedCount;
                    if (!filters.isEmpty()) {
                        Bson filter = Filters.or(filters);
                        if (QUERY_LOG.isDebugEnabled()) {
                            QUERY_LOG.debug("Executing Mongo 'deleteMany' with filter: {}", filter.toBsonDocument().toJson());
                        }
                        modifiedCount = Mono.from(collection.deleteMany(ctx.clientSession, filter, getDeleteOptions(ctx.annotationMetadata))).map(DeleteResult::getDeletedCount);
                    } else {
                        modifiedCount = Mono.just(0L);
                    }
                    if (persistentEntity.getVersion() != null) {
                        modifiedCount = modifiedCount.map(count -> {
                            checkOptimisticLocking(filters.size(), count);
                            return count;
                        });
                    }
                    return modifiedCount.map(count -> Tuples.of(list, count));
                }).cache();
                entities = entitiesWithRowsUpdated.flatMap(t -> Mono.just(t.getT1()));
                rowsUpdated = entitiesWithRowsUpdated.map(Tuple2::getT2);
            }
        };
    }

    private  MongoReactiveEntitiesOperation createMongoDeleteOneInBulkOperation(MongoOperationContext ctx,
                                                                                      RuntimePersistentEntity persistentEntity,
                                                                                      Iterable entities,
                                                                                      MongoStoredQuery storedQuery) {
        return new MongoReactiveEntitiesOperation<>(ctx, persistentEntity, entities, false) {

            @Override
            protected void execute() throws RuntimeException {
                Mono, Long>> entitiesWithRowsUpdated = entities.flatMap(list -> {
                    List> deletes = new ArrayList<>(list.size());
                    for (Data d : list) {
                        if (d.vetoed) {
                            continue;
                        }
                        MongoDelete deleteOne = storedQuery.getDeleteOne(d.entity);
                        deletes.add(new DeleteOneModel<>(deleteOne.getFilter(), deleteOne.getOptions()));
                    }
                    return Mono.from(getCollection(ctx, persistentEntity).bulkWrite(ctx.clientSession, deletes)).map(bulkWriteResult -> {
                        if (storedQuery.isOptimisticLock()) {
                            checkOptimisticLocking(deletes.size(), bulkWriteResult.getDeletedCount());
                        }
                        return Tuples.of(list, (long) bulkWriteResult.getDeletedCount());
                    });
                }).cache();
                entities = entitiesWithRowsUpdated.flatMap(t -> Mono.just(t.getT1()));
                rowsUpdated = entitiesWithRowsUpdated.map(Tuple2::getT2);
            }
        };
    }

    private  MongoReactiveEntitiesOperation createMongoInsertManyOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities) {
        return new MongoReactiveEntitiesOperation<>(ctx, persistentEntity, entities, true) {

            @Override
            protected void execute() throws RuntimeException {
                entities = entities.flatMap(list -> {
                    List toInsert = list.stream().filter(d -> !d.vetoed).map(d -> d.entity).toList();
                    if (toInsert.isEmpty()) {
                        return Mono.just(list);
                    }

                    MongoCollection collection = getCollection(persistentEntity, ctx.repositoryType);
                    if (QUERY_LOG.isDebugEnabled()) {
                        QUERY_LOG.debug("Executing Mongo 'insertMany' for collection: {} with documents: {}", collection.getNamespace().getFullName(), toInsert);
                    }
                    return Mono.from(collection.insertMany(ctx.clientSession, toInsert, getInsertManyOptions(ctx.annotationMetadata))).flatMap(insertManyResult -> {
                        if (hasGeneratedId) {
                            Map insertedIds = insertManyResult.getInsertedIds();
                            RuntimePersistentProperty identity = persistentEntity.getIdentity();
                            BeanProperty idProperty = identity.getProperty();
                            int index = 0;
                            for (Data d : list) {
                                if (!d.vetoed) {
                                    BsonValue id = insertedIds.get(index);
                                    if (id == null) {
                                        throw new DataAccessException("Failed to generate ID for entity: " + d.entity);
                                    }
                                    d.entity = updateEntityId(idProperty, d.entity, id);
                                }
                                index++;
                            }
                        }
                        return Mono.just(list);
                    });

                });
            }
        };
    }

    private abstract class MongoReactiveEntityOperation extends AbstractReactiveEntityOperations {

        /**
         * Create a new instance.
         *
         * @param ctx              The context
         * @param persistentEntity The RuntimePersistentEntity
         * @param entity           The entity instance
         * @param insert           If the operation does the insert
         */
        protected MongoReactiveEntityOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, T entity, boolean insert) {
            super(ctx, DefaultReactiveMongoRepositoryOperations.this.cascadeOperations, DefaultReactiveMongoRepositoryOperations.this.conversionService, DefaultReactiveMongoRepositoryOperations.this.entityEventRegistry, persistentEntity, entity, insert);
        }

        @Override
        protected void collectAutoPopulatedPreviousValues() {
        }
    }

    private abstract class MongoReactiveEntitiesOperation extends AbstractReactiveEntitiesOperations {

        protected MongoReactiveEntitiesOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities, boolean insert) {
            super(ctx, DefaultReactiveMongoRepositoryOperations.this.cascadeOperations, DefaultReactiveMongoRepositoryOperations.this.conversionService, DefaultReactiveMongoRepositoryOperations.this.entityEventRegistry, persistentEntity, entities, insert);
        }

        @Override
        protected void collectAutoPopulatedPreviousValues() {
        }

    }

    protected static final class MongoOperationContext extends OperationContext {

        private final ClientSession clientSession;

        public MongoOperationContext(ClientSession clientSession, Class repositoryType, AnnotationMetadata annotationMetadata) {
            super(annotationMetadata, repositoryType);
            this.clientSession = clientSession;
        }
    }
}