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

software.amazon.awssdk.enhanced.dynamodb.extensions.AtomicCounterExtension 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.extensions;

import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef;
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef;
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.ifNotExists;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAtomicCounter;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AtomicCounterTag;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.AtomicCounter;
import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.update.SetAction;
import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.utils.CollectionUtils;
import software.amazon.awssdk.utils.Logger;

/**
 * This extension enables atomic counter attributes to be changed in DynamoDb by creating instructions for modifying
 * an existing value or setting a start value. The extension is loaded by default when you instantiate a
 * {@link DynamoDbEnhancedClient} and only needs to be added to the client if you are adding custom extensions to the client.
 * 

* To utilize atomic counters, first create a field in your model that will be used to store the counter. * This class field should of type {@link Long} and you need to tag it as an atomic counter: *

    *
  • If you are using the * {@link BeanTableSchema}, you should annotate with * {@link DynamoDbAtomicCounter}
  • *
  • If you are using the {@link StaticTableSchema}, * use the {@link StaticAttributeTags#atomicCounter()} static attribute tag.
  • *
*

* Every time a new update of the record is successfully written to the database, the counter will be updated automatically. * By default, the counter starts at 0 and increments by 1 for each update. The tags provide the capability of adjusting * the counter start and increment/decrement values such as described in {@link DynamoDbAtomicCounter}. *

* Example 1: Using a bean based table schema *

 * {@code
 * @DynamoDbBean
 * public class CounterRecord {
 *     @DynamoDbAtomicCounter(delta = 5, startValue = 10)
 *     public Long getCustomCounter() {
 *         return customCounter;
 *     }
 * }
 * }
 * 
*

* Example 2: Using a static table schema *

 * {@code
 *     private static final StaticTableSchema ITEM_MAPPER =
 *         StaticTableSchema.builder(AtomicCounterItem.class)
 *                          .newItemSupplier(AtomicCounterItem::new)
 *                          .addAttribute(Long.class, a -> a.name("defaultCounter")
 *                                                          .getter(AtomicCounterItem::getDefaultCounter)
 *                                                          .setter(AtomicCounterItem::setDefaultCounter)
 *                                                          .addTag(StaticAttributeTags.atomicCounter()))
 *                          .build();
 * }
 * 
*

* NOTES: *

    *
  • When using putItem, the counter will be reset to its start value.
  • *
  • The extension will remove any existing occurrences of the atomic counter attributes from the record during an * updateItem operation. Manually editing attributes marked as atomic counters will have NO EFFECT.
  • *
*/ @SdkPublicApi public final class AtomicCounterExtension implements DynamoDbEnhancedClientExtension { private static final Logger log = Logger.loggerFor(AtomicCounterExtension.class); private AtomicCounterExtension() { } public static AtomicCounterExtension.Builder builder() { return new AtomicCounterExtension.Builder(); } /** * @param context The {@link DynamoDbExtensionContext.BeforeWrite} context containing the state of the execution. * @return WriteModification contains an update expression representing the counters. */ @Override public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { Map counters = AtomicCounterTag.resolve(context.tableMetadata()); WriteModification.Builder modificationBuilder = WriteModification.builder(); if (CollectionUtils.isNullOrEmpty(counters)) { return modificationBuilder.build(); } switch (context.operationName()) { case PUT_ITEM: modificationBuilder.transformedItem(addToItem(counters, context.items())); break; case UPDATE_ITEM: modificationBuilder.updateExpression(createUpdateExpression(counters)); modificationBuilder.transformedItem(filterFromItem(counters, context.items())); break; default: break; } return modificationBuilder.build(); } private UpdateExpression createUpdateExpression(Map counters) { return UpdateExpression.builder() .actions(counters.entrySet().stream().map(this::counterAction).collect(Collectors.toList())) .build(); } private Map addToItem(Map counters, Map items) { Map itemToTransform = new HashMap<>(items); counters.forEach((attribute, counter) -> itemToTransform.put(attribute, attributeValue(counter.startValue().value()))); return Collections.unmodifiableMap(itemToTransform); } private Map filterFromItem(Map counters, Map items) { Map itemToTransform = new HashMap<>(items); List removedAttributes = new ArrayList<>(); for (String attributeName : counters.keySet()) { if (itemToTransform.containsKey(attributeName)) { itemToTransform.remove(attributeName); removedAttributes.add(attributeName); } } if (!removedAttributes.isEmpty()) { log.debug(() -> String.format("Filtered atomic counter attributes from existing update item to avoid collisions: %s", String.join(",", removedAttributes))); } return Collections.unmodifiableMap(itemToTransform); } private SetAction counterAction(Map.Entry e) { String attributeName = e.getKey(); AtomicCounter counter = e.getValue(); String startValueName = attributeName + counter.startValue().name(); String deltaValueName = attributeName + counter.delta().name(); String valueExpression = ifNotExists(attributeName, startValueName) + " + " + valueRef(deltaValueName); AttributeValue startValue = attributeValue(counter.startValue().value() - counter.delta().value()); AttributeValue deltaValue = attributeValue(counter.delta().value()); return SetAction.builder() .path(keyRef(attributeName)) .value(valueExpression) .putExpressionName(keyRef(attributeName), attributeName) .putExpressionValue(valueRef(startValueName), startValue) .putExpressionValue(valueRef(deltaValueName), deltaValue) .build(); } private AttributeValue attributeValue(long value) { return AtomicCounter.CounterAttribute.resolvedValue(value); } public static final class Builder { private Builder() { } public AtomicCounterExtension build() { return new AtomicCounterExtension(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy