使用 JavaScript 对 Amazon DynamoDB 进行编程 - Amazon DynamoDB
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

使用 JavaScript 对 Amazon DynamoDB 进行编程

本指南为想要结合 JavaScript 使用 Amazon DynamoDB 的程序员提供了指导。了解Amazon SDK for JavaScript、可用的抽象层、配置连接、处理错误、定义重试策略、管理 keep-alive 等。

关于 Amazon SDK for JavaScript

Amazon SDK for JavaScript 支持使用浏览器脚本或 Node.js 访问 Amazon Web Services。本文档重点介绍最新版本的 SDK(V3)。Amazon SDK for JavaScript V3 由 Amazon 作为 GitHub 上托管的开源项目进行维护。问题和特征请求是公开的,您可以在 GitHub 存储库的问题页面上进行访问。

JavaScript V2 与 V3 类似,但存在语法差异。V3 更加模块化,可以更轻松地发布较小的依赖项,并且具有一流的 TypeScript 支持。建议使用最新版本的 SDK。

使用Amazon SDK for JavaScript V3

您可以使用节点程序包管理器将 SDK 添加到 Node.js 应用程序中。以下示例展示了如何添加最常用的 SDK 包来使用 DynamoDB。

  • npm install @aws-sdk/client-dynamodb

  • npm install @aws-sdk/lib-dynamodb

  • npm install @aws-sdk/util-dynamodb

安装程序包会添加对 package.json 项目文件依赖项部分的引用。您可以选择使用更新的 ECMAScript 模块语法。有关这两种方法的更多详细信息,请参阅“注意事项”部分。

访问 JavaScript 文档

通过以下资源开始学习 JavaScript 文档:

  • 访问开发人员指南,获取核心 JavaScript 文档。安装说明位于设置部分。

  • 访问 API 参考文档,浏览所有可用的类和方法。

  • 适用于 JavaScript 的 SDK 除了 DynamoDB 之外还支持许多 Amazon Web Services。使用以下步骤查找 DynamoDB 的特定 API 覆盖范围:

    1. 服务中选择 DynamoDB 和库。这记录了低级别客户端。

    2. 选择 lib-dynamodb。这记录了高级别客户端。这两个客户端代表两个不同的抽象层,您可以选择使用。有关抽象层的更多信息,请参阅以下部分。

抽象层

适用于 JavaScript 的 SDK V3 有一个低级别客户端(DynamoDBClient)和一个高级别客户端(DynamoDBDocumentClient)。

低级别客户端(DynamoDBClient

低级别客户端不对底层线路议提供额外抽象。它使您可以完全控制通信的各个方面,但是由于没有抽象,您必须做一些使用 DynamoDB JSON 格式提供项目定义之类的事情。

如以下示例所示,使用这种格式,必须明确说明数据类型。S 表示字符串值,N 表示数字值。线路上的数字始终以标记为数字类型的字符串发送,以确保精度没有损失。低级别 API 调用有命名模式,如 PutItemCommandGetItemCommand

以下示例使用的是 Item 使用 DynamoDB JSON 定义的低级别客户端:

const { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb"); const client = new DynamoDBClient({}); async function addProduct() { const params = { TableName: "products", Item: { "id": { S: "Product01" }, "description": { S: "Hiking Boots" }, "category": { S: "footwear" }, "sku": { S: "hiking-sku-01" }, "size": { N: "9" } } }; try { const data = await client.send(new PutItemCommand(params)); console.log('result : ' + JSON.stringify(data)); } catch (error) { console.error("Error:", error); } } addProduct();

高级别客户端(DynamoDBDocumentClient

高级别 DynamoDB 文档客户端提供了内置的便利特征,例如无需手动编组数据,并允许使用标准 JavaScript 对象直接读取和写入数据。lib-dynamodb 文档列出了相关优点。

要实例化 DynamoDBDocumentClient,请构造一个低级别 DynamoDBClient,然后使用 DynamoDBDocumentClient 对其进行封装。这两个程序包的函数命名约定略有不同。例如,低级别使用 PutItemCommand,而高级别使用 PutCommand。不同的名称允许两组函数共存于同一个上下文中,从而允许您在同一个脚本中混合使用这两组函数。

const { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); const { DynamoDBDocumentClient, PutCommand } = require("@aws-sdk/lib-dynamodb"); const client = new DynamoDBClient({}); const docClient = DynamoDBDocumentClient.from(client); async function addProduct() { const params = { TableName: "products", Item: { id: "Product01", description: "Hiking Boots", category: "footwear", sku: "hiking-sku-01", size: 9, }, }; try { const data = await docClient.send(new PutCommand(params)); console.log('result : ' + JSON.stringify(data)); } catch (error) { console.error("Error:", error); } } addProduct();

当您使用 GetItemQueryScan 等 API 操作读取项目时,使用模式是一致的。

使用编组实用程序函数

您可以使用低级别客户端,并自行编组或解组数据类型。实用程序包 util-dynamodb 有一个接受 JSON 并生成 DynamoDB JSON 的 marshall() 实用程序函数,还有一个执行反向操作的 unmarshall() 函数。以下示例使用低级别客户端,数据编组由 marshall() 调用处理。

const { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb"); const { marshall } = require("@aws-sdk/util-dynamodb"); const client = new DynamoDBClient({}); async function addProduct() { const params = { TableName: "products", Item: marshall({ id: "Product01", description: "Hiking Boots", category: "footwear", sku: "hiking-sku-01", size: 9, }), }; try { const data = await client.send(new PutItemCommand(params)); } catch (error) { console.error("Error:", error); } } addProduct();

读取项目

要从 DynamoDB 中读取单个项目,请使用 GetItem API 操作。与 PutItem 命令类似,您可以选择使用低级别客户端,也可以选择使用高级别 Document 客户端。以下示例演示了如何使用高级别 Document 客户端检索项目。

const { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); const { DynamoDBDocumentClient, GetCommand } = require("@aws-sdk/lib-dynamodb"); const client = new DynamoDBClient({}); const docClient = DynamoDBDocumentClient.from(client); async function getProduct() { const params = { TableName: "products", Key: { id: "Product01", }, }; try { const data = await docClient.send(new GetCommand(params)); console.log('result : ' + JSON.stringify(data)); } catch (error) { console.error("Error:", error); } } getProduct();

使用 Query API 操作读取多个项目。您可以使用低级别客户端或 Document 客户端。以下示例使用高级别 Document 客户端。

const { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); const { DynamoDBDocumentClient, QueryCommand, } = require("@aws-sdk/lib-dynamodb"); const client = new DynamoDBClient({}); const docClient = DynamoDBDocumentClient.from(client); async function productSearch() { const params = { TableName: "products", IndexName: "GSI1", KeyConditionExpression: "#category = :category and begins_with(#sku, :sku)", ExpressionAttributeNames: { "#category": "category", "#sku": "sku", }, ExpressionAttributeValues: { ":category": "footwear", ":sku": "hiking", }, }; try { const data = await docClient.send(new QueryCommand(params)); console.log('result : ' + JSON.stringify(data)); } catch (error) { console.error("Error:", error); } } productSearch();

带条件写入

DynamoDB 写入操作可以指定逻辑条件表达式,该表达式的计算结果必须为 true 才能继续写入。如果条件的计算结果不是 true,则写入操作会引发异常。条件表达式可以检查项目是否已经存在,或者其属性是否符合某些约束。

ConditionExpression = "version = :ver AND size(VideoClip) < :maxsize"

当条件表达式失败时,您可以使用 ReturnValuesOnConditionCheckFailure 请求错误响应中包含不满足条件的项目,以推断问题出在哪里。有关更多详细信息,请参阅使用 Amazon DynamoDB 处理高并发场景中的条件写入错误

try { const response = await client.send(new PutCommand({ TableName: "YourTableName", Item: item, ConditionExpression: "attribute_not_exists(pk)", ReturnValuesOnConditionCheckFailure: "ALL_OLD" })); } catch (e) { if (e.name === 'ConditionalCheckFailedException') { console.log('Item already exists:', e.Item); } else { throw e; } }

JavaScript SDK V3 文档DynamoDB-SDK-Examples GitHub 存储库中提供了更多显示 JavsScript SDK V3 使用情况其它方面的代码示例。

分页

诸如 ScanQuery 之类的读取请求可能会返回数据集中的多个项目。如果您使用 Limit 参数执行 ScanQuery,那么一旦系统读取许多项目,就会发送部分响应,您需要分页才能检索其它项目。

系统每次请求最多只能读取 1 MB 的数据。如果包含 Filter 表达式,系统仍将从磁盘读取最多 1 MB 的数据,但会返回与筛选条件匹配的相应 MB 的项目。筛选操作可能会针对一个页面返回 0 个项目,但在搜索用尽之前仍需要进一步分页。

您应该在响应中查找 LastEvaluatedKey,并在后续请求中将其用作 ExclusiveStartKey 参数,才能继续检索数据。如以下示例所示,这用作书签。

注意

样本在首次迭代时传递一个空 lastEvaluatedKey 作为 ExclusiveStartKey,这是允许的。

使用 LastEvaluatedKey 的示例:

const { DynamoDBClient, ScanCommand } = require("@aws-sdk/client-dynamodb"); const client = new DynamoDBClient({}); async function paginatedScan() { let lastEvaluatedKey; let pageCount = 0; do { const params = { TableName: "products", ExclusiveStartKey: lastEvaluatedKey, }; const response = await client.send(new ScanCommand(params)); pageCount++; console.log(`Page ${pageCount}, Items:`, response.Items); lastEvaluatedKey = response.LastEvaluatedKey; } while (lastEvaluatedKey); } paginatedScan().catch((err) => { console.error(err); });

使用 paginateScan 便捷方法

SDK 提供了名为 paginateScanpaginateQuery 的便捷方法,这些方法可以为您完成这项工作,并在后台重复请求。使用标准 Limit 参数指定每次请求可读取的最大项目数。

const { DynamoDBClient, paginateScan } = require("@aws-sdk/client-dynamodb"); const client = new DynamoDBClient({}); async function paginatedScanUsingPaginator() { const params = { TableName: "products", Limit: 100 }; const paginator = paginateScan({client}, params); let pageCount = 0; for await (const page of paginator) { pageCount++; console.log(`Page ${pageCount}, Items:`, page.Items); } } paginatedScanUsingPaginator().catch((err) => { console.error(err); });
注意

除非表很小,否则不建议定期执行全表扫描。

指定配置

设置 DynamoDBClient 时,您可以通过将配置对象传递给构造函数来指定各种配置覆盖。例如,如果调用上下文或要使用的端点 URL 尚不知道要连接的区域,则可以指定要连接的区域。如果您希望出于开发目的选择 DynamoDB Local 实例,这会很有用。

const client = new DynamoDBClient({ region: "eu-west-1", endpoint: "http://localhost:8000", });

超时配置

DynamoDB 使用 HTTPS 进行客户端-服务器通信。您可以通过提供 NodeHttpHandler 对象来控制 HTTP 层的某些方面。例如,您可以调整密钥超时值 connectionTimeoutrequestTimeoutconnectionTimeout 是客户端在尝试建立连接时,在放弃之前等待的最大持续时间(以毫秒为单位)。

requestTimeout 定义了发送请求后客户端将等待响应的时间,也以毫秒为单位。两者的默认值均为零,这意味着超时已禁用,如果响应未到达,客户端的等待时间将没有限制。您应该将超时设置为合理的值,以便在出现网络问题时,请求将出错并可以启动新的请求。例如:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { NodeHttpHandler } from "@smithy/node-http-handler"; const requestHandler = new NodeHttpHandler({ connectionTimeout: 2000, requestTimeout: 2000, }); const client = new DynamoDBClient({ requestHandler });
注意

提供的示例使用 Smithy 导入。Smithy 是一种用于定义服务和 SDK 的语言,是开源的,由 Amazon 维护。

除了配置超时值外,您还可以设置最大套接字数,这样可以增加每个源的并发连接数。开发人员指南包含有关配置 maxSockets 参数的详细信息

keep-alive 配置

使用 HTTPS 时,第一个请求总是需要一些往来通信才能建立安全连接。HTTP Keep-Alive 允许后续请求重用已经建立的连接,从而提高请求的效率并降低延迟。JavaScript V3 默认启用 HTTP Keep-Alive。

空闲连接可以保持活动状态的时间是有限制的。如果您有一个空闲的连接,但希望下次请求使用已经建立的连接,可以考虑定期发送请求,比如每分钟发送一次。

注意

请注意,在较旧的 SDK V2 中,keep-alive 默认处于关闭状态,这意味着每个连接在使用后都会立即关闭。如果使用 V2,则您可以覆盖此设置。

重试配置

当 SDK 收到错误响应且 SDK 确定错误可以恢复时,例如节流异常或临时服务异常,它将会重试。您作为调用方,看不到这种情况的发生,只是您可能会注意到,请求花了更长时间才成功完成。

默认情况下,适用于 JavaScript 的 SDK V3 总共会发出 3 个请求,然后才放弃并将错误传递到调用上下文。您可以调整这些重试的次数和频率。

DynamoDBClient 构造函数接受一个 maxAttempts 设置,该设置限制了将要发生的尝试次数。以下示例将值从默认的 3 提高到总计 5。如果您将其设置为 0 或 1,则表示您不想进行任何自动重试,而是想在 catch 块中自己处理任何可恢复的错误。

const client = new DynamoDBClient({ maxAttempts: 5, });

您还可以使用自定义重试策略来控制重试的时间。为此,请导入 util-retry 实用程序包并创建一个自定义回退函数,该函数根据当前的重试计数计算重试之间的等待时间。

下面的示例表明,如果第一次尝试失败,最多可以尝试 5 次,延迟时间为 15、30、90 和 360 毫秒。自定义回退函数 calculateRetryBackoff 通过接受重试尝试次数(首次重试从 1 开始)来计算延迟,并返回等待该请求的毫秒数。

const { ConfiguredRetryStrategy } = require("@aws-sdk/util-retry"); const calculateRetryBackoff = (attempt) => { const backoffTimes = [15, 30, 90, 360]; return backoffTimes[attempt - 1] || 0; }; const client = new DynamoDBClient({ retryStrategy: new ConfiguredRetryStrategy( 5, // max attempts. calculateRetryBackoff // backoff function. ), });

Waiter

DynamoDB 客户端包含两个有用的 Waiter 函数,当您希望代码等待表修改完成后再继续执行时,可以在创建、修改或删除表时使用这些函数。例如,您可以部署表,调用 waitUntilTableExists 函数,代码将会阻塞,直到表变为 ACTIVE 状态。Waiter 函数每 20 秒在内部使用 describe-table 轮询一次 DynamoDB 服务。

import {waitUntilTableExists, waitUntilTableNotExists} from "@aws-sdk/client-dynamodb"; … <create table details> const results = await waitUntilTableExists({client: client, maxWaitTime: 180}, {TableName: "products"}); if (results.state == 'SUCCESS') { return results.reason.Table } console.error(`${results.state} ${results.reason}`);

仅当 waitUntilTableExists 特征可以执行显示表状态为 ACTIVEdescribe-table 命令时,该特征才会返回控制权。这样可以确保您能够使用 waitUntilTableExists 等待创建完成以及诸如添加 GSI 索引之类的修改完成,这些修改可能需要一些时间才能应用,然后表才会恢复为 ACTIVE 状态。

错误处理

在此处的早期示例中,我们已经广泛地发现了所有错误。但是,在实际应用中,区分各种错误类型并实现更精确的错误处理非常重要。

DynamoDB 错误响应包含元数据,其中包括错误名称。您可以捕获错误,然后与错误条件中可能的字符串名称进行匹配,来确定如何继续。对于服务器端错误,您可以利用错误类型由 @aws-sdk/client-dynamodb 程序包导出的 instanceof 运算符,来高效地管理错误处理。

需要注意的是,这些错误只有在所有重试都用尽后才会显示。如果重试错误并最终成功调用,则从代码的角度来看,没有错误,只是延迟略有增加。重试将在 Amazon CloudWatch 图表中显示为失败的请求,例如节流请求或错误请求。如果客户端达到最大重试次数,它将放弃并引发异常。客户端以此表明它不会重试。

下面是一个代码段,用于捕获错误并根据返回的错误类型采取行动。

import { ResourceNotFoundException ProvisionedThroughputExceededException, DynamoDBServiceException, } from "@aws-sdk/client-dynamodb"; try { await client.send(someCommand); } catch (e) { if (e instanceof ResourceNotFoundException) { // Handle ResourceNotFoundException } else if (e instanceof ProvisionedThroughputExceededException) { // Handle ProvisionedThroughputExceededException } else if (e instanceof DynamoDBServiceException) { // Handle DynamoDBServiceException } else { // Other errors such as those from the SDK if (e.name === "TimeoutError") { // Handle SDK TimeoutError. } else { // Handle other errors. } } }

有关常见错误字符串,请参阅《DynamoDB 开发人员指南》中的DynamoDB 错误处理。任何特定 API 调用可能出现的确切错误都可以在该 API 调用的文档中找到,例如查询 API 文档

错误的元数据包括其它属性,具体视错误而定。对于 TimeoutError,元数据包括尝试次数和 totalRetryDelay,如下所示。

{ "name": "TimeoutError", "$metadata": { "attempts": 3, "totalRetryDelay": 199 } }

如果您管理自己的重试策略,则需要区分节流和错误:

  • 节流(由 ProvisionedThroughputExceededExceptionThrottlingException 表示)表示服务运行正常,它会通知您已超出 DynamoDB 表或分区的读取或写入容量。每过一毫秒,就会有多一点的读取或写入容量可用,因此您可以快速重试,例如每 50 毫秒重试一次,尝试访问新释放的容量。

    使用节流,您并不特别需要指数回退,因为节流属于轻量级,DynamoDB 可以返回,而且不会向您收取每次请求的费用。指数回退会将更长的延迟分配给已经等待最长时间的客户端线程,从统计学上讲,将超越 p50 和 p99。

  • 错误(由 InternalServerErrorServiceUnavailable 等表示)表示服务存在暂时性问题,可能是整个表,也可能只是您正在读取或写入的分区。使用错误,您可以在重试前暂停更长时间,例如 250 毫秒或 500 毫秒,并使用抖动来错开重试。

日志记录

开启日志记录功能以获取有关 SDK 正在执行的操作的更多详细信息。您可以在 DynamoDBClient 上设置参数,如以下示例所示。更多日志信息将显示在控制台中,包括状态码和已用容量等元数据。如果您在终端窗口中本地运行代码,则日志会显示于此。如果您在 Amazon Lambda 中运行代码,并且设置了 Amazon CloudWatch Logs,则控制台输出将写入于此。

const client = new DynamoDBClient({ logger: console });

您还可以挂钩到内部 SDK 活动,并在某些事件发生时执行自定义日志记录。以下示例使用客户端的 middlewareStack 拦截从 SDK 发送的每个请求,并在请求发生时将其记录下来。

const client = new DynamoDBClient({}); client.middlewareStack.add( (next) => async (args) => { console.log("Sending request from AWS SDK", { request: args.request }); return next(args); }, { step: "build", name: "log-ddb-calls", } );

MiddlewareStack 提供了用于观察和控制 SDK 行为的强大钩子。有关更多信息,请参阅博客 Introducing Middleware Stack in Modular Amazon SDK for JavaScript

注意事项

在您的项目中实施Amazon SDK for JavaScript 时,需要考虑以下其它因素。

模块系统

该 SDK 支持两个模块系统,CommonJS 和 ES (ECMAScript)。CommonJS 使用 require 函数,而 ES 使用 import 关键字。

  1. Common JSconst { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb");

  2. ES (ECMAScriptimport { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";

项目类型指示了要使用的模块系统,并在 package.json 文件的类型部分中指定。默认为 CommonJS。使用 "type": "module" 指示 ES 项目。如果您有一个使用 CommonJS 程序包格式的现有 Node.JS 项目,您仍然可以通过使用 .mjs 扩展名命名函数文件,来使用更现代的 SDK V3 Import 语法添加函数。这将允许将代码文件视为 ES (ECMAScript)。

异步操作

您将看到许多使用回调和承诺来处理 DynamoDB 操作结果的代码示例。在现代 JavaScript 中,不再需要这种复杂性,开发人员可以利用更简洁、可读性更好的异步/等待语法执行异步操作。

Web 浏览器运行时

使用 React 或 React Native 构建的网络和移动开发人员可以在他们的项目中使用适用于 JavaScript 的 SDK。在较早的 SDK V2 中,Web 开发人员必须将完整的 SDK 加载到浏览器中,并引用托管在 https://sdk.amazonaws.com/js/ 上的 SDK 图片。

在 V3 中,您可以使用 Webpack 将所需的 V3 客户端模块和所有必需的 JavaScript 函数捆绑到一个 JavaScript 文件中,然后将其添加到 HTML 页面 <head> 的脚本标签中,如 SDK 文档的浏览器脚本入门部分所述。

DAX 数据面板操作

适用于 JavaScript 的 SDK V3 目前不支持 Amazon DynamoDB Streams Accelerator(DAX)数据面板操作。如果您申请 DAX 支持,请考虑使用支持 DAX 数据面板操作的 JavaScript SDK V2。