DynamoDB - 插入或编辑项目的动态方法
当 AWS 和互联网上有大量可用文档时,写一篇关于在 DynamoDB 上更新项目的帖子似乎毫无意义,但我必须说,由于 ,我很难让它正常工作AWS SDK v2 和 v3、DynamoDbClient 和 DynamoDBDocumentClient 的差异以及由于 marshalling/unmarshalling 和 condition expressions 而导致的各种问题。
因此,我决定分享(并保留它作为将来自己的参考)我的斗争结果。
编辑项目或创建新项目
根据文档UpdateItem 方法:
编辑现有项目的属性,如果新项目不存在,则将其添加到表中。 您可以放置、删除或添加属性值。您还可以对现有项目执行条件更新(如果不存在则插入新的属性名称-值对,如果现有的名称-值对具有某些预期的属性值,则替换它)。
这正是我所需要的。我从 API 接收到一些数据,并希望将其填充到 DynamoDB 中。如果已经有一个具有相同 ID 的元素,我想更新我收到的所有属性,否则我将简单地插入一个新行。
存在这样的方法很好,否则我们将不得不 Search for an Item,并且 如果找不到则执行 Put,如果找到则执行 Edit。 不太方便,对吧?
客户端还是文档客户端?
自从我开始使用 DynamoDB 以来,我注意到的最令人困惑的一件事是,在 AWS SDK for Javascript 中存在两种做事方式:通过 DynamoDB 客户端和 DynamoDBDocumentClient - 这是你应该做的一直在使用**,因为它通过抽象出属性的编组/解组来简化任何方法通过使用本机 Javascript 类型):
比较 DynamoDBClient 放置
// 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 的通用函数,无需找出道具和类型,然后在参数中详细指定!
AWS 开发工具包版本 3
现在让我们添加要求更改以使其与 AWS SDK 版本 3 一起使用(我在这篇文章](https://dev.to/dvddpl/aws-sdk-v2-or-v3-which-one-should-you-use-3kaj)中写了关于[的主要区别):
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 or Update,有什么区别?
将插入一行数据,更新编辑现有行或添加新行。
因此,甚至不要考虑使用 Put 仅更新某些属性。如果您这样做,DynamoDB 将覆盖您的当前行并删除您未传递给 put 方法的所有其他属性(除非您添加了 ConditionExpression 来阻止它)。
另一方面,如果您始终确定您拥有整个对象,以及您需要的所有属性以及行中的所有属性,并且您不在乎数据被完全覆盖(想象一下,如果您有一些插入的_timestamp , 或 versionNr ) 那么你也可以使用 Put.
但通常情况下,使用 UpdateItem 更有意义。
表达您的更新
由于 UpdateExpressions,我发现 Update 方法有点复杂。
与 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') -
列出所有正在更改的值,给它们一个以 : 开头的 propname (
':v_songTitle': "Call me tomorrow") -
指定在更新表达式中设置的值 (
'set #title = :v_songTitle')
使其动态化
当它带有实际更新时一切都很好,其中只有一些属性正在更改,但是如果对象是新的并且我必须将它们全部列出,如果我要动态的怎么办:给定一个对象,只需给我所有这些它拥有的所有道具的表达方式?
在 StackOverflow](https://stackoverflow.com/a/66036730)上的[快速搜索给了我一个有趣的代码片段,我立即尝试了,但由于我的表的构建方式,考虑到我正在传递的对象,并且考虑到属性的编组/解组,我很难同时让它正常工作。
// 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 及其值相关的奇怪错误:
ValidationException:“键”处的值为 null 未能满足约束:成员不得为 null
或者
ValidationException:提供的关键元素与架构不匹配
然后,当我终于得到它_right_时,我被困在:
ValidationException:一个或多个参数值无效:无法更新属性“my-key”。此属性是键的一部分
当然是!由于我还没有任何对象,这实际上类似于 PUT(插入而不是编辑!),因此我需要指定哪些数据进入分区键!但是,如果 Update 方法应该做到这一点(编辑一个项目或创建一个新项目)我做错了什么?
解决方案
事实证明,问题在于(由于动态表达式/属性)我告诉 dynamo 为我的主键设置值,这是不允许的。
一旦我从该方法中过滤出主键属性,返回每个对象属性的所有属性名称和值,一切都按预期工作!
最后,答案中建议的 Marshalling 和 Unmarshalling 似乎也没有必要(这不正是 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 拍摄onUnsplash
更多推荐
所有评论(0)