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;
}