Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.micronaut.data.cosmos.operations.DefaultReactiveCosmosRepositoryOperations Maven / Gradle / Ivy
/*
* 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.cosmos.operations;
import com.azure.cosmos.CosmosAsyncClient;
import com.azure.cosmos.CosmosAsyncContainer;
import com.azure.cosmos.CosmosAsyncDatabase;
import com.azure.cosmos.CosmosException;
import com.azure.cosmos.implementation.RequestOptions;
import com.azure.cosmos.implementation.batch.ItemBulkOperation;
import com.azure.cosmos.models.CosmosBulkItemResponse;
import com.azure.cosmos.models.CosmosItemOperation;
import com.azure.cosmos.models.CosmosItemOperationType;
import com.azure.cosmos.models.CosmosItemRequestOptions;
import com.azure.cosmos.models.CosmosItemResponse;
import com.azure.cosmos.models.CosmosQueryRequestOptions;
import com.azure.cosmos.models.FeedResponse;
import com.azure.cosmos.models.PartitionKey;
import com.azure.cosmos.models.SqlParameter;
import com.azure.cosmos.models.SqlQuerySpec;
import com.azure.cosmos.util.CosmosPagedFlux;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.micronaut.aop.MethodInvocationContext;
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.convert.ConversionService;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.annotation.Relation;
import io.micronaut.data.cosmos.common.Constants;
import io.micronaut.data.cosmos.common.CosmosAccessException;
import io.micronaut.data.cosmos.common.CosmosEntity;
import io.micronaut.data.cosmos.common.CosmosUtils;
import io.micronaut.data.cosmos.config.CosmosDatabaseConfiguration;
import io.micronaut.data.document.model.query.builder.CosmosSqlQueryBuilder2;
import io.micronaut.data.event.EntityEventListener;
import io.micronaut.data.exceptions.EmptyResultException;
import io.micronaut.data.exceptions.NonUniqueResultException;
import io.micronaut.data.exceptions.OptimisticLockException;
import io.micronaut.data.intercept.annotation.DataMethod;
import io.micronaut.data.model.DataType;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.PersistentProperty;
import io.micronaut.data.model.runtime.AttributeConverterRegistry;
import io.micronaut.data.model.runtime.BatchOperation;
import io.micronaut.data.model.runtime.DeleteBatchOperation;
import io.micronaut.data.model.runtime.DeleteOperation;
import io.micronaut.data.model.runtime.EntityOperation;
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.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.operations.reactive.ReactiveRepositoryOperations;
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.AbstractRepositoryOperations;
import io.micronaut.data.runtime.operations.internal.OperationContext;
import io.micronaut.data.runtime.operations.internal.sql.SqlPreparedQuery;
import io.micronaut.data.runtime.query.MethodContextAwareStoredQueryDecorator;
import io.micronaut.data.runtime.query.PreparedQueryDecorator;
import io.micronaut.data.runtime.query.internal.QueryResultStoredQuery;
import io.netty.handler.codec.http.HttpResponseStatus;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.math.BigDecimal;
import java.sql.Time;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
/**
* The reactive Cosmos DB repository operations implementation.
*
* @author radovanradic
* @since 3.9.0
*/
@Singleton
@Internal
@SuppressWarnings({"java:S110", "java:S107"}) // Disabled SonarLint rules: Inheritance tree of classes should not be too deep, Methods should not have too many parameters
public final class DefaultReactiveCosmosRepositoryOperations extends AbstractRepositoryOperations implements
ReactorReactiveRepositoryOperations,
ReactiveRepositoryOperations,
MethodContextAwareStoredQueryDecorator,
PreparedQueryDecorator {
// This should return exact collection item by the id in given container
private static final String FIND_ONE_DEFAULT_QUERY = "SELECT * FROM root WHERE root.id = @ROOT_ID";
private static final String FAILED_TO_QUERY_ITEMS = "Failed to query items: ";
private static final Logger QUERY_LOG = DataSettings.QUERY_LOG;
private static final Logger LOG = LoggerFactory.getLogger(DefaultReactiveCosmosRepositoryOperations.class);
private final CosmosSerde cosmosSerde;
private final CosmosAsyncDatabase cosmosAsyncDatabase;
private CosmosSqlQueryBuilder2 defaultCosmosSqlQueryBuilder;
private final CosmosDiagnosticsProcessor cosmosDiagnosticsProcessor;
private final boolean queryMetricsEnabled;
/**
* Default constructor.
*
* @param dateTimeProvider The date time provider
* @param runtimeEntityRegistry The entity registry
* @param conversionService The conversion service
* @param attributeConverterRegistry The attribute converter registry
* @param cosmosAsyncClient The Cosmos async client
* @param cosmosSerde The Cosmos de/serialization helper
* @param cosmosDiagnosticsProcessor The Cosmos diagnostics processor, can be null
* @param configuration The Cosmos database configuration
*/
public DefaultReactiveCosmosRepositoryOperations(DateTimeProvider dateTimeProvider,
RuntimeEntityRegistry runtimeEntityRegistry,
DataConversionService conversionService,
AttributeConverterRegistry attributeConverterRegistry,
CosmosAsyncClient cosmosAsyncClient,
CosmosSerde cosmosSerde,
@Nullable
CosmosDiagnosticsProcessor cosmosDiagnosticsProcessor,
CosmosDatabaseConfiguration configuration) {
super(dateTimeProvider, runtimeEntityRegistry, conversionService, attributeConverterRegistry);
this.cosmosSerde = cosmosSerde;
this.cosmosAsyncDatabase = cosmosAsyncClient.getDatabase(configuration.getDatabaseName());
this.cosmosDiagnosticsProcessor = cosmosDiagnosticsProcessor;
this.queryMetricsEnabled = configuration.isQueryMetricsEnabled();
}
@Override
public PreparedQuery decorate(PreparedQuery preparedQuery) {
return new CosmosSqlPreparedQuery<>(preparedQuery);
}
@Override
public StoredQuery decorate(MethodInvocationContext, ?> context, StoredQuery storedQuery) {
if (defaultCosmosSqlQueryBuilder == null) {
defaultCosmosSqlQueryBuilder = new CosmosSqlQueryBuilder2(context.getAnnotationMetadata());
}
String update = null;
if (storedQuery instanceof QueryResultStoredQuery queryResultStoredQuery) {
update = queryResultStoredQuery.getQueryResult().getUpdate();
}
RuntimePersistentEntity runtimePersistentEntity = runtimeEntityRegistry.getEntity(storedQuery.getRootEntity());
return new CosmosSqlStoredQuery<>(storedQuery, runtimePersistentEntity, defaultCosmosSqlQueryBuilder, update);
}
@Override
@NonNull
public Mono findOne(@NonNull Class type, Object id) {
RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(type);
CosmosAsyncContainer container = getContainer(persistentEntity);
final SqlParameter param = new SqlParameter("@ROOT_ID", id.toString());
final SqlQuerySpec querySpec = new SqlQuerySpec(FIND_ONE_DEFAULT_QUERY, param);
logQuery(querySpec);
final CosmosQueryRequestOptions options = createCosmosQueryRequestOptions();
if (isIdPartitionKey(persistentEntity)) {
options.setPartitionKey(new PartitionKey(id.toString()));
}
CosmosPagedFlux result = container.queryItems(querySpec, options, ObjectNode.class);
return result.byPage().flatMap(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS, response.getCosmosDiagnostics(),
response.getActivityId(), response.getRequestCharge());
Iterator iterator = response.getResults().iterator();
if (iterator.hasNext()) {
ObjectNode item = iterator.next();
if (iterator.hasNext()) {
return Flux.error(new NonUniqueResultException());
}
return Mono.just(cosmosSerde.deserialize(persistentEntity, item, Argument.of(type)));
}
return Flux.empty();
}).onErrorMap(e -> CosmosUtils.cosmosAccessException(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS,
"Failed to query item by id", e)).next();
}
@Override
public Mono exists(@NonNull PreparedQuery pq) {
SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq);
preparedQuery.attachPageable(preparedQuery.getPageable(), true);
preparedQuery.prepare(null);
SqlQuerySpec querySpec = new SqlQuerySpec(preparedQuery.getQuery(), new ParameterBinder().bindParameters(preparedQuery));
logQuery(querySpec);
CosmosPagedFlux result = getCosmosResults(preparedQuery, querySpec, ObjectNode.class);
return result.byPage().flatMap(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS, response.getCosmosDiagnostics(),
response.getActivityId(), response.getRequestCharge());
return Mono.just(response.getResults().iterator().hasNext());
}).onErrorMap(e -> CosmosUtils.cosmosAccessException(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS,
"Failed to execute exists query", e)).next();
}
@Override
@NonNull
public Mono findOne(@NonNull PreparedQuery pq) {
SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq);
preparedQuery.attachPageable(preparedQuery.getPageable(), true);
preparedQuery.prepare(null);
SqlQuerySpec querySpec = new SqlQuerySpec(preparedQuery.getQuery(), new ParameterBinder().bindParameters(preparedQuery));
logQuery(querySpec);
boolean dtoProjection = preparedQuery.isDtoProjection();
boolean isEntity = preparedQuery.getResultDataType() == DataType.ENTITY;
if (isEntity || dtoProjection) {
return findOneEntityOrDto(preparedQuery, querySpec);
} else {
return findOneCustomResult(preparedQuery, querySpec);
}
}
@Override
@NonNull
public Mono findOptional(@NonNull Class type, @NonNull Object id) {
return findOne(type, id).onErrorReturn(EmptyResultException.class, (T) Mono.empty());
}
@Override
@NonNull
public Mono findOptional(@NonNull PreparedQuery preparedQuery) {
return findOne(preparedQuery).onErrorReturn(EmptyResultException.class, (R) Mono.empty());
}
@Override
@NonNull
public Flux findAll(PagedQuery pagedQuery) {
throw new UnsupportedOperationException("The findAll method without an explicit query is not supported. Use findAll(PreparedQuery) instead");
}
@Override
@NonNull
public Mono count(PagedQuery pagedQuery) {
throw new UnsupportedOperationException("The count method without an explicit query is not supported. Use findAll(PreparedQuery) instead");
}
@Override
@NonNull
public Flux findAll(@NonNull PreparedQuery pq) {
SqlPreparedQuery preparedQuery = getSqlPreparedQuery(pq);
preparedQuery.attachPageable(preparedQuery.getPageable(), false);
preparedQuery.prepare(null);
boolean dtoProjection = preparedQuery.isDtoProjection();
boolean isEntity = preparedQuery.getResultDataType() == DataType.ENTITY;
SqlQuerySpec querySpec = new SqlQuerySpec(preparedQuery.getQuery(), new ParameterBinder().bindParameters(preparedQuery));
logQuery(querySpec);
if (isEntity || dtoProjection) {
CosmosPagedFlux result = getCosmosResults(preparedQuery, querySpec, ObjectNode.class);
Argument argument;
if (dtoProjection) {
argument = (Argument) Argument.of(ReflectionUtils.getWrapperType(preparedQuery.getResultType()));
return result.map(item -> cosmosSerde.deserialize(item, argument)).onErrorResume(e -> Flux.error(new CosmosAccessException(FAILED_TO_QUERY_ITEMS + e.getMessage(), e)));
} else {
argument = Argument.of(preparedQuery.getResultType());
}
return result.byPage().flatMap(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS, response.getCosmosDiagnostics(),
response.getActivityId(), response.getRequestCharge());
return Flux.fromIterable(response.getResults().stream().map(item -> cosmosSerde.deserialize(item, argument)).toList());
}).onErrorMap(e -> CosmosUtils.cosmosAccessException(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS,
FAILED_TO_QUERY_ITEMS, e));
}
DataType dataType = preparedQuery.getResultDataType();
Class resultType = preparedQuery.getResultType();
CosmosPagedFlux> result = getCosmosResults(preparedQuery, querySpec, getDataTypeClass(dataType));
return result.byPage().flatMap(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS, response.getCosmosDiagnostics(),
response.getActivityId(), response.getRequestCharge());
return Flux.fromIterable(response.getResults().stream().map(item -> {
if (resultType.isInstance(item)) {
return (R) item;
}
if (item != null) {
return conversionService.convertRequired(item, resultType);
}
return null;
}).toList());
}).onErrorMap(e -> CosmosUtils.cosmosAccessException(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS,
FAILED_TO_QUERY_ITEMS, e));
}
@Override
@NonNull
public Mono persist(@NonNull InsertOperation operation) {
CosmosReactiveOperationContext ctx = createCosmosReactiveOperationContext(operation);
CosmosReactiveEntityOperation op = createCosmosInsertOneOperation(ctx, operation.getEntity());
op.persist();
return op.getEntity();
}
@Override
@NonNull
public Mono update(@NonNull UpdateOperation operation) {
CosmosReactiveOperationContext ctx = createCosmosReactiveOperationContext(operation);
CosmosReactiveEntityOperation op = createCosmosReactiveReplaceItemOperation(ctx, operation.getEntity());
op.update();
return op.getEntity();
}
@Override
@NonNull
public Flux updateAll(@NonNull UpdateBatchOperation operation) {
CosmosReactiveOperationContext ctx = createCosmosReactiveOperationContext(operation);
CosmosReactiveEntitiesOperation op = createCosmosReactiveBulkOperation(ctx, operation, BulkOperationType.UPDATE);
op.update();
return op.getEntities();
}
@Override
@NonNull
public Flux persistAll(@NonNull InsertBatchOperation operation) {
CosmosReactiveOperationContext ctx = createCosmosReactiveOperationContext(operation);
CosmosReactiveEntitiesOperation op = createCosmosReactiveBulkOperation(ctx, operation, BulkOperationType.CREATE);
op.persist();
return op.getEntities();
}
@Override
@NonNull
public Mono executeUpdate(@NonNull PreparedQuery, Number> pq) {
if (isRawQuery(pq)) {
return Mono.error(new IllegalStateException("Cosmos Db does not support raw update queries."));
}
SqlPreparedQuery, Number> preparedQuery = getSqlPreparedQuery(pq);
preparedQuery.prepare(null);
RuntimePersistentEntity> persistentEntity = runtimeEntityRegistry.getEntity(preparedQuery.getRootEntity());
String update = preparedQuery.getAnnotationMetadata().stringValue(Query.class, "update").orElse(null);
if (update == null) {
// This is case when query is created via predicate spec
update = getUpdate(preparedQuery);
}
if (update == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("Could not resolve update properties for Cosmos Db entity {} and query [{}]", persistentEntity.getPersistedName(), preparedQuery.getQuery());
}
return Mono.just(0);
}
List updatePropertyList = Arrays.asList(update.split(","));
ParameterBinder parameterBinder = new ParameterBinder(true, updatePropertyList);
SqlQuerySpec querySpec = new SqlQuerySpec(preparedQuery.getQuery(), parameterBinder.bindParameters(preparedQuery));
Map propertiesToUpdate = parameterBinder.getPropertiesToUpdate();
if (propertiesToUpdate.isEmpty()) {
if (LOG.isWarnEnabled()) {
LOG.warn("No properties found to be updated for Cosmos Db entity {} and query [{}]", persistentEntity.getPersistedName(), preparedQuery.getQuery());
}
return Mono.just(0);
}
CosmosAsyncContainer container = getContainer(persistentEntity);
Optional optPartitionKey = preparedQuery.getParameterInRole(Constants.PARTITION_KEY_ROLE, PartitionKey.class);
CosmosPagedFlux items = getCosmosResults(preparedQuery, querySpec, ObjectNode.class);
return executeBulk(container, items, BulkOperationType.UPDATE, persistentEntity, optPartitionKey, item -> updateProperties(item, propertiesToUpdate))
.onErrorMap(e -> handleCosmosOperationException("Failed to update item(s)", e, CosmosDiagnosticsProcessor.EXECUTE_BULK, persistentEntity));
}
@Override
@NonNull
public Mono executeDelete(@NonNull PreparedQuery, Number> pq) {
if (isRawQuery(pq)) {
return Mono.error(new IllegalStateException("Cosmos Db does not support raw delete queries."));
}
SqlPreparedQuery, Number> preparedQuery = getSqlPreparedQuery(pq);
preparedQuery.prepare(null);
RuntimePersistentEntity> persistentEntity = runtimeEntityRegistry.getEntity(preparedQuery.getRootEntity());
CosmosAsyncContainer container = getContainer(persistentEntity);
Optional optPartitionKey = preparedQuery.getParameterInRole(Constants.PARTITION_KEY_ROLE, PartitionKey.class);
SqlQuerySpec querySpec = new SqlQuerySpec(preparedQuery.getQuery(), new ParameterBinder().bindParameters(preparedQuery));
CosmosPagedFlux items = getCosmosResults(preparedQuery, querySpec, ObjectNode.class);
return executeBulk(container, items, BulkOperationType.DELETE, persistentEntity, optPartitionKey, null)
.onErrorMap(e -> handleCosmosOperationException("Failed to delete item(s)", e, CosmosDiagnosticsProcessor.EXECUTE_BULK, persistentEntity));
}
@Override
@NonNull
public Mono delete(@NonNull DeleteOperation operation) {
CosmosReactiveOperationContext ctx = createCosmosReactiveOperationContext(operation);
CosmosReactiveEntityOperation op = createCosmosReactiveDeleteOneOperation(ctx, operation.getEntity());
op.delete();
return op.getRowsUpdated();
}
@Override
@NonNull
public Mono deleteAll(@NonNull DeleteBatchOperation operation) {
CosmosReactiveOperationContext ctx = createCosmosReactiveOperationContext(operation);
CosmosReactiveEntitiesOperation op = createCosmosReactiveBulkOperation(ctx, operation, BulkOperationType.DELETE);
op.update();
return op.getRowsUpdated();
}
@Override
@NonNull
public Mono> findPage(@NonNull PagedQuery pagedQuery) {
throw new UnsupportedOperationException("Not supported");
}
// Query related methods
/**
* Gets cosmos reactive results for given prepared query.
*
* @param preparedQuery the prepared query
* @param querySpec the Cosmos Sql query spec
* @param itemsType the result iterator items type
* @param The query entity type
* @param The query result type
* @param the Cosmos iterator items type
* @return CosmosPagedFlux with values of I type
*/
private CosmosPagedFlux getCosmosResults(PreparedQuery preparedQuery, SqlQuerySpec querySpec, Class itemsType) {
RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(preparedQuery.getRootEntity());
CosmosAsyncContainer container = getContainer(persistentEntity);
CosmosQueryRequestOptions requestOptions = createCosmosQueryRequestOptions();
preparedQuery.getParameterInRole(Constants.PARTITION_KEY_ROLE, PartitionKey.class).ifPresent(requestOptions::setPartitionKey);
return container.queryItems(querySpec, requestOptions, itemsType);
}
/**
* Finds one entity or DTO projection.
*
* @param preparedQuery the prepared query
* @param querySpec the Cosmos SQL query
* @param The entity type
* @param The result type
* @return entity or DTO projection
*/
private Mono findOneEntityOrDto(PreparedQuery preparedQuery, SqlQuerySpec querySpec) {
CosmosPagedFlux result = getCosmosResults(preparedQuery, querySpec, ObjectNode.class);
return result.byPage().flatMap(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS, response.getCosmosDiagnostics(),
response.getActivityId(), response.getRequestCharge());
Iterator iterator = response.getResults().iterator();
if (iterator.hasNext()) {
ObjectNode item = iterator.next();
if (iterator.hasNext()) {
return Flux.error(new NonUniqueResultException());
}
if (preparedQuery.isDtoProjection()) {
@SuppressWarnings("unchecked") Class wrapperType = (Class) ReflectionUtils.getWrapperType(preparedQuery.getResultType());
return Mono.just(cosmosSerde.deserialize(item, Argument.of(wrapperType)));
}
RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(preparedQuery.getRootEntity());
return Mono.just(cosmosSerde.deserialize(persistentEntity, item, Argument.of(preparedQuery.getResultType())));
}
return Flux.empty();
}).onErrorMap(e -> CosmosUtils.cosmosAccessException(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS,
"Failed to query item", e)).next();
}
/**
* Finds query and returns as custom result type.
*
* @param preparedQuery the prepared query
* @param querySpec the Cosmos SQL query
* @param The entity type
* @param The result type
* @return custom result type as a result of prepared query execution
*/
private Mono findOneCustomResult(PreparedQuery preparedQuery, SqlQuerySpec querySpec) {
DataType dataType = preparedQuery.getResultDataType();
Class resultType = preparedQuery.getResultType();
CosmosPagedFlux> result = getCosmosResults(preparedQuery, querySpec, getDataTypeClass(dataType));
return result.byPage().flatMap(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS, response.getCosmosDiagnostics(),
response.getActivityId(), response.getRequestCharge());
if (dataType.isArray()) {
Collection> collection = CollectionUtils.iterableToList(response.getElements());
if (collection.isEmpty()) {
return Mono.just((R) collection.toArray());
}
return Mono.just(conversionService.convertRequired(collection, resultType));
}
Mono singleResult = fetchSingleResult(response, resultType);
if (singleResult != null) {
return singleResult;
}
return Flux.empty();
}).onErrorMap(e -> CosmosUtils.cosmosAccessException(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.QUERY_ITEMS,
"Failed to query item", e)).next();
}
private Mono fetchSingleResult(FeedResponse> response, Class resultType) {
Iterator> iterator = response.getResults().iterator();
if (iterator.hasNext()) {
Object item = iterator.next();
if (iterator.hasNext()) {
return Mono.error(new NonUniqueResultException());
}
if (resultType.isInstance(item)) {
return Mono.just((R) item);
}
if (item != null) {
return Mono.just(conversionService.convertRequired(item, resultType));
}
}
return null;
}
/**
* Logs Cosmos Db SQL query being executed along with parameter values (debug level).
*
* @param querySpec the SQL query spec
*/
private void logQuery(SqlQuerySpec querySpec) {
if (QUERY_LOG.isDebugEnabled()) {
QUERY_LOG.debug("Executing query: {}", querySpec.getQueryText());
if (QUERY_LOG.isTraceEnabled()) {
for (SqlParameter param : querySpec.getParameters()) {
QUERY_LOG.trace("Parameter: name={}, value={}", param.getName(), param.getValue(Object.class));
}
}
}
}
/**
* Gets an indicator telling whether {@link PreparedQuery} is raw query.
*
* @param preparedQuery the prepared query
* @return true if prepared query is created from raw query
*/
private boolean isRawQuery(@NonNull PreparedQuery, ?> preparedQuery) {
return preparedQuery.getAnnotationMetadata().stringValue(Query.class, DataMethod.META_MEMBER_RAW_QUERY).isPresent();
}
private SqlPreparedQuery getSqlPreparedQuery(PreparedQuery preparedQuery) {
if (preparedQuery instanceof SqlPreparedQuery sqlPreparedQuery) {
return sqlPreparedQuery;
}
throw new IllegalStateException("Expected for prepared query to be of type: SqlPreparedQuery got: " + preparedQuery.getClass().getName());
}
private CosmosSqlPreparedQuery getCosmosSqlPreparedQuery(PreparedQuery preparedQuery) {
if (preparedQuery instanceof CosmosSqlPreparedQuery cosmosSqlPreparedQuery) {
return cosmosSqlPreparedQuery;
}
throw new IllegalStateException("Expected for prepared query to be of type: CosmosSqlPreparedQuery got: " + preparedQuery.getClass().getName());
}
/**
* Creates new {@link CosmosQueryRequestOptions} and inits default settings.
* @return the {@link CosmosQueryRequestOptions} instance
*/
private CosmosQueryRequestOptions createCosmosQueryRequestOptions() {
CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setQueryMetricsEnabled(this.queryMetricsEnabled);
return options;
}
// Container util methods
/**
* Gets the async container for given persistent entity. It is expected that at this point container is created.
*
* @param persistentEntity the persistent entity (to be persisted in container)
* @return the Cosmos async container
*/
private CosmosAsyncContainer getContainer(RuntimePersistentEntity> persistentEntity) {
return cosmosAsyncDatabase.getContainer(CosmosEntity.get(persistentEntity).getContainerName());
}
// Partition key logic
/**
* Gets an indicator telling whether persistent entity identity field matches with the container partition key for that entity.
*
* @param runtimePersistentEntity persistent entity
* @return true if persistent entity identity field matches with the container partition key for that entity
*/
private boolean isIdPartitionKey(RuntimePersistentEntity> runtimePersistentEntity) {
PersistentProperty identity = runtimePersistentEntity.getIdentity();
if (identity == null) {
return false;
}
String partitionKey = getPartitionKeyDefinition(runtimePersistentEntity);
return partitionKey.equals(Constants.PARTITION_KEY_SEPARATOR + identity.getName());
}
/**
* Gets partition key definition for given persistent entity.
* It may happen that persistent entity does not have defined partition key and in that case we return empty string (or null).
*
* @param runtimePersistentEntity the persistent entity
* @return partition key definition it exists for persistent entity, otherwise empty/null string
*/
@NonNull
private String getPartitionKeyDefinition(RuntimePersistentEntity> runtimePersistentEntity) {
CosmosEntity cosmosEntity = CosmosEntity.get(runtimePersistentEntity);
return cosmosEntity.getPartitionKey();
}
/**
* Gets partition key for a document. Partition keys can be only string or number values.
*
* @param persistentEntity the persistent entity
* @param item item from the Cosmos Db
* @return partition key, if partition key defined and value set otherwise null
*/
@Nullable
private PartitionKey getPartitionKey(RuntimePersistentEntity> persistentEntity, ObjectNode item) {
String partitionKeyDefinition = getPartitionKeyDefinition(persistentEntity);
if (partitionKeyDefinition.startsWith(Constants.PARTITION_KEY_SEPARATOR)) {
partitionKeyDefinition = partitionKeyDefinition.substring(1);
}
return getPartitionKey(partitionKeyDefinition, item);
}
/**
* Gets partition key for a document. Partition keys can be only string or number values.
* TODO: Later deal with nested paths when we support it.
*
* @param partitionKeyField the partition key field without "/" at the beginning
* @param item item from the Cosmos Db
* @return partition key, if partition key defined and value set otherwise null
*/
@Nullable
private PartitionKey getPartitionKey(String partitionKeyField, ObjectNode item) {
com.fasterxml.jackson.databind.JsonNode jsonNode = item.get(partitionKeyField);
if (jsonNode == null) {
return null;
}
Object value;
if (jsonNode.isNumber()) {
value = jsonNode.numberValue();
} else if (jsonNode.isBoolean()) {
value = jsonNode.booleanValue();
} else {
value = jsonNode.textValue();
}
return new PartitionKey(value);
}
/**
* Gets the id from {@link ObjectNode} document in Cosmos Db. Can return null if document ({@link ObjectNode} not yet persisted.
*
* @param item the item/document in the db
* @return document id
*/
private String getItemId(ObjectNode item) {
com.fasterxml.jackson.databind.JsonNode idNode = item.get(Constants.INTERNAL_ID);
if (idNode == null) {
return null;
}
return idNode.textValue();
}
/**
* Gets underlying java class for the {@link DataType}.
*
* @param dataType the data type
* @return java class for the data type
*/
private Class> getDataTypeClass(DataType dataType) {
return switch (dataType) {
case STRING, JSON -> String.class;
case UUID -> UUID.class;
case LONG -> Long.class;
case INTEGER -> Integer.class;
case BOOLEAN -> Boolean.class;
case BYTE -> Byte.class;
case TIMESTAMP, DATE -> Date.class;
case CHARACTER -> Character.class;
case FLOAT -> Float.class;
case SHORT -> Short.class;
case DOUBLE -> Double.class;
case BIGDECIMAL -> BigDecimal.class;
case TIME -> Time.class;
default -> Object.class;
};
}
// Create, update, delete
/**
* If entity has auto generated id, we can generate only String and UUID.
*
* @param persistentEntity the persistent entity
* @param entity the entity
* @param the entity type
*/
private void generateId(RuntimePersistentEntity persistentEntity, T entity) {
RuntimePersistentProperty identity = persistentEntity.getIdentity();
if (identity.getProperty().get(entity) == null) {
if (identity.getDataType().equals(DataType.STRING)) {
identity.getProperty().convertAndSet(entity, UUID.randomUUID().toString());
}
if (identity.getDataType().equals(DataType.UUID)) {
identity.getProperty().convertAndSet(entity, UUID.randomUUID());
}
}
}
/**
* Sets version value from etag when entity is saved.
*
* @param persistentEntity the persistent entity
* @param entity the entity
* @param versionField the version field
* @param eTagVersion the etag version value
* @param the entity type
*/
private void setETagVersion(RuntimePersistentEntity persistentEntity, T entity, String versionField, String eTagVersion) {
RuntimePersistentProperty versionProperty = persistentEntity.getPropertyByName(versionField);
if (versionProperty == null) {
return;
}
versionProperty.getProperty().convertAndSet(entity, eTagVersion);
}
/**
* Sets version value from etag when entity is saved from the {@link CosmosItemResponse} if persistent entity has version field
* marked with {@link io.micronaut.data.cosmos.annotation.ETag} annotation.
*
* @param cosmosItemResponse the cosmos item response
* @param persistentEntity the persistent entity
* @param entity the entity
* @param the entity type
* @param the cosmos response item type
*/
private void setETagVersionIfApplicable(CosmosItemResponse cosmosItemResponse, RuntimePersistentEntity persistentEntity, T entity) {
CosmosEntity cosmosEntity = CosmosEntity.get(persistentEntity);
String versionField = cosmosEntity.getVersionField();
if (versionField == null) {
return;
}
setETagVersion(persistentEntity, entity, versionField, cosmosItemResponse.getETag());
}
/**
* Updates existing {@link ObjectNode} item with given property values.
*
* @param item the {@link ObjectNode} item to be updated
* @param propertiesToUpdate map with property keys and values to update
* @return updated {@link ObjectNode} with new values
*/
private ObjectNode updateProperties(ObjectNode item, Map propertiesToUpdate) {
// iterate through properties, update and replace item
for (Map.Entry propertyToUpdate : propertiesToUpdate.entrySet()) {
String property = propertyToUpdate.getKey();
Object value = propertyToUpdate.getValue();
com.fasterxml.jackson.databind.JsonNode objectNode;
if (value == null) {
objectNode = NullNode.getInstance();
} else {
objectNode = cosmosSerde.serialize(value, Argument.of(value.getClass()));
}
item.set(property, objectNode);
}
return item;
}
/**
* Gets update statement from the prepared query for {@link #executeUpdate(PreparedQuery)}.
* In this case, it is list of properties to be updated.
*
* @param preparedQuery the prepared query
* @param the entity type
* @param the result type
* @return update statement (list of props to update in Azure Cosmos)
*/
private String getUpdate(PreparedQuery preparedQuery) {
CosmosSqlPreparedQuery cosmosSqlPreparedQuery = getCosmosSqlPreparedQuery(preparedQuery);
return cosmosSqlPreparedQuery.getUpdate();
}
/**
* Creates list of {@link CosmosItemOperation} to be executed in bulk operation.
*
* @param items the items to be updated/deleted in a bulk operation
* @param bulkOperationType the bulk operation type (delete or update)
* @param persistentEntity the persistent entity
* @param optPartitionKey the optional partition key, will be used if not empty
* @param handleItem function that will apply some changes before adding item to the list, if null then ignored
* @return list of {@link CosmosItemOperation}s
*/
private List createBulkOperations(Iterable items, BulkOperationType bulkOperationType, RuntimePersistentEntity> persistentEntity,
Optional optPartitionKey, UnaryOperator handleItem) {
List bulkOperations = new ArrayList<>();
RequestOptions requestOptions = new RequestOptions();
String partitionKeyDefinition = getPartitionKeyDefinition(persistentEntity);
if (partitionKeyDefinition.startsWith(Constants.PARTITION_KEY_SEPARATOR)) {
partitionKeyDefinition = partitionKeyDefinition.substring(1);
}
final String partitionKeyField = partitionKeyDefinition;
for (ObjectNode item : items) {
if (handleItem != null) {
item = handleItem.apply(item);
}
String id = getItemId(item);
ObjectNode finalItem = item;
PartitionKey partitionKey = optPartitionKey.orElseGet(() -> getPartitionKey(partitionKeyField, finalItem));
bulkOperations.add(new ItemBulkOperation<>(bulkOperationType.cosmosItemOperationType, id, partitionKey, requestOptions, item, null));
}
return bulkOperations;
}
/**
* Executes bulk operation (update or delete) for given iterable of {@link ObjectNode}.
*
* @param container the container where documents are being updated or deleted
* @param items the items being updated or deleted
* @param bulkOperationType the bulk operation type (DELETE or UPDATE)
* @param persistentEntity the persistent entity corresponding to the items
* @param optPartitionKey {@link Optional} with {@link PartitionKey} as value, if empty then will obtain partition key from each item
* @param handleItem function that will apply some changes before adding item to the list, if null then ignored
* @return number of affected items
*/
private Mono executeBulk(CosmosAsyncContainer container, CosmosPagedFlux items, BulkOperationType bulkOperationType, RuntimePersistentEntity> persistentEntity, Optional optPartitionKey,
UnaryOperator handleItem) {
// Update/replace using provided partition key or partition key calculated from each item
Flux updateItems = items.byPage().flatMap(response -> {
List bulkOperations = createBulkOperations(response.getResults(), bulkOperationType, persistentEntity, optPartitionKey, handleItem);
return Flux.fromIterable(bulkOperations);
});
return container.executeBulkOperations(updateItems).reduce(-1, (affectedCount, bulkOperationResponse) -> {
CosmosBulkItemResponse response = bulkOperationResponse.getResponse();
if (affectedCount.intValue() == -1) {
// The response diagnostic is the same for each iteration, so we don't want to log it for each item
affectedCount = 0;
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.EXECUTE_BULK, response.getCosmosDiagnostics(), response.getActivityId(),
response.getRequestCharge());
}
if (response.getStatusCode() == bulkOperationType.expectedOperationStatusCode) {
affectedCount = (int) affectedCount + 1;
}
return affectedCount;
});
}
private CosmosReactiveEntityOperation createCosmosInsertOneOperation(CosmosReactiveOperationContext ctx, T entity) {
return new CosmosReactiveEntityOperation<>(entityEventRegistry, conversionService, ctx, ctx.getPersistentEntity(), entity, true) {
@Override
protected void execute() throws RuntimeException {
CosmosAsyncContainer container = ctx.getContainer();
data = data.flatMap(d -> {
if (hasGeneratedId) {
generateId(persistentEntity, d.entity);
}
ObjectNode item = cosmosSerde.serialize(persistentEntity, d.entity, Argument.of(ctx.getRootEntity()));
PartitionKey partitionKey = getPartitionKey(persistentEntity, item);
return Mono.from(container.createItem(item, partitionKey, new CosmosItemRequestOptions())).map(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.CREATE_ITEM, response.getDiagnostics(), response.getActivityId(),
response.getRequestCharge());
setETagVersionIfApplicable(response, persistentEntity, d.entity);
return d;
}).onErrorMap(e -> CosmosUtils.cosmosAccessException(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.CREATE_ITEM, "Failed to insert item", e));
});
}
};
}
private void setIfMatchETag(CosmosItemRequestOptions requestOptions, ObjectNode item) {
final com.fasterxml.jackson.databind.JsonNode versionValue = item.get(Constants.ETAG_FIELD_NAME);
if (versionValue != null) {
requestOptions.setIfMatchETag(versionValue.textValue());
}
}
private Throwable handleCosmosOperationException(String message, Throwable e, String operationName, RuntimePersistentEntity> persistentEntity) {
if (e instanceof CosmosException cosmosException) {
if (cosmosException.getStatusCode() == HttpResponseStatus.PRECONDITION_FAILED.code()) {
CosmosEntity cosmosEntity = CosmosEntity.get(persistentEntity);
if (cosmosEntity.getVersionField() != null) {
return new OptimisticLockException("Operation failed due to optimistic locking conflict.");
}
}
}
return CosmosUtils.cosmosAccessException(cosmosDiagnosticsProcessor, operationName, message, e);
}
/**
* Creates {@link CosmosReactiveOperationContext} for given entity operation.
*
* @param operation the entity operation
* @param the entity type
* @return the context for Cosmos reactive operation
*/
private CosmosReactiveOperationContext createCosmosReactiveOperationContext(EntityOperation operation) {
Class rootEntity = operation.getRootEntity();
RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(rootEntity);
CosmosAsyncContainer container = getContainer(persistentEntity);
return new CosmosReactiveOperationContext<>(operation.getAnnotationMetadata(),
operation.getRepositoryType(), container, rootEntity, persistentEntity);
}
private CosmosReactiveEntityOperation createCosmosReactiveReplaceItemOperation(CosmosReactiveOperationContext ctx, T entity) {
return new CosmosReactiveEntityOperation<>(entityEventRegistry, conversionService, ctx, ctx.getPersistentEntity(), entity, false) {
@Override
protected void execute() throws RuntimeException {
CosmosAsyncContainer container = ctx.getContainer();
data = data.flatMap(d -> {
ObjectNode item = cosmosSerde.serialize(persistentEntity, d.entity, Argument.of(ctx.getRootEntity()));
PartitionKey partitionKey = getPartitionKey(persistentEntity, item);
String id = getItemId(item);
CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions();
setIfMatchETag(requestOptions, item);
Mono> replaceItemResponse = container.replaceItem(item, id, partitionKey, requestOptions);
return Mono.from(replaceItemResponse).map(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.REPLACE_ITEM, response.getDiagnostics(), response.getActivityId(),
response.getRequestCharge());
if (response.getStatusCode() != HttpResponseStatus.OK.code()) {
if (LOG.isWarnEnabled()) {
LOG.warn("Failed to update entity with id {} in container {}", id, container.getId());
}
d.rowsUpdated = 0;
} else {
d.rowsUpdated = 1;
setETagVersionIfApplicable(response, persistentEntity, d.entity);
}
return d;
}).onErrorMap(e -> handleCosmosOperationException("Failed to replace item", e, CosmosDiagnosticsProcessor.REPLACE_ITEM, persistentEntity));
});
}
};
}
private CosmosReactiveEntityOperation createCosmosReactiveDeleteOneOperation(CosmosReactiveOperationContext ctx, T entity) {
return new CosmosReactiveEntityOperation<>(entityEventRegistry, conversionService, ctx, ctx.getPersistentEntity(), entity, false) {
@Override
protected void execute() throws RuntimeException {
CosmosAsyncContainer container = ctx.getContainer();
data = data.flatMap(d -> {
ObjectNode item = cosmosSerde.serialize(persistentEntity, d.entity, Argument.of(ctx.getRootEntity()));
CosmosItemRequestOptions options = new CosmosItemRequestOptions();
setIfMatchETag(options, item);
String id = getItemId(item);
PartitionKey partitionKey = getPartitionKey(persistentEntity, item);
Mono> deleteItemResponse = container.deleteItem(id, partitionKey, options);
return Mono.from(deleteItemResponse).map(response -> {
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.DELETE_ITEM, response.getDiagnostics(), response.getActivityId(),
response.getRequestCharge());
if (response.getStatusCode() == HttpResponseStatus.NO_CONTENT.code()) {
d.rowsUpdated = 1;
} else {
d.rowsUpdated = 0;
}
return d;
}).onErrorMap(e -> handleCosmosOperationException("Failed to delete item", e, CosmosDiagnosticsProcessor.DELETE_ITEM, persistentEntity));
});
}
};
}
private CosmosReactiveEntitiesOperation createCosmosReactiveBulkOperation(CosmosReactiveOperationContext ctx,
BatchOperation operation,
BulkOperationType operationType) {
boolean insert = BulkOperationType.CREATE.equals(operationType);
boolean delete = BulkOperationType.DELETE.equals(operationType);
CosmosEntity cosmosEntity = CosmosEntity.get(ctx.getPersistentEntity());
String versionField = cosmosEntity.getVersionField();
boolean setETagVersion = versionField != null && !delete;
return new CosmosReactiveBulkEntitiesOperation<>(entityEventRegistry, conversionService, ctx, ctx.getPersistentEntity(), operation, insert,
setETagVersion, operationType, versionField);
}
// Helper classes, enums
/**
* Custom class used for binding parameters for Cosmos sql queries.
* Needed to be able to extract update parameters for update actions, so we can call replace API.
*/
private class ParameterBinder {
private final boolean updateQuery;
private final List updatingProperties;
private final Map propertiesToUpdate = new HashMap<>();
ParameterBinder() {
this.updateQuery = false;
this.updatingProperties = Collections.emptyList();
}
ParameterBinder(boolean updateQuery, List updateProperties) {
this.updateQuery = updateQuery;
this.updatingProperties = updateProperties;
}
/**
* Returns list of {@link SqlParameter} after binding parameters for {@link PreparedQuery}.
*
* @param preparedQuery the prepared query
* @param The entity type of prepared query
* @param The result type of prepared query
* @return SqlParameter list to be used in Azure Cosmos SQL query
*/
List bindParameters(PreparedQuery preparedQuery) {
boolean isRawQuery = isRawQuery(preparedQuery);
RuntimePersistentEntity persistentEntity = runtimeEntityRegistry.getEntity(preparedQuery.getRootEntity());
List parameterList = new ArrayList<>();
SqlPreparedQuery sqlPreparedQuery = getSqlPreparedQuery(preparedQuery);
CosmosBinder cosmosBinder = new CosmosBinder(runtimeEntityRegistry, attributeConverterRegistry, parameterList, isRawQuery,
updateQuery, persistentEntity, updatingProperties);
sqlPreparedQuery.bindParameters(cosmosBinder);
propertiesToUpdate.putAll(cosmosBinder.getPropertiesToUpdate());
return parameterList;
}
Map getPropertiesToUpdate() {
return propertiesToUpdate;
}
}
/**
* The bulk operation type used when creating bulk operations against Cosmos Db.
* Need to know what type (supported CREATE, DELETE and REPLACE) and what expected status code
* for each item is to be treated as successful.
*/
private enum BulkOperationType {
CREATE(CosmosItemOperationType.CREATE, HttpResponseStatus.CREATED.code()),
DELETE(CosmosItemOperationType.DELETE, HttpResponseStatus.NO_CONTENT.code()),
UPDATE(CosmosItemOperationType.REPLACE, HttpResponseStatus.OK.code());
final CosmosItemOperationType cosmosItemOperationType;
final int expectedOperationStatusCode;
BulkOperationType(CosmosItemOperationType cosmosItemOperationType, int expectedOperationStatusCode) {
this.cosmosItemOperationType = cosmosItemOperationType;
this.expectedOperationStatusCode = expectedOperationStatusCode;
}
}
/**
* The Cosmos Db reactive operation context.
*
* @param the entity type
*/
private static class CosmosReactiveOperationContext extends OperationContext {
private final CosmosAsyncContainer container;
private final Class rootEntity;
private final RuntimePersistentEntity persistentEntity;
public CosmosReactiveOperationContext(AnnotationMetadata annotationMetadata, Class> repositoryType, CosmosAsyncContainer container, Class rootEntity,
RuntimePersistentEntity persistentEntity) {
super(annotationMetadata, repositoryType);
this.container = container;
this.rootEntity = rootEntity;
this.persistentEntity = persistentEntity;
}
/**
* @return gets the container in which operation is executing
*/
public CosmosAsyncContainer getContainer() {
return container;
}
/**
* @return the root entity class
*/
public Class getRootEntity() {
return rootEntity;
}
/**
* @return the runtime persistent entity
*/
public RuntimePersistentEntity getPersistentEntity() {
return persistentEntity;
}
}
/**
* Base class for Cosmos reactive entity operation (insert, update and delete).
*
* @param the entity type
*/
private abstract static class CosmosReactiveEntityOperation extends AbstractReactiveEntityOperations, T, RuntimeException> {
/**
* Default constructor.
*
* @param entityEventListener The entity event listener
* @param conversionService The conversion service
* @param ctx The context
* @param persistentEntity The persistent entity
* @param entity The entity
* @param insert The insert
*/
protected CosmosReactiveEntityOperation(EntityEventListener entityEventListener,
ConversionService conversionService,
CosmosReactiveOperationContext ctx,
RuntimePersistentEntity persistentEntity,
T entity,
boolean insert) {
super(ctx, null, conversionService, entityEventListener, persistentEntity, entity, insert);
}
@Override
protected void cascadePre(Relation.Cascade cascadeType) {
}
@Override
protected void cascadePost(Relation.Cascade cascadeType) {
}
@Override
protected void collectAutoPopulatedPreviousValues() {
}
}
/**
* Base class for Cosmos reactive multiple entity operation.
*
* @param the entity type
*/
private abstract static class CosmosReactiveEntitiesOperation extends AbstractReactiveEntitiesOperations, T, RuntimeException> {
/**
* Default constructor.
*
* @param entityEventListener The entity event listener
* @param conversionService The conversion service
* @param ctx The context
* @param persistentEntity The persistent entity
* @param entities The entities
* @param insert Whether the operation is inserting
*/
protected CosmosReactiveEntitiesOperation(EntityEventListener entityEventListener,
ConversionService conversionService,
CosmosReactiveOperationContext ctx,
RuntimePersistentEntity persistentEntity,
Iterable entities,
boolean insert) {
super(ctx, null, conversionService, entityEventListener, persistentEntity, entities, insert);
}
@Override
protected void cascadePre(Relation.Cascade cascadeType) {
}
@Override
protected void cascadePost(Relation.Cascade cascadeType) {
}
@Override
protected void collectAutoPopulatedPreviousValues() {
}
}
private class CosmosReactiveBulkEntitiesOperation extends CosmosReactiveEntitiesOperation {
private final boolean setETagVersion;
private final BulkOperationType operationType;
private final String versionField;
protected CosmosReactiveBulkEntitiesOperation(EntityEventListener entityEventListener,
ConversionService conversionService,
CosmosReactiveOperationContext ctx,
RuntimePersistentEntity persistentEntity,
Iterable entities,
boolean insert,
boolean setETagVersion,
BulkOperationType operationType,
String versionField) {
super(entityEventListener, conversionService, ctx, persistentEntity, entities, insert);
this.setETagVersion = setETagVersion;
this.operationType = operationType;
this.versionField = versionField;
}
@Override
protected void execute() throws RuntimeException {
Argument arg = Argument.of(ctx.getRootEntity());
String partitionKeyDefinition = getPartitionKeyDefinition(persistentEntity);
if (partitionKeyDefinition.startsWith(Constants.PARTITION_KEY_SEPARATOR)) {
partitionKeyDefinition = partitionKeyDefinition.substring(1);
}
final String partitionKeyField = partitionKeyDefinition;
boolean generateId = hasGeneratedId && insert;
// Update/replace using partition key calculated from each item
Mono, Long>> entitiesWithRowsUpdated = entities
.flatMap(e -> {
Map entitiesById = new HashMap<>(e.size());
List> notVetoedEntities = e.stream().filter(this::notVetoed).map(x -> {
if (generateId) {
generateId(persistentEntity, x.entity);
}
ObjectNode item = cosmosSerde.serialize(persistentEntity, x.entity, arg);
ItemBulkOperation, ?> itemBulkOperation = createItemBulkOperation(item, partitionKeyField, operationType.cosmosItemOperationType, insert);
entitiesById.put(itemBulkOperation.getId(), x.entity);
return itemBulkOperation;
}).collect(Collectors.toList());
if (notVetoedEntities.isEmpty()) {
return Mono.just(Tuples.of(e, 0L));
}
return executeAndGetRowsUpdated(notVetoedEntities, entitiesById)
.map(Number::longValue)
.map(rowsUpdated -> Tuples.of(e, rowsUpdated));
})
.onErrorMap(e -> handleCosmosOperationException("Failed to execute bulk operation", e, CosmosDiagnosticsProcessor.EXECUTE_BULK, persistentEntity))
.cache();
entities = entitiesWithRowsUpdated.flatMap(t -> Mono.just(t.getT1()));
rowsUpdated = entitiesWithRowsUpdated.map(Tuple2::getT2);
}
private Mono executeAndGetRowsUpdated(List> bulkOperations, Map entitiesById) {
return ctx.getContainer().executeBulkOperations(Flux.fromIterable(bulkOperations)).reduce(-1, (count, bulkOperationResponse) -> {
CosmosBulkItemResponse response = bulkOperationResponse.getResponse();
if (count.intValue() == -1) {
count = 0;
// The response diagnostic is the same for each iteration, so we don't want to log it for each item
CosmosUtils.processDiagnostics(cosmosDiagnosticsProcessor, CosmosDiagnosticsProcessor.EXECUTE_BULK, response.getCosmosDiagnostics(), response.getActivityId(),
response.getRequestCharge());
}
if (response.getStatusCode() == operationType.expectedOperationStatusCode) {
count = (int) count + 1;
if (setETagVersion) {
String id = bulkOperationResponse.getOperation().getId();
T entity = entitiesById.get(id);
if (entity != null) {
setETagVersion(persistentEntity, entity, versionField, response.getETag());
}
}
}
return count;
});
}
private ItemBulkOperation, ?> createItemBulkOperation(ObjectNode item, String partitionKeyField, CosmosItemOperationType cosmosItemOperationType, boolean insert) {
String id = getItemId(item);
PartitionKey partitionKey = getPartitionKey(partitionKeyField, item);
RequestOptions requestOptions = new RequestOptions();
if (!insert) {
final com.fasterxml.jackson.databind.JsonNode versionValue = item.get(Constants.ETAG_FIELD_NAME);
if (versionValue != null) {
requestOptions.setIfMatchETag(versionValue.textValue());
}
}
return new ItemBulkOperation<>(cosmosItemOperationType, id, partitionKey, requestOptions, item, null);
}
}
}