当 AWS 和互联网上有大量可用文档时,写一篇关于在 DynamoDB 上更新项目的帖子似乎毫无意义,但我必须说,由于 ,我很难让它正常工作AWS SDK v2 和 v3DynamoDbClient 和 DynamoDBDocumentClient 的差异以及由于 marshalling/unmarshallingcondition 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

Logo

云原生社区为您提供最前沿的新闻资讯和知识内容

更多推荐