2

Lets say we have a Dynamodb table with column name lastEventId (String). We want to update a ddb row only if one of the following conditions are true

if ( lastEventId does not exists OR lastEventId == 123(let's say) ).

How can we specify multiple conditions on same column using DynamoDBMapper APIs.

Trying to use dynamoDBMapper.save(ModelClass, DynamoDbSaveExpression)

Sandeep Rao
  • 1,749
  • 6
  • 23
  • 41
Scorpion
  • 633
  • 3
  • 11
  • 24

2 Answers2

3

I realise this is an old question, but in the hope that it helps others, here's what I had to do;

DynamoDBMapper does not support chained conditions.
I.e. It doesn't support condition expressions like update if (a = b AND (c<>d OR e > f)). With Mapper, all conditions are either ANDed or ORed together. To support more complex requirements, you need to use putItem or updateItem from the low level API.

Here's a java example: Let's say we have records where the hash key is the userId and the range key is created timestamp. We want to upsert a record if the hash key does not exist, or if it does exist and has a field THREAD_ID with a certain value.

This was a real situation that I suffered with when doing a data migration from a postgres db to dynamo. The postgres db stored a created timestamp with a 1 second granularity and used the threadId as the primary key. Of course, when we tried to sync these records using the userId/created tuple as the hash/range key, then all the records with the same userId/created key simply overwrote each other - not what we wanted.

At first I tried using updateItem, however that didn't work (for reasons I'm yet to discover). In the end I needed to use putItem with some hacks - if you have ideas how to improve this code - please shout!

    //Capture the threadId of the new record
    Map<String, AttributeValue> eav = new HashMap();
    eav.put(":" + THREAD_ID, new AttributeValue().withS(record.getThreadId()));

    Map<String, AttributeValue> attributeValues = record.getAtributeValues(); //See example below;

    PutItemRequest putItemRequest = new PutItemRequest()
        .withTableName(configuration.getTableName())
        .withItem(attributeValues)
        .withExpressionAttributeValues(eav)
        .withReturnValues(ReturnValue.ALL_OLD) //If nothing is written this will return a result with null attributes
        .withConditionExpression("(attribute_not_exists(" + USER_ID +
            ") AND attribute_not_exists(" + CREATED +
            ")) OR (attribute_exists(" + USER_ID +
            ") AND attribute_exists(" + CREATED +
            ") AND " + THREAD_ID + " = :" + THREAD_ID + ")");

    int count = 0;
    int maxTries = 50; //There really shouldn't be 50 records for the same user within 50 ms of each other. If there is, increase this number
    while(true) {
      try {
        //It seems that due to the condition expression the putItem will only work if there is an item to compare to (uncomfirmed).
        //If there isn't a record, it does nothing and returns a result with null attributes.
        //In that case we must save normally.  I've not found a way to do this in one pass....
        PutItemResult putItemResult = client.putItem(putItemRequest);
        if (putItemResult.getAttributes() == null)
          mapper.save(record);
        break;
      } catch (ConditionalCheckFailedException ce) {
        //In this case a record already exists with this hash/range key combination.
        //Increment the created timestamp and try again.
        record.setCreated(record.getCreated() + 1);
        //We must reset the putItemRequest values to reflect the new created value.
        attributeValues = record.getAtributeValues();
        putItemRequest.withItem(attributeValues);
        if (++count == maxTries)
          throw new InternalServerErrorException(
              new TError("Duplicate userId / created error after " + maxTries + "attempts for userId " + record.userId + " and created " + record.getCreated()));
      }
    }

  //Attributes must not be null
  @DynamoDBIgnore
  public Map<String, AttributeValue> getAtributeValues() {
    Map<String, AttributeValue> attributeValueHashMap = new HashMap<>();
    //The hash, range key
    if (!Strings.isNullOrEmpty(this.userId))
      attributeValueHashMap.put( USER_ID, new AttributeValue().withS(this.userId));
    if (this.created > 0)
      attributeValueHashMap.put( CREATED, new AttributeValue().withN(Long.toString(this.created)));

    //Record values
    if (!Strings.isNullOrEmpty(this.authorId))
      attributeValueHashMap.put( AUTHOR_ID, new AttributeValue().withS(this.authorId));
    if (!Strings.isNullOrEmpty(this.notificationText))
      attributeValueHashMap.put( NOTIFICATION_TEXT, new AttributeValue().withS(this.notificationText));
    if (!Strings.isNullOrEmpty(this.threadId))
      attributeValueHashMap.put( THREAD_ID, new AttributeValue().withS(this.threadId));
    //etc for other params

    return attributeValueHashMap;
  }
mark
  • 1,769
  • 3
  • 19
  • 38
-3

You just use the following save expression.

dynamoDBMapper.save(ModelClass);

There is no need to use the DynamoDbSaveExpression. Because your requirement is to save the object.

save works like hibernates saveorupdate operation.

If lastEventId is not available, then it will save as new row.

If lastEventId is already exists, then it will update with other changes.

N.B:

But you have to know about primary key(hash key and range key).

hash key = primary key

hash key + range key = primary key. It works like composite key.

Resource Link:

  1. Insert DynamoDB Items with DynamoDBMapper
  2. DynamoDBMapper for conditional saves
Community
  • 1
  • 1
SkyWalker
  • 28,384
  • 14
  • 74
  • 132
  • This doesn't really answer the question - and it's a good question. Using save will ALWAYS update, the OP asked how to update only under certain chained conditions. If the conditions are not met, the original record should not be updated. – mark Mar 28 '17 at 10:47