aws-sdk-java-v2 icon indicating copy to clipboard operation
aws-sdk-java-v2 copied to clipboard

AutoGenerateUUIDExtension Overriding Existing Attribute Values

Open Boyapati36 opened this issue 1 year ago • 4 comments

Describe the bug

The AutoGenerateUUIDExtension was incorrectly overriding existing UUID values for items that already contained non-empty values.

This caused some major issues while I working on a project by using DynamoDbTable.updateItem() method. This method didn't update the existing record instead it saves a new record with auto-generated values. When I debuged the code I found the issue is nit with updateItem() method but with the extension. When I tag partition key attribute with tag @DynamoDbAutoGeneratedUuid. I will never be able to update existing record in the Db using updateItem() method.

Regression Issue

  • [ ] Select this option if this issue appears to be a regression.

Expected Behavior

When provided an an object of DynamoDbBean.

Expected: when you try to update an existing record on the DB. it updates the record as per the attributes provided by the user operation.

Current Behavior

Current: Instead of updating the existing record, the operation saves a new record onto the DB

Reproduction Steps

DynamoDB Bug Replication Steps: Auto-Generated UUID Overwriting Issue

Prerequisites

  • Spring Boot project with:
    • Lombok library for model annotations.
    • AWS SDK v2.29.1 for DynamoDB.
  • Local DynamoDB instance set up using NoSQL Workbench.

Steps to Reproduce the Issue

1. Create a DynamoDB Entity (Model)

Create a DynamoDB bean using Lombok annotations:

@DynamoDbBean
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
    private String customerId;
    private String customerName;
    private Instant createdDate;

    @DynamoDbAttribute(value = "CustomerId")
    @DynamoDbAutoGeneratedUuid
    @DynamoDbPartitionKey
    @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
    public String getCustomerId() {
        return customerId;
    }
    public void setCustomerId(String customerId) {
        this.customerId = customerId;
    }

    @DynamoDbAttribute(value = "CustomerName")
    public String getCustomerName() {
        return customerName;
    }
    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    @DynamoDbAttribute(value = "CreatedDate")
    @DynamoDbAutoGeneratedTimestampAttribute
    @DynamoDbConvertedBy(CstTimeFormatConverter.class)
    @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
    public Instant getCreatedDate() {
        return createdDate;
    }
    public void setCreatedDate(Instant createdDate) {
        this.createdDate = createdDate;
    }
}

2. Configure DynamoDB Enhanced Client Bean

Create a DynamoDbEnhancedClient bean in your Spring Boot configuration:

@Configuration
public class DynamoDbConfig {
    @Bean
    public DynamoDbTable<Customer> customerDynamoDbTable(DynamoDbEnhancedClient dynamoDbEnhancedClient){
        return dynamoDbEnhancedClient.table("Customer", TableSchema.fromBean(Customer.class));
    }

    @Bean
    public DynamoDbClient dynamoDbClient() {
        return DynamoDbClient.builder()
                .endpointOverride(java.net.URI.create("http://localhost:8000"))
                .region(Region.US_EAST_1) // Replace with your desired region
                .credentialsProvider(StaticCredentialsProvider.create(
                        AwsBasicCredentials.create("dummy", "vyh65"))) // Use dummy credentials for local
                .build();
    }

    @Bean
    public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) {
        return DynamoDbEnhancedClient.builder()
                .dynamoDbClient(dynamoDbClient)
                .extensions(AutoGeneratedTimestampRecordExtension.create(), AutoGeneratedUuidExtension.create())
                .build();
    }
}

3. Create a DynamoDB Table Bean

Create a DynamoDbTable<MyEntity> using the DynamoDbEnhancedClient:

@Service
public class DynamoDbTableService {
    
    @Autowired
    private DynamoDbEnhancedClient dynamoDbEnhancedClient;

    public DynamoDbTable<MyEntity> getDynamoDbTable() {
        return dynamoDbEnhancedClient.table("MyEntityTable", TableSchema.fromBean(MyEntity.class));
    }
}

4. Create a controller endpoints for table creation and record saving/updating

@RestController
public class ApiController {

    @Autowired
    DynamoDbTable<Customer> customerDynamoDbTable;

    @PostMapping("createRecord")
    public Customer createRecord(@RequestBody Customer customer){
        customer = customerDynamoDbTable.updateItem(customer);

        // Return the updated item
        return customer;
    }

    @PostMapping("createTable")
    public void createTable(){
        customerDynamoDbTable.createTable();
    }

6. Attempt creating table using the above API and then save a record by providing body

{
    "customerName": "XXYYZZ"
}

After record creation you will receive a save records copy as response

{
    "customerId": "623bc3df-579f-4b3d-841e-bed00cfeea0f",
    "customerName": "XXYYZZ", //modify to send as body again
    "createdDate": "2024-11-09T08:09:45.479307200Z"
}

using the modified response as body hit the /createRecord again it should updated the existing record. Instead it creates a new record.

6. Attempt to Update an Existing Record

  1. Create a new MyEntity object, save it using updateItem().
  2. Modify the object's attributes (except for the partition key).
  3. Use updateItem() again to update the record in DynamoDB.

Expected Behavior

  • The updateItem() method should update the existing record with new attribute values.

Actual Behavior

  • Bug: The @DynamoDbAutoGeneratedUuid annotation overrides the existing partition key.
  • The updateItem() method creates a new record instead of updating the existing one.

Summary

The issue is caused by the @DynamoDbAutoGeneratedUuid tag which regenerates the UUID during updateItem() calls, leading to unintended new record creation instead of updating existing entries.

Possible Solution

No response

Additional Information/Context

No response

AWS Java SDK version used

2.29.1

JDK version used

21

Operating System and version

windows 11 23H2

Boyapati36 avatar Nov 09 '24 14:11 Boyapati36

I submitted a PR to fix the issue where @DynamoDbAutoGeneratedUuid was incorrectly overwriting existing UUIDs during updateItem() operations.

Boyapati36 avatar Nov 09 '24 15:11 Boyapati36

Hi @Boyapati36 thank you for reporting this issue, I can reproduce.

Right now, the bug we are considering is the fact that @DynamoDbAutoGeneratedUuid and UpdateBehavior.WRITE_IF_NOT_EXISTS don't work with partition keys. @DynamoDbAutoGeneratedUuid itself is supposed to get a new value with every write, according to the documentation:

Every time a record with this attribute is written to the database it will update the attribute with a UUID.randomUUID() string

In your PR, the change you are proposing is a breaking change, it would impact customers that may be relying on this behavior. Let us know if you'd be interested in making the change more backwards compatible, we can discuss options. We appreciate your PR.

debora-ito avatar Jan 13 '25 23:01 debora-ito

This is a really nasty behaviour, and the fact that it doesn't work even with WRITE_IF_NOT_EXISTS is even worse, same with AutoGeneratedTimestamp which can only be used for fields that write every time but not for a createdAt field

eciuca avatar Sep 06 '25 04:09 eciuca

Hello @debora-ito,

It is important to note that the @DynamoDbAutoGeneratedUuid annotation is not in scope to be used with partition keys. According to the documentation, this annotation is intended only for non-key attributes.

For key use cases, we already have a separate ticket to introduce the @DynamoDbAutoGeneratedKey annotation that can be applied on keys (originating from v1): Ticket #5497 with the corresponding PR.

Additionally, as specified in the documentation, UpdateBehavior.WRITE_IF_NOT_EXISTS is supported only for update operations and not for put. In our testing, @DynamoDbAutoGeneratedUuid works successfully with UpdateBehavior.WRITE_IF_NOT_EXISTS, but only for non-key attributes.

How would you like to proceed with this ticket?

Thank you!

anasatirbasa avatar Sep 10 '25 16:09 anasatirbasa