I have some TypeScript code in a project that does a number of native DynamoDB update operations:
import { nativeDocumentClient, nativeDynamo } from '../utils/aws';
// snipped code
// updatedProducts is of type `{ id: string; siteId: string; state: ProductState }[]`
await Promise.all(
updatedProducts.map(({ id, siteId, state }) =>
nativeDocumentClient
.update({
TableName,
Key: {
id,
siteId,
},
ExpressionAttributeNames: {
'#s': 'state',
},
ExpressionAttributeValues: {
':state': state,
':updatedAt': new Date().toISOString(),
},
UpdateExpression: 'SET #s = :state, updatedAt = :updatedAt',
ReturnValues: 'ALL_NEW',
})
.promise()
)
);
Now we would always expect this record (with its composite Key) to exist, but we have found a rare situation where this is not the case (it's probably just poor data in a nonprod environment rather than a bug specifically). Unfortunately it looks like the underlying UpdateItemCommand
does an upsert and not an update, despite its name. From the official docs:
Edits an existing item's attributes, or adds a new item to the table if it does not already exist
We can do a guard clause that does a get on the record, and skips the upsert if it does not exist. However that feels like an invitation for a race condition† - is there an option on .update()
that would get us what we want without separate conditionals?
† Update - a guard clause might look like this in pseudocode:
object = get(TableName, Key: {
id,
siteId,
})
// Do update only if the document exists
if (object) {
update(
TableName,
Key: {
id,
siteId,
},
...
)
}
I think this could be susceptible to a race condition because the read and write operations are not wrapped in any sort of transaction or lock.
CodePudding user response:
All writes/updates to DynamoDB are strongly serializable.
Using a ConditionExpression
for attribute_exists()
would ensure you only update the item if it exists, if it does not exist the update will fail.
aws dynamodb update-item \
--table-name MyTable \
--key file://config.json \
--update-expression "SET #Y = :y, #AT = :t" \
--expression-attribute-names file://expression-attribute-names.json \
--expression-attribute-values file://expression-attribute-values.json \
--condition-expression "attribute_exists(#PK)"