41

Im trying to write a lambda function to add new data to a DynamoDB Table. From reading the docs at:

http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#put-property The PUT method: "Creates a new item, or replaces an old item with a new item by delegating to AWS.DynamoDB.putItem()."

Other than doing a check for an object before 'putting' is there a setting or flag to fail the object exists when the PUT is attempted?

I can see in

params -> Expected -> Exists (Bool)

but can't see any documentation on what this does.

What would be the best architecture (or fasted) to prevent an item overwrite?

Query the table first and if no item exists then add the item

or

Attempt to insert the item and on failure because of duplicate entry report this back? (Is there a way to prevent item overwrite?)
Svend
  • 6,352
  • 1
  • 25
  • 38
Mathew Jenkinson
  • 844
  • 2
  • 11
  • 18

4 Answers4

49

The ConditionExpression can be used to check whether the key attribute values already exists in table and perform the PUT operation only if the key values are not present in the table.

When you run the below code, first time the put operation should be successful. In the second run, the put operation should fail with "Conditional request failed" exception.

My movies table has both partition and sort keys. So, I have used both the attributes in conditional expression.

Sample code with conditional put:-

    var table = "Movies";
    
    var year = 1502;
    var title = "The Big New Movie";
    
    var params = {
        TableName:table,
        Item:{
            "yearkey": year,
            "title": title,
            "info":{
                "plot": "Nothing happens at all.",
                "rating": 0
            }
        },
        ConditionExpression: "yearkey <> :yearKeyVal AND #title <>  :title",
        ExpressionAttributeNames: { 
            "#title" : "title" 
         },
        ExpressionAttributeValues: {
            ":yearKeyVal" : year,
            ":title": {"S": title}
        }
    };
    
    console.log("Adding a new item...");
    docClient.put(params, function(err, data) {
        if (err) {
            console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2));
        } else {        
            console.log("Added item:", JSON.stringify(data, null, 2));
        }
    });

Exception when put operation is performed second time:-

Unable to add item. Error JSON: {
  "message": "The conditional request failed",
  "code": "ConditionalCheckFailedException",
  "time": "2017-10-02T18:26:26.093Z",
  "requestId": "7ae3b0c4-3872-478d-908c-94bc9492a43a",
  "statusCode": 400,
  "retryable": false,
  "retryDelay": 0
}
Pablo Fernandez
  • 279,434
  • 135
  • 377
  • 622
notionquest
  • 37,595
  • 6
  • 111
  • 105
  • 2
    Awesome, thank you very much for your information and help! – Mathew Jenkinson Oct 02 '17 at 22:21
  • 2
    Just a warning (cause this took me hours to find): This approach does not work in bulk requests! It seems like conditionals only work on single PutItem requests. – spaceemotion Nov 19 '19 at 20:45
  • I don't understand why use "AND" in "ConditionExpression" rather than "OR" since two items put separately such as ('A', 'A') and ('A', 'B') will seem as fail since the first field had failed but should be accepted. However, the truth is no matter use "AND" or "OR", the result is same: "Once the two attribute not both match and it will pass". – Chumicat May 08 '20 at 08:51
  • 1
    @Chumicat, actually, just checking `yearkey <> :yearKeyVal` without AND nor OR is enough. This `ConditionExpression` is only going to be checked against an already existing item, as identified by having the same primary key (which would be ok if our intention was to update such item and not adding a new one). This means that if Dynamo finds a existing row to check this condition against, we're already sure we want the condition to fail without even looking at its content. So any predicate that is trivially true on such data would work. I added my own answer below as another such example. – Svend Jan 15 '21 at 05:26
  • 1
    @spaceemotion, `conditionCheck` can indeed not be used on `BatchWriteItem`, but they can with `TransactWriteItems`. In that case the behavior is that if any check fails, the whole transaction is cancelled. If several conditions were expressed in the `TransactWriteItems` and some of them fail, their success/failure status is provided in the response in the same order as in the request, which can be used to interpret what's going on. – Svend Jan 15 '21 at 05:34
  • Thanks for this, I dont know from where you have found out this information. This is nowhere in the docs – KJ Sudarshan Apr 24 '21 at 15:54
12

As an addition to the correct answer of notionquest, the recommended way to express a condition that makes a putItem fail in case the item already exists is to assert that the partition key is not present in any potentially existing item:

"ConditionExpression": "attribute_not_exists(pk)",

This reads "if this item already exists before the putItem, make sure it does not already have a partition key" (pk being the partition key in this example). Since it is impossible for an item to exist without a partition key, this effectively means "make sure this item does not already exist".

See also the "Note" block at the beginning of this page: https://awscli.amazonaws.com/v2/documentation/api/latest/reference/dynamodb/put-item.html

There's an example here when used to guarantee uniqueness of non key columns:

https://aws.amazon.com/blogs/database/simulating-amazon-dynamodb-unique-constraints-using-transactions/

Svend
  • 6,352
  • 1
  • 25
  • 38
10

I see that this question relates to JavaScript language, anyway I will write also for Java (maybe it will be useful for someone):

DynamoDBSaveExpression saveExpression = new DynamoDBSaveExpression();
Map<String, ExpectedAttributeValue> expectedAttributes =
        ImmutableMapParameter.<String, ExpectedAttributeValue>builder()
                .put("hashKeyAttrName", new ExpectedAttributeValue(false))
                .put("rangeKeyAttrName", new ExpectedAttributeValue(false))
                .build();
saveExpression.setExpected(expectedAttributes);
saveExpression.setConditionalOperator(ConditionalOperator.AND);

try {
    dynamoDBMapper.save(item, saveExpression);
} catch (ConditionalCheckFailedException e) {
    e.printStackTrace();
}

ConditionalCheckFailedException will be thrown in case we will try to save item with already existing pair of hashKey and rangeKey in DynamoDB.

Vasyl Sarzhynskyi
  • 3,689
  • 2
  • 22
  • 55
1

As and addition to @Svend solution full Java example (for AWS SDK v2, dynamodb-enchanced)

public <T> void insert(DynamoDbClient client, String tableName, T object, Class<T> clazz) {

  DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder().dynamoDbClient(client).build();

  DynamoDbTable<T> table = enhancedClient.table(tableName, TableSchema.fromBean(clazz));

  table.putItem(PutItemEnhancedRequest.builder(clazz)
      .item(object)
      .conditionExpression(Expression.builder()
          .expression("attribute_not_exists(primaryKey)")
          .build())
      .build());
}

where

  • primaryKey
    • name of column on which uniqueness is checked
  • software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException
    • exception thrown on duplicated put
Łukasz Kotyński
  • 1,007
  • 1
  • 9
  • 10