DynamoDB - 用于插入或编辑项目的动态方法
鉴于 AWS 和互联网上有大量关于更新 DynamoDB 项目的文档,写一篇关于更新 DynamoDB 项目的文章似乎毫无意义,但我必须说,由于AWS SDK v2 和 v3 的差异、DynamoDbClient 和 DynamoDBDocumentClient以及由于序列化/反序列化和条件表达式引起的各种问题,我费了很大劲才使其正常工作。
因此,我决定分享(并留作将来参考)我奋斗的结果。
编辑现有项目或创建新项目
根据文档, UpdateItem 方法:
编辑现有项目的属性,或者在项目不存在时向表中添加新项目。您可以添加、删除或赋值属性。您还可以对现有项目执行条件更新(如果属性名称-值对不存在,则插入新的属性名称-值对;如果现有名称-值对具有某些预期属性值,则替换现有名称-值对)。
这正是我需要的。我从 API 获取一些数据,并想将其导入 DynamoDB。如果已存在具有相同 ID 的元素,我希望更新所有接收到的属性;否则,我将直接插入一个新行。
幸好有这样的方法,否则我们就得先搜索某个项目,如果没找到就执行上传操作,如果找到了就执行编辑操作。这样就不太方便了,对吧?
客户端还是文档客户端?
自从我开始使用 DynamoDB 以来,我注意到最令人困惑的事情之一是,在 AWS SDK for Javascript 中存在两种实现方式:通过 DynamoDB Client 和DynamoDBDocumentClient——你应该始终使用DynamoDBDocumentClient ,因为它通过使用原生 Javascript 类型抽象化属性的序列化/反序列化,从而简化了所有方法。
比较 DynamoDBClient Put
// you must specify attributes
const dynamodb = new AWS.DynamoDB({apiVersion: '2012-08-10'});
const params = {
Item: {
"Artist": {
S: "No One You Know"
},
"SongTitle": {
S: "Call Me Today"
},
"Year": {
N: 2001
}
},
TableName: "Music"
};
const response = await dynamodb.putItem(params).promise()
// Don't use this method!
使用 DocumentClient:
const documentClient = new AWS.DynamoDB.DocumentClient();
const params = {
Item: {
"Artist": "No One You Know",
"SongTitle": "Call Me Today",
"Year": 2001
}
},
TableName: "Music"
};
const response = await documentClient.put(params).promise()
// pay attention to the method name, it is slightly different
是不是很方便?当然,因为这意味着你可以接收数据并进行验证,然后直接将其传递给负责put操作的通用函数,无需查找 props 和类型,也无需在参数中冗长地指定!
AWS SDK 版本 3
现在让我们添加必要的更改,使其能够与 AWS SDK 版本 3 配合使用(我在这篇文章中介绍了主要区别):
import {DynamoDBClient} from "@aws-sdk/client-dynamodb";
import {DynamoDBDocumentClient, PutCommand} from "@aws-sdk/lib-dynamodb";
const dynamoClient = new DynamoDBClient()
const documentClient = DynamoDBDocumentClient.from(dynamoClient)
const params = {
Item: {
"Artist": "No One You Know",
"SongTitle": "Call Me Today",
"Year": 2001
}
},
TableName: "Music"
};
const response = await documentClient.send(new PutCommand(params))
但我们还是回到这篇文章的主题:如何编辑一个项目。
提交或更新,有什么区别?
Put 操作会将数据插入到行中,update 操作会编辑现有行或添加新行。
因此,千万不要尝试使用 Put 方法仅更新部分属性。如果这样做,DynamoDB 会覆盖当前行,并删除所有未传递给 Put 方法的其他属性(除非您添加了 ConditionExpression 来阻止这种情况)。
另一方面,如果您始终确信拥有完整的对象,包含行中所需的所有属性,并且您不介意数据被完全覆盖(例如,如果您有 inserted_timestamp 或 versionNr 等属性),那么您也可以直接使用 Put 方法。
不过通常情况下,使用 UpdateItem 会更有意义。
发表您的更新
我发现 Update 方法由于 UpdateExpressions 的存在而显得有些复杂。
与 put 方法不同,你不能只传递一个包含几个已更改属性的对象,而是必须(使用一种略显笨拙的语法)指定表达式、值以及已更改的属性名称:
const params = {
TableName: "Music",
Key: {
"Artist": "No One You Know",
},
UpdateExpression:
'set #title = :v_songTitle, #year = :v_year',
ExpressionAttributeNames: {
'#title': 'SongTitle',
'#year': 'Year'
},
ExpressionAttributeValues: {
':v_songTitle': "Call me tomorrow",
':v_year': 1998
},
ReturnValues: "ALL_NEW"
}
const response = await documentClient.update(params).promise()
不太清楚,对吧?那个#title,那个:v_songTitle到底是什么?!
在这个具体的例子中,ExpressionAttributeNames实际上可以省略,而可以使用真正的属性名称,但我想要展示的是,如果属性与某些 Dynamo 保留键冲突,情况会变得多么复杂(完整列表请参见此处)
。保留键的数量远超你的想象:
- 姓名?已预留!
- 柜台?已预订!
- 意见?保留
- 日期?已预订!
- 状态?已预留
- 语言?保留!
如您所见,普通数据库对象中许多属性名称都可能已被保留。因此,如果您不想看到更新函数失败,请习惯使用ExpressionAttributeNames。
这意味着,
- 请列出所有你要编辑的属性名称,并在名称前加上# . (
'#title': 'SongTitle')。 - 列出所有正在改变的值,并给它们赋予一个以冒号(
':v_songTitle': "Call me tomorrow")开头的属性名。 - 指定要在更新表达式中设置哪些值(
'set #title = :v_songTitle')
让它充满活力
如果只是实际更新,只有部分属性发生变化,那当然没问题。但如果对象是新的,我需要列出所有属性呢?如果我希望它是动态的呢?给定一个对象,就给我它所有属性的表达式?
我在 StackOverflow 上快速搜索了一下,找到了一段有趣的代码片段,我立即尝试了一下,但是由于我的表的构建方式、我传递的对象以及属性的序列化/反序列化,我费了好大劲才让它正常工作。
// solution from https://stackoverflow.com/a/66036730
const {
DynamoDBClient, UpdateItemCommand,
} = require('@aws-sdk/client-dynamodb');
const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb');
const client = new DynamoDBClient({});
/**
* Update item in DynamoDB table
* @param {string} tableName // Name of the target table
* @param {object} key // Object containing target item key(s)
* @param {object} item // Object containing updates for target item
*/
const update = async (tableName, key, item) => {
const itemKeys = Object.keys(item);
// When we do updates we need to tell DynamoDB what fields we want updated.
// If that's not annoying enough, we also need to be careful as some field names
// are reserved - so DynamoDB won't like them in the UpdateExpressions list.
// To avoid passing reserved words we prefix each field with "#field" and provide the correct
// field mapping in ExpressionAttributeNames. The same has to be done with the actual
// value as well. They are prefixed with ":value" and mapped in ExpressionAttributeValues
// along witht heir actual value
const { Attributes } = await client.send(new UpdateItemCommand({
TableName: tableName,
Key: marshall(key),
ReturnValues: 'ALL_NEW',
UpdateExpression: `SET ${itemKeys.map((k, index) => `#field${index} = :value${index}`).join(', ')}`,
ExpressionAttributeNames: itemKeys.reduce((accumulator, k, index) => ({ ...accumulator, [`#field${index}`]: k }), {}),
ExpressionAttributeValues: marshall(itemKeys.reduce((accumulator, k, index) => ({ ...accumulator, [`:value${index}`]: item[k] }), {})),
}));
return unmarshall(Attributes);
};
首先,我遇到了一些与键及其值相关的奇怪错误,根据我尝试的不同迭代方式,会出现不同的错误:
验证异常:键“key”处的值为 null,不符合约束条件:成员不能为 null
或者
验证异常:提供的键元素与架构不匹配
然后,当我终于做对了之后,我却卡在了这里:
验证异常:一个或多个参数值无效:无法更新属性“my-key”。此属性是键的一部分。
当然是这样!因为我还没有任何对象,这实际上类似于 PUT 操作(插入而不是编辑!),因此我需要指定分区键要存储哪些数据!但是如果 Update 方法应该做的正是编辑现有项或创建新项,那我到底哪里做错了?
解决方案
原来问题出在(由于使用了动态表达式/属性),我让 DynamoDB 设置了主键的值,这是不允许的。
一旦我从返回所有对象属性名称和值的方法中过滤掉主键属性,一切就都正常了!
最后,答案中建议的序列化和反序列化似乎也并非必要(这不正是 DocumentClient 所负责的吗?——如果您了解更多,请在评论中写下来)。
这是我的最终动态PutOrEdit方法:
/**
* Edit item in DynamoDB table or inserts new if not existing
* @param {string} tableName // Name of the target table
* @param {string} pk // partition key of the item ( necessary for new inserts but not modifiable by the update/edit)
* @param {object} item // Object containing all the props for new item or updates for already existing item
**/
const update = async (tableName, item, pk) => {
const itemKeys = Object.keys(item).filter(k => k !== pk);
const params = {
TableName: tableName,
UpdateExpression: `SET ${itemKeys.map((k, index) => `#field${index} = :value${index}`).join(', ')}`,
ExpressionAttributeNames: itemKeys.reduce((accumulator, k, index) => ({
...accumulator,
[`#field${index}`]: k
}), {}),
ExpressionAttributeValues: itemKeys.reduce((accumulator, k, index) => ({
...accumulator,
[`:value${index}`]: item[k]
}), {}),
Key: {
[pk]: item[pk]
},
ReturnValues: 'ALL_NEW'
};
return await dynamoDocClient.send(new UpdateCommand(params))
希望对您有所帮助。
照片由Max Langelott拍摄,来自Unsplash
文章来源:https://dev.to/dvddpl/dynamodb-dynamic-method-to-insert-or-edit-an-item-5fnh