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

software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb.internal.operations;

import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.readAndTransformSingleItem;
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.operationExpression;
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.Expression;
import software.amazon.awssdk.enhanced.dynamodb.OperationContext;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionConverter;
import software.amazon.awssdk.enhanced.dynamodb.model.TransactUpdateItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
import software.amazon.awssdk.services.dynamodb.model.Update;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
import software.amazon.awssdk.utils.CollectionUtils;
import software.amazon.awssdk.utils.Either;

@SdkInternalApi
public class UpdateItemOperation
    implements TableOperation>,
               TransactableWriteOperation {

    private final Either, TransactUpdateItemEnhancedRequest> request;

    private UpdateItemOperation(UpdateItemEnhancedRequest request) {
        this.request = Either.left(request);
    }

    private UpdateItemOperation(TransactUpdateItemEnhancedRequest request) {
        this.request = Either.right(request);
    }

    public static  UpdateItemOperation create(UpdateItemEnhancedRequest request) {
        return new UpdateItemOperation<>(request);
    }

    public static  UpdateItemOperation create(TransactUpdateItemEnhancedRequest request) {
        return new UpdateItemOperation<>(request);
    }

    @Override
    public OperationName operationName() {
        return OperationName.UPDATE_ITEM;
    }

    @Override
    public UpdateItemRequest generateRequest(TableSchema tableSchema,
                                             OperationContext operationContext,
                                             DynamoDbEnhancedClientExtension extension) {
        if (!TableMetadata.primaryIndexName().equals(operationContext.indexName())) {
            throw new IllegalArgumentException("UpdateItem cannot be executed against a secondary index.");
        }

        T item = request.map(UpdateItemEnhancedRequest::item, TransactUpdateItemEnhancedRequest::item);
        Boolean ignoreNulls = request.map(r -> Optional.ofNullable(r.ignoreNulls()),
                                          r -> Optional.ofNullable(r.ignoreNulls()))
                                     .orElse(null);

        Map itemMap = tableSchema.itemToMap(item, Boolean.TRUE.equals(ignoreNulls));
        TableMetadata tableMetadata = tableSchema.tableMetadata();

        WriteModification transformation =
            extension != null
            ? extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
                                                                   .items(itemMap)
                                                                   .operationContext(operationContext)
                                                                   .tableMetadata(tableMetadata)
                                                                   .tableSchema(tableSchema)
                                                                   .operationName(operationName())
                                                                   .build())
            : null;

        if (transformation != null && transformation.transformedItem() != null) {
            itemMap = transformation.transformedItem();
        }

        Collection primaryKeys = tableSchema.tableMetadata().primaryKeys();

        Map keyAttributes = filterMap(itemMap, entry -> primaryKeys.contains(entry.getKey()));
        Map nonKeyAttributes = filterMap(itemMap, entry -> !primaryKeys.contains(entry.getKey()));

        Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, nonKeyAttributes);
        Expression conditionExpression = generateConditionExpressionIfExist(transformation, request);

        Map expressionNames = coalesceExpressionNames(updateExpression, conditionExpression);
        Map expressionValues = coalesceExpressionValues(updateExpression, conditionExpression);

        UpdateItemRequest.Builder requestBuilder = UpdateItemRequest.builder()
            .tableName(operationContext.tableName())
            .key(keyAttributes)
            .returnValues(ReturnValue.ALL_NEW);

        if (request.left().isPresent()) {
            addPlainUpdateItemParameters(requestBuilder, request.left().get());
        }
        if (updateExpression != null) {
            requestBuilder.updateExpression(updateExpression.expression());
        }
        if (conditionExpression != null) {
            requestBuilder.conditionExpression(conditionExpression.expression());
        }
        if (CollectionUtils.isNotEmpty(expressionNames)) {
            requestBuilder = requestBuilder.expressionAttributeNames(expressionNames);
        }
        if (CollectionUtils.isNotEmpty(expressionValues)) {
            requestBuilder = requestBuilder.expressionAttributeValues(expressionValues);
        }

        return requestBuilder.build();
    }

    @Override
    public UpdateItemEnhancedResponse transformResponse(UpdateItemResponse response,
                               TableSchema tableSchema,
                               OperationContext operationContext,
                               DynamoDbEnhancedClientExtension extension) {
        try {
            T attributes = readAndTransformSingleItem(response.attributes(), tableSchema, operationContext, extension);

            return UpdateItemEnhancedResponse.builder(null)
                .attributes(attributes)
                .consumedCapacity(response.consumedCapacity())
                .itemCollectionMetrics(response.itemCollectionMetrics())
                .build();
        } catch (RuntimeException e) {
            // With a partial update it's possible to update the record into a state that the mapper can no longer
            // read or validate. This is more likely to happen with signed and encrypted records that undergo partial
            // updates (that practice is discouraged for this reason).
            throw new IllegalStateException("Unable to read the new item returned by UpdateItem after the update "
                                            + "occurred. Rollbacks are not supported by this operation, therefore the "
                                            + "record may no longer be readable using this model.", e);
        }
    }

    @Override
    public Function serviceCall(DynamoDbClient dynamoDbClient) {
        return dynamoDbClient::updateItem;
    }

    @Override
    public Function> asyncServiceCall(
        DynamoDbAsyncClient dynamoDbAsyncClient) {

        return dynamoDbAsyncClient::updateItem;
    }

    @Override
    public TransactWriteItem generateTransactWriteItem(TableSchema tableSchema, OperationContext operationContext,
                                                       DynamoDbEnhancedClientExtension dynamoDbEnhancedClientExtension) {
        UpdateItemRequest updateItemRequest = generateRequest(tableSchema, operationContext, dynamoDbEnhancedClientExtension);

        Update.Builder builder = Update.builder()
                                       .key(updateItemRequest.key())
                                       .tableName(updateItemRequest.tableName())
                                       .updateExpression(updateItemRequest.updateExpression())
                                       .conditionExpression(updateItemRequest.conditionExpression())
                                       .expressionAttributeValues(updateItemRequest.expressionAttributeValues())
                                       .expressionAttributeNames(updateItemRequest.expressionAttributeNames());

        request.right()
               .map(TransactUpdateItemEnhancedRequest::returnValuesOnConditionCheckFailureAsString)
               .ifPresent(builder::returnValuesOnConditionCheckFailure);

        return TransactWriteItem.builder()
                                .update(builder.build())
                                .build();
    }

    /**
     * Retrieves the UpdateExpression from extensions if existing, and then creates an UpdateExpression for the request POJO
     * if there are attributes to be updated (most likely). If both exist, they are merged and the code generates a final
     * Expression that represent the result.
     */
    private Expression generateUpdateExpressionIfExist(TableMetadata tableMetadata,
                                                       WriteModification transformation,
                                                       Map attributes) {
        UpdateExpression updateExpression = null;
        if (transformation != null && transformation.updateExpression() != null) {
            updateExpression = transformation.updateExpression();
        }
        if (!attributes.isEmpty()) {
            List nonRemoveAttributes = UpdateExpressionConverter.findAttributeNames(updateExpression);
            UpdateExpression operationUpdateExpression = operationExpression(attributes, tableMetadata, nonRemoveAttributes);
            if (updateExpression == null) {
                updateExpression = operationUpdateExpression;
            } else {
                updateExpression = UpdateExpression.mergeExpressions(updateExpression, operationUpdateExpression);
            }
        }
        return UpdateExpressionConverter.toExpression(updateExpression);
    }

    /**
     * Retrieves the ConditionExpression from extensions if existing, and retrieves the ConditionExpression from the request
     * if existing. If both exist, they are merged.
     */
    private Expression generateConditionExpressionIfExist(
            WriteModification transformation,
            Either, TransactUpdateItemEnhancedRequest> request) {

        Expression conditionExpression = null;

        if (transformation != null && transformation.additionalConditionalExpression() != null) {
            conditionExpression = transformation.additionalConditionalExpression();
        }

        Expression operationConditionExpression = request.map(r -> Optional.ofNullable(r.conditionExpression()),
                                                              r -> Optional.ofNullable(r.conditionExpression()))
                                                         .orElse(null);
        if (operationConditionExpression != null) {
            conditionExpression = operationConditionExpression.and(conditionExpression);
        }
        return conditionExpression;
    }

    private UpdateItemRequest.Builder addPlainUpdateItemParameters(UpdateItemRequest.Builder requestBuilder,
                                                                   UpdateItemEnhancedRequest enhancedRequest) {
        requestBuilder = requestBuilder.returnConsumedCapacity(enhancedRequest.returnConsumedCapacityAsString());
        requestBuilder = requestBuilder.returnItemCollectionMetrics(enhancedRequest.returnItemCollectionMetricsAsString());
        return requestBuilder;
    }

    private static Map coalesceExpressionNames(Expression firstExpression, Expression secondExpression) {
        Map expressionNames = null;
        if (firstExpression != null && !CollectionUtils.isNullOrEmpty(firstExpression.expressionNames())) {
            expressionNames = firstExpression.expressionNames();
        }
        if (secondExpression != null && !CollectionUtils.isNullOrEmpty(secondExpression.expressionNames())) {
            expressionNames = Expression.joinNames(expressionNames, secondExpression.expressionNames());
        }
        return expressionNames;
    }

    private static Map coalesceExpressionValues(Expression firstExpression, Expression secondExpression) {
        Map expressionValues = null;
        if (firstExpression != null && !CollectionUtils.isNullOrEmpty(firstExpression.expressionValues())) {
            expressionValues = firstExpression.expressionValues();
        }
        if (secondExpression != null && !CollectionUtils.isNullOrEmpty(secondExpression.expressionValues())) {
            expressionValues = Expression.joinValues(expressionValues, secondExpression.expressionValues());
        }
        return expressionValues;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy