发布于 2026-01-06 1 阅读
0

DynamoDB - 用于插入或编辑项目的动态方法

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!
Enter fullscreen mode Exit fullscreen mode

使用 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
Enter fullscreen mode Exit fullscreen mode

是不是很方便?当然,因为这意味着你可以接收数据并进行验证,然后直接将其传递给负责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))
Enter fullscreen mode Exit fullscreen mode

但我们还是回到这篇文章的主题:如何编辑一个项目。

提交或更新,有什么区别?

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() 
Enter fullscreen mode Exit fullscreen mode

不太清楚,对吧?那个#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);
};

Enter fullscreen mode Exit fullscreen mode

首先,我遇到了一些与键及其值相关的奇怪错误,根据我尝试的不同迭代方式,会出现不同的错误:

验证异常:键“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))
Enter fullscreen mode Exit fullscreen mode

希望对您有所帮助。


照片由Max Langelott拍摄,来自Unsplash

文章来源:https://dev.to/dvddpl/dynamodb-dynamic-method-to-insert-or-edit-an-item-5fnh