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.isNullAttributeValue;
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.readAndTransformSingleItem;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
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.EnhancedClientUtils;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
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;

@SdkInternalApi
public class UpdateItemOperation
    implements TableOperation,
               TransactableWriteOperation {

    private static final Function EXPRESSION_VALUE_KEY_MAPPER =
        key -> ":AMZN_MAPPED_" + EnhancedClientUtils.cleanAttributeName(key);

    private static final Function EXPRESSION_KEY_MAPPER =
        key -> "#AMZN_MAPPED_" + EnhancedClientUtils.cleanAttributeName(key);

    private final UpdateItemEnhancedRequest request;

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

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

    @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.");
        }

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

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

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

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

        Map keyAttributeValues = itemMap.entrySet().stream()
            .filter(entry -> primaryKeys.contains(entry.getKey()))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

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

        Map filteredAttributeValues = itemMap.entrySet().stream()
            .filter(entry -> !primaryKeys.contains(entry.getKey()))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        requestBuilder = addExpressionsIfExist(transformation, filteredAttributeValues, requestBuilder);

        return requestBuilder.build();
    }

    @Override
    public T transformResponse(UpdateItemResponse response,
                               TableSchema tableSchema,
                               OperationContext operationContext,
                               DynamoDbEnhancedClientExtension extension) {
        try {
            return readAndTransformSingleItem(response.attributes(), tableSchema, operationContext, extension);
        } 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 update = Update.builder()
                              .key(updateItemRequest.key())
                              .tableName(updateItemRequest.tableName())
                              .updateExpression(updateItemRequest.updateExpression())
                              .conditionExpression(updateItemRequest.conditionExpression())
                              .expressionAttributeValues(updateItemRequest.expressionAttributeValues())
                              .expressionAttributeNames(updateItemRequest.expressionAttributeNames())
                              .build();

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

    private static Expression generateUpdateExpression(Map attributeValuesToUpdate) {
        // Sort the updates into 'SET' or 'REMOVE' based on null value
        List updateSetActions = new ArrayList<>();
        List updateRemoveActions = new ArrayList<>();

        attributeValuesToUpdate.forEach((key, value) -> {
            if (!isNullAttributeValue(value)) {
                updateSetActions.add(EXPRESSION_KEY_MAPPER.apply(key) + " = " + EXPRESSION_VALUE_KEY_MAPPER.apply(key));
            } else {
                updateRemoveActions.add(EXPRESSION_KEY_MAPPER.apply(key));
            }
        });

        // Combine the expressions
        List updateActions = new ArrayList<>();

        if (!updateSetActions.isEmpty()) {
            updateActions.add("SET " + String.join(", ", updateSetActions));
        }

        if (!updateRemoveActions.isEmpty()) {
            updateActions.add("REMOVE " + String.join(", ", updateRemoveActions));
        }

        String updateExpression = String.join(" ", updateActions);

        Map expressionAttributeValues =
            attributeValuesToUpdate.entrySet()
                                   .stream()
                                   .filter(entry -> !isNullAttributeValue(entry.getValue()))
                                   .collect(Collectors.toMap(
                                       entry -> EXPRESSION_VALUE_KEY_MAPPER.apply(entry.getKey()),
                                       Map.Entry::getValue));

        Map expressionAttributeNames =
            attributeValuesToUpdate.keySet()
                                   .stream()
                                   .collect(Collectors.toMap(EXPRESSION_KEY_MAPPER, key -> key));

        return Expression.builder()
                         .expression(updateExpression)
                         .expressionValues(Collections.unmodifiableMap(expressionAttributeValues))
                         .expressionNames(expressionAttributeNames)
                         .build();
    }

    private UpdateItemRequest.Builder addExpressionsIfExist(WriteModification transformation,
                                                            Map filteredAttributeValues,
                                                            UpdateItemRequest.Builder requestBuilder) {
        Map expressionNames = null;
        Map expressionValues = null;
        String conditionExpressionString = null;

        /* Add update expression for transformed non-key attributes if applicable */
        if (!filteredAttributeValues.isEmpty()) {
            Expression fullUpdateExpression = generateUpdateExpression(filteredAttributeValues);
            expressionNames = fullUpdateExpression.expressionNames();
            expressionValues = fullUpdateExpression.expressionValues();
            requestBuilder = requestBuilder.updateExpression(fullUpdateExpression.expression());
        }

        /* Merge in conditional expression from extension WriteModification if applicable */
        if (transformation != null && transformation.additionalConditionalExpression() != null) {
            expressionNames =
                Expression.joinNames(expressionNames,
                                     transformation.additionalConditionalExpression().expressionNames());
            expressionValues =
                Expression.joinValues(expressionValues,
                                      transformation.additionalConditionalExpression().expressionValues());
            conditionExpressionString = transformation.additionalConditionalExpression().expression();
        }

        /* Merge in conditional expression from specified 'conditionExpression' if applicable */
        if (this.request.conditionExpression() != null) {
            expressionNames = Expression.joinNames(expressionNames, this.request.conditionExpression().expressionNames());
            expressionValues = Expression.joinValues(expressionValues, this.request.conditionExpression().expressionValues());
            conditionExpressionString = Expression.joinExpressions(conditionExpressionString,
                                                                   this.request.conditionExpression().expression(), " AND ");
        }

        // Avoiding adding empty collections that the low level SDK will propagate to DynamoDb where it causes error.
        if (expressionNames != null && !expressionNames.isEmpty()) {
            requestBuilder = requestBuilder.expressionAttributeNames(expressionNames);
        }

        if (expressionValues != null && !expressionValues.isEmpty()) {
            requestBuilder = requestBuilder.expressionAttributeValues(expressionValues);
        }

        return requestBuilder.conditionExpression(conditionExpressionString);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy