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

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

There is a newer version: 4.9.3
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.bulk.BulkWriteResult;
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.ClientSession;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
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.client.result.InsertManyResult;
import com.mongodb.client.result.InsertOneResult;
import com.mongodb.client.result.UpdateResult;
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.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.data.connection.ConnectionDefinition;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
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.RequiresSyncMongo;
import io.micronaut.data.mongodb.operations.options.MongoAggregationOptions;
import io.micronaut.data.mongodb.operations.options.MongoFindOptions;
import io.micronaut.data.mongodb.session.MongoConnectionOperations;
import io.micronaut.data.operations.async.AsyncCapableRepository;
import io.micronaut.data.operations.reactive.ReactiveCapableRepository;
import io.micronaut.data.operations.reactive.ReactiveRepositoryOperations;
import io.micronaut.data.runtime.convert.DataConversionService;
import io.micronaut.data.runtime.date.DateTimeProvider;
import io.micronaut.data.runtime.operations.ExecutorAsyncOperations;
import io.micronaut.data.runtime.operations.ExecutorReactiveOperations;
import io.micronaut.data.runtime.operations.internal.AbstractSyncEntitiesOperations;
import io.micronaut.data.runtime.operations.internal.AbstractSyncEntityOperations;
import io.micronaut.data.runtime.operations.internal.OperationContext;
import io.micronaut.data.runtime.operations.internal.SyncCascadeOperations;
import io.micronaut.inject.qualifiers.Qualifiers;
import jakarta.inject.Named;
import org.bson.BsonDocument;
import org.bson.BsonDocumentWrapper;
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Default Mongo repository operations.
 *
 * @author Denis Stepanov
 * @since 3.3
 */
@RequiresSyncMongo
@EachBean(MongoClient.class)
@Internal
final class DefaultMongoRepositoryOperations extends AbstractMongoRepositoryOperations implements
        MongoRepositoryOperations,
        AsyncCapableRepository,
        ReactiveCapableRepository,
        SyncCascadeOperations.SyncCascadeOperationsHelper {
    private final MongoClient mongoClient;
    private final SyncCascadeOperations cascadeOperations;
    private final MongoConnectionOperations connectionOperations;
    private ExecutorAsyncOperations asyncOperations;
    private ExecutorService executorService;

    /**
     * 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 Mongo client
     * @param collectionNameProvider      The Mongo collection name provider
     * @param executorService             The executor service
     */
    DefaultMongoRepositoryOperations(@Nullable @Parameter String serverName,
                                     BeanContext beanContext,
                                     DateTimeProvider dateTimeProvider,
                                     RuntimeEntityRegistry runtimeEntityRegistry,
                                     DataConversionService conversionService,
                                     AttributeConverterRegistry attributeConverterRegistry,
                                     MongoClient mongoClient,
                                     MongoCollectionNameProvider collectionNameProvider,
                                     @Named("io") @Nullable ExecutorService executorService) {
        super(dateTimeProvider, runtimeEntityRegistry, conversionService, attributeConverterRegistry, collectionNameProvider,
            beanContext.getBean(MongoDatabaseNameProvider.class, "Primary".equals(serverName) ? null : Qualifiers.byName(serverName)));
        this.mongoClient = mongoClient;
        this.cascadeOperations = new SyncCascadeOperations<>(conversionService, this);
        boolean isPrimary = "Primary".equals(serverName);
        this.connectionOperations = beanContext.getBean(MongoConnectionOperations.class, isPrimary ? null : Qualifiers.byName(serverName));
        this.executorService = executorService;
    }

    @Override
    public  T 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 collection.find(clientSession, filter, type).first();
        });
    }

    @Override
    public  R 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);
            }
        });
    }

    private  R getCount(ClientSession clientSession, MongoPreparedQuery preparedQuery) {
        Class resultType = preparedQuery.getResultType();
        RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity();
        MongoDatabase database = getDatabase(preparedQuery);
        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()).collect(Collectors.toList()));
            }
            R result = aggregate(clientSession, preparedQuery, BsonDocument.class)
                    .map(bsonDocument -> convertResult(database.getCodecRegistry(), resultType, bsonDocument, false))
                    .first();
            if (result == null) {
                result = conversionService.convertRequired(0, resultType);
            }
            return result;
        } 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());
            }
            long count = getCollection(database, persistentEntity, BsonDocument.class)
                    .countDocuments(clientSession, filter);
            return conversionService.convertRequired(count, resultType);
        }
    }

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

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

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

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

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

    @Override
    public  Iterable findAll(PreparedQuery preparedQuery) {
        return withClientSession(clientSession -> findAll(clientSession, getMongoPreparedQuery(preparedQuery), false));
    }

    @Override
    public  Stream findStream(PreparedQuery preparedQuery) {
        return withClientSession(clientSession -> {
            MongoIterable iterable = (MongoIterable) findAll(clientSession, getMongoPreparedQuery(preparedQuery), true);
            MongoCursor iterator = iterable.iterator();
            Spliterators.AbstractSpliterator spliterator = new Spliterators.AbstractSpliterator(Long.MAX_VALUE,
                    Spliterator.ORDERED | Spliterator.IMMUTABLE) {
                @Override
                public boolean tryAdvance(Consumer action) {
                    if (iterator.hasNext()) {
                        action.accept(iterator.next());
                        return true;
                    }
                    iterator.close();
                    return false;
                }
            };
            return StreamSupport.stream(spliterator, false).onClose(iterator::close);
        });
    }

    private  Iterable findAll(ClientSession clientSession, MongoPreparedQuery preparedQuery, boolean stream) {
        if (preparedQuery.isCount()) {
            return Collections.singletonList(getCount(clientSession, preparedQuery));
        }
        if (preparedQuery.isAggregate()) {
            return findAllAggregated(clientSession, preparedQuery, stream);
        }
        return findAllFiltered(clientSession, preparedQuery, stream);
    }

    private  R findOneFiltered(ClientSession clientSession, MongoPreparedQuery preparedQuery) {
        return find(clientSession, preparedQuery)
                .limit(1)
                .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;
                }).first();
    }

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

    private  Iterable findAllAggregated(ClientSession clientSession,
                                                 MongoPreparedQuery preparedQuery,
                                                 boolean stream) {
        Pageable pageable = preparedQuery.getPageable();
        int limit = pageable == Pageable.UNPAGED ? -1 : pageable.getSize();
        Class type = preparedQuery.getRootEntity();
        Class resultType = preparedQuery.getResultType();
        MongoIterable aggregate;
        if (!resultType.isAssignableFrom(type)) {
            MongoDatabase database = getDatabase(preparedQuery);
            aggregate = aggregate(clientSession, preparedQuery, BsonDocument.class)
                    .map(result -> convertResult(database.getCodecRegistry(), resultType, result, preparedQuery.isDtoProjection()));
        } else {
            aggregate = aggregate(clientSession, preparedQuery, resultType);
        }
        return stream ? aggregate : aggregate.into(new ArrayList<>(limit > 0 ? limit : 20));
    }

    private  Iterable findAllFiltered(ClientSession clientSession,
                                               MongoPreparedQuery preparedQuery,
                                               boolean stream) {
        Pageable pageable = preparedQuery.getPageable();
        int limit = pageable == Pageable.UNPAGED ? -1 : pageable.getSize();
        Class type = preparedQuery.getRootEntity();
        Class resultType = preparedQuery.getResultType();
        MongoIterable findIterable;
        if (!resultType.isAssignableFrom(type)) {
            MongoDatabase database = getDatabase(preparedQuery);
            findIterable = find(clientSession, preparedQuery, BsonDocument.class)
                    .map(result -> convertResult(database.getCodecRegistry(), resultType, result, preparedQuery.isDtoProjection()));
        } else {
            findIterable = find(clientSession, preparedQuery);
        }
        return stream ? findIterable : findIterable.into(new ArrayList<>(limit > 0 ? limit : 20));
    }

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

    private  FindIterable 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);
        FindIterable findIterable = collection.find(clientSession, resultType);
        return applyFindOptions(find.getOptions(), findIterable);
    }

    private  FindIterable applyFindOptions(@Nullable MongoFindOptions findOptions, FindIterable 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  AggregateIterable 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);
        }
        AggregateIterable aggregateIterable = collection.aggregate(clientSession, aggregation.getPipeline(), resultType);
        return applyAggregateOptions(aggregation.getOptions(), aggregateIterable);
    }

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

    private  AggregateIterable applyAggregateOptions(@Nullable MongoAggregationOptions aggregateOptions, AggregateIterable 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 (maxAwaitTimeMS != 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;
    }

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

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

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

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

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

    @Override
    public  Optional deleteAll(DeleteBatchOperation operation) {
        return withClientSession(clientSession -> {
            MongoOperationContext ctx = new MongoOperationContext(clientSession, operation.getAnnotationMetadata(), operation.getRepositoryType());
            StoredQuery storedQuery = operation.getStoredQuery();
            if (storedQuery != null) {
                MongoStoredQuery mongoStoredQuery = getMongoStoredQuery(storedQuery);
                MongoEntitiesOperation op = createMongoDeleteOneInBulkOperation(ctx, mongoStoredQuery.getRuntimePersistentEntity(), operation, mongoStoredQuery);
                op.delete();
                return Optional.of(op.modifiedCount);
            }
            RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(operation.getRootEntity());
            if (operation.all()) {
                MongoDatabase mongoDatabase = getDatabase(persistentEntity, operation.getRepositoryType());
                long deletedCount = getCollection(mongoDatabase, persistentEntity, persistentEntity.getIntrospection().getBeanType()).deleteMany(EMPTY).getDeletedCount();
                return Optional.of(deletedCount);
            }
            MongoEntitiesOperation op = createMongoDeleteManyOperation(ctx, persistentEntity, operation);
            op.delete();
            return Optional.of(op.modifiedCount);
        });
    }

    @Override
    public Optional 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());
            }
            UpdateResult updateResult = getCollection(mongoPreparedQuery)
                    .updateMany(clientSession, updateMany.getFilter(), updateMany.getUpdate(), updateMany.getOptions());
            if (preparedQuery.isOptimisticLock()) {
                checkOptimisticLocking(1, (int) updateResult.getModifiedCount());
            }
            return Optional.of(updateResult.getModifiedCount());
        });
    }

    @Override
    public Optional 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());
            }
            DeleteResult deleteResult = getCollection(mongoPreparedQuery).
                    deleteMany(clientSession, deleteMany.getFilter(), deleteMany.getOptions());
            if (preparedQuery.isOptimisticLock()) {
                checkOptimisticLocking(1, (int) deleteResult.getDeletedCount());
            }
            return Optional.of(deleteResult.getDeletedCount());
        });
    }

    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  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) {
                            for (Object value : ((Iterable) o)) {
                                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(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 MongoDatabase getDatabase(PersistentEntity persistentEntity, Class repositoryClass) {
        return mongoClient.getDatabase(databaseNameProvider.provide(persistentEntity, repositoryClass));
    }

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

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

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

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

    @Override
    public void 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);
        }
        collection.insertOne(ctx.clientSession, association, getInsertOneOptions(ctx.annotationMetadata));
    }

    @Override
    public void persistManyAssociationBatch(MongoOperationContext ctx, RuntimeAssociation runtimeAssociation,
                                            Object value,
                                            RuntimePersistentEntity persistentEntity,
                                            Iterable child,
                                            RuntimePersistentEntity childPersistentEntity) {
        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 documents: {}", collection.getNamespace().getFullName(), associations);
        }
        collection.insertMany(ctx.clientSession, associations, getInsertManyOptions(ctx.annotationMetadata));
    }

    private  T withClientSession(Function function) {
        return connectionOperations.execute(ConnectionDefinition.DEFAULT, status -> function.apply(status.getConnection()));
    }

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

            @Override
            protected void execute() throws RuntimeException {
                MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
                MongoCollection collection = getCollection(mongoDatabase, persistentEntity, persistentEntity.getIntrospection().getBeanType());
                if (QUERY_LOG.isDebugEnabled()) {
                    QUERY_LOG.debug("Executing Mongo 'insertOne' with entity: {}", entity);
                }
                InsertOneResult insertOneResult = collection.insertOne(ctx.clientSession, entity, getInsertOneOptions(ctx.annotationMetadata));
                BsonValue insertedId = insertOneResult.getInsertedId();
                BeanProperty property = persistentEntity.getIdentity().getProperty();
                if (property.get(entity) == null) {
                    entity = updateEntityId(property, entity, insertedId);
                }
            }
        };
    }

    private  MongoEntityOperation createMongoReplaceOneOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, T entity) {
        return new MongoEntityOperation(ctx, persistentEntity, entity, false) {

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

            @Override
            protected void collectAutoPopulatedPreviousValues() {
                filter = createFilterIdAndVersion(persistentEntity, entity, mongoDatabase.getCodecRegistry());
            }

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

        };
    }

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

            @Override
            protected void collectAutoPopulatedPreviousValues() {
            }

            @Override
            protected void execute() throws RuntimeException {
                List> updates = new ArrayList<>(entities.size());
                for (Data d : entities) {
                    if (d.vetoed) {
                        continue;
                    }
                    MongoUpdate updateOne = storedQuery.getUpdateOne(d.entity);
                    if (QUERY_LOG.isDebugEnabled()) {
                        QUERY_LOG.debug("Executing Mongo 'updateOne' with filter: {} and update: {}", updateOne.getFilter().toBsonDocument().toJson(), updateOne.getUpdate().toBsonDocument().toJson());
                    }
                    updates.add(new UpdateOneModel<>(updateOne.getFilter(), updateOne.getUpdate(), updateOne.getOptions()));
                }
                BulkWriteResult bulkWriteResult = getCollection(ctx, persistentEntity).bulkWrite(ctx.clientSession, updates);
                modifiedCount += bulkWriteResult.getModifiedCount();
                if (persistentEntity.getVersion() != null) {
                    checkOptimisticLocking(updates.size(), (int) modifiedCount);
                }
            }
        };
    }

    private  MongoEntitiesOperation createMongoReplaceOneInBulkOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities) {
        return new MongoEntitiesOperation(ctx, persistentEntity, entities, false) {

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

            @Override
            protected void collectAutoPopulatedPreviousValues() {
                filters = entities.stream()
                        .collect(Collectors.toMap(d -> d, d -> createFilterIdAndVersion(persistentEntity, d.entity, collection.getCodecRegistry())));
            }

            @Override
            protected void execute() throws RuntimeException {
                List> replaces = new ArrayList<>(entities.size());
                for (Data d : entities) {
                    if (d.vetoed) {
                        continue;
                    }
                    Bson filter = filters.get(d);
                    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)));
                }
                BulkWriteResult bulkWriteResult = collection.bulkWrite(ctx.clientSession, replaces);
                modifiedCount = bulkWriteResult.getModifiedCount();
                if (persistentEntity.getVersion() != null) {
                    checkOptimisticLocking(replaces.size(), (int) modifiedCount);
                }
            }
        };
    }

    private  MongoEntityOperation createMongoDeleteOneOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, T entity) {
        return new MongoEntityOperation(ctx, persistentEntity, entity, false) {

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

            @Override
            protected void collectAutoPopulatedPreviousValues() {
                filter = createFilterIdAndVersion(persistentEntity, entity, collection.getCodecRegistry());
            }

            @Override
            protected void execute() throws RuntimeException {
                if (QUERY_LOG.isDebugEnabled()) {
                    QUERY_LOG.debug("Executing Mongo 'deleteOne' with filter: {}", filter.toBsonDocument().toJson());
                }
                DeleteResult deleteResult = collection.deleteOne(ctx.clientSession, filter, getDeleteOptions(ctx.annotationMetadata));
                modifiedCount = deleteResult.getDeletedCount();
                if (persistentEntity.getVersion() != null) {
                    checkOptimisticLocking(1, (int) modifiedCount);
                }
            }
        };
    }

    private  MongoEntitiesOperation createMongoDeleteManyOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities) {
        return new MongoEntitiesOperation(ctx, persistentEntity, entities, false) {

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

            @Override
            protected void collectAutoPopulatedPreviousValues() {
                filters = entities.stream().collect(Collectors.toMap(d -> d, d -> createFilterIdAndVersion(persistentEntity, d.entity, collection.getCodecRegistry())));
            }

            @Override
            protected void execute() throws RuntimeException {
                List filters = entities.stream().filter(d -> !d.vetoed).map(d -> this.filters.get(d)).collect(Collectors.toList());
                if (!filters.isEmpty()) {
                    Bson filter = Filters.or(filters);
                    if (QUERY_LOG.isDebugEnabled()) {
                        QUERY_LOG.debug("Executing Mongo 'deleteMany' with filter: {}", filter.toBsonDocument().toJson());
                    }
                    DeleteResult deleteResult = collection.deleteMany(ctx.clientSession, filter, getDeleteOptions(ctx.annotationMetadata));
                    modifiedCount = deleteResult.getDeletedCount();
                }
                if (persistentEntity.getVersion() != null) {
                    int expected = (int) entities.stream().filter(d -> !d.vetoed).count();
                    checkOptimisticLocking(expected, (int) modifiedCount);
                }
            }
        };
    }

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

            @Override
            protected void execute() throws RuntimeException {
                List> deletes = new ArrayList<>(entities.size());
                for (Data d : entities) {
                    if (d.vetoed) {
                        continue;
                    }
                    MongoDelete deleteOne = storedQuery.getDeleteOne(d.entity);
                    if (QUERY_LOG.isDebugEnabled()) {
                        QUERY_LOG.debug("Executing Mongo 'deleteOne' with filter: {} ", deleteOne.getFilter().toBsonDocument().toJson());
                    }
                    deletes.add(new DeleteOneModel<>(deleteOne.getFilter(), deleteOne.getOptions()));
                }
                BulkWriteResult bulkWriteResult = getCollection(ctx, persistentEntity).bulkWrite(ctx.clientSession, deletes);
                modifiedCount = bulkWriteResult.getDeletedCount();
                if (persistentEntity.getVersion() != null) {
                    checkOptimisticLocking(deletes.size(), (int) modifiedCount);
                }
            }
        };
    }

    private  MongoEntitiesOperation createMongoInsertManyOperation(MongoOperationContext ctx, RuntimePersistentEntity persistentEntity, Iterable entities) {
        return new MongoEntitiesOperation(ctx, persistentEntity, entities, true) {

            @Override
            protected void execute() throws RuntimeException {
                List toInsert = entities.stream().filter(d -> !d.vetoed).map(d -> d.entity).collect(Collectors.toList());
                if (toInsert.isEmpty()) {
                    return;
                }
                if (QUERY_LOG.isDebugEnabled()) {
                    QUERY_LOG.debug("Executing Mongo 'insertMany' with entities: {}", toInsert);
                }
                MongoDatabase mongoDatabase = getDatabase(persistentEntity, ctx.repositoryType);
                InsertManyResult insertManyResult = getCollection(mongoDatabase, persistentEntity, persistentEntity.getIntrospection().getBeanType())
                        .insertMany(ctx.clientSession, toInsert, getInsertManyOptions(ctx.annotationMetadata));
                if (hasGeneratedId) {
                    Map insertedIds = insertManyResult.getInsertedIds();
                    RuntimePersistentProperty identity = persistentEntity.getIdentity();
                    BeanProperty idProperty = identity.getProperty();
                    int index = 0;
                    for (Data d : entities) {
                        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++;
                    }
                }
            }
        };
    }

    @NonNull
    @Override
    public ExecutorAsyncOperations async() {
        ExecutorAsyncOperations asyncOperations = this.asyncOperations;
        if (asyncOperations == null) {
            synchronized (this) { // double check
                asyncOperations = this.asyncOperations;
                if (asyncOperations == null) {
                    asyncOperations = new ExecutorAsyncOperations(
                            this,
                            executorService != null ? executorService : newLocalThreadPool()
                    );
                    this.asyncOperations = asyncOperations;
                }
            }
        }
        return asyncOperations;
    }

    @NonNull
    private ExecutorService newLocalThreadPool() {
        this.executorService = Executors.newCachedThreadPool();
        return executorService;
    }

    @NonNull
    @Override
    public ReactiveRepositoryOperations reactive() {
        return new ExecutorReactiveOperations(async(), conversionService);
    }

    private abstract class MongoEntityOperation extends AbstractSyncEntityOperations {

        protected long modifiedCount;

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

        @Override
        protected void collectAutoPopulatedPreviousValues() {
        }
    }

    private abstract class MongoEntitiesOperation extends AbstractSyncEntitiesOperations {

        protected long modifiedCount;

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

        @Override
        protected void collectAutoPopulatedPreviousValues() {
        }

    }

    protected static class MongoOperationContext extends OperationContext {

        private final ClientSession clientSession;

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