AWS 无服务器入门 - 数据库
在上一篇文章中,我介绍了使用 CDK 在 AWS 上创建 Lambda 函数的基础知识。在本文中,我将介绍如何使用 DynamoDB 将数据存储在数据库中。
⬇️ 我会定期发布无服务器内容,如果你想了解更多 ⬇️
快速公告:我还在开发一个名为🛡 sls-mentor 🛡的库。它汇集了 30 条无服务器最佳实践,这些实践会在您的 AWS 无服务器项目中自动检查(无论使用哪种框架)。它是免费开源的,欢迎随时查看!
使用 DynamoDB 将数据存储在无服务器数据库中
AWS 提供了多种数据存储方式,但在本文中,我将介绍最常见的一种允许以无服务器方式存储数据的服务:DynamoDB。DynamoDB 是一个 NoSQL 数据库,这意味着它不使用 SQL 来查询数据。它是一个键值存储:基本上,您将数据存储在 JSON 对象的形式下,可以使用键进行查询。
与我们上次介绍的 Lambda 和 API Gateway 类似,DynamoDB 由 AWS 托管,并且采用无服务器架构,这意味着您无需管理基础设施,只需为所使用的资源(存储、请求等)付费。当数据量和 IOPS(每秒输入输出)较少时,DynamoDB 是免费的,因此您可以免费开始使用。
在 DynamoDB 中,数据被组织成表。表用于存储键值对。每个表都有一个主键,用于唯一标识表中的每个项目。主键通常由分区键 (PK) 和排序键 (SK) 组成。PK 用于标识已排序数据的分区(数据的子集),SK 用于对分区内的数据进行排序。
所有其他键都称为属性,基本上可以在其中存储任何类型的数据:DynamoDB 的设计初衷是在同一张表中存储多种类型的项目。例如,下面这张表存储了用户和注释:
主键 (PK) 用于确定某项是用户还是笔记。主键 (SK) 用于在表中唯一标识该项,使用唯一 ID(UUID)。其他属性并非始终存在:用户有用户名 (userName) 和年龄 (age),但笔记只有笔记内容 (noteContent)。
例如,使用此设计,您可以通过将公钥设置为“user”进行查询来列出表中的所有用户,或者将公钥设置为“note”,将密钥设置为其唯一 ID 来获取单个注释。请记住,在 DynamoDB 中查询数据时,您始终必须至少指定公钥(在上面的示例中,同时查询用户和注释是一种反模式)。
DynamoDB 是一个非常广泛且复杂的主题,我不会在本文中涵盖所有细节。如果您想了解有关 DynamoDB 的更多信息,我建议您查看官方文档。
示例:创建存储笔记的数据库
让我们创建一个简单的应用程序,将笔记存储在 DynamoDB 表中。在本文的最后,用户将能够创建和阅读笔记。我们还可以实现列出、更新和删除笔记的功能,但我将把这个留作家庭作业🤓。
看一下我们想要构建的架构:
该应用程序将由一个 REST API 组成,该 API 由两个路由、两个 Lambda 函数和一个 DynamoDB 表组成。第一个路由将使用 POST 请求创建笔记,第二个路由将使用 GET 请求读取笔记。Lambda 函数将由 API 网关触发,并与 DynamoDB 表进行交互。
关于数据结构,我们将使用类似于上图所示的设计。PK
是用户 ID,SK
是笔记 ID。笔记内容将存储在noteContent
属性中。此结构允许通过用户 ID 和笔记 ID 获取任何笔记,也可以通过用户 ID 列出该用户的所有笔记。
在本文中,我将从上一篇文章中创建的项目开始。如果您想继续学习,可以克隆代码库并从分支继续introduction
。如果您想从头开始,可以按照上一篇文章的说明,使用 CDK 创建一个新项目。
创建 DynamoDB 表
首先,我们需要创建 DynamoDB 表。与 Lambda 函数类似,我们将使用 CDK 来创建。在my-first-app-stack.ts
文件的构造函数中添加表的声明:
//... previous code
export class MyFirstAppStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
//... previous code
const notesTable = new cdk.aws_dynamodb.Table(this, 'notesTable', {
partitionKey: {
name: 'PK',
type: cdk.aws_dynamodb.AttributeType.STRING,
},
sortKey: {
name: 'SK',
type: cdk.aws_dynamodb.AttributeType.STRING,
},
billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST,
});
}
}
在此代码片段中,您将创建一个表,并将“PK”设置为分区键,“SK”设置为排序键,它们都用于存储string
数据。计费模式设置为“按请求付费”,这意味着您只需按实际使用的资源付费。您也可以为表设置固定价格,但不建议用于小型应用程序。(有关更多信息,请参阅这篇非常精彩的文章)。
创建两个与数据库交互的 Lambda 函数
现在我们有了数据库,我们需要创建两个与之交互的 Lambda 函数。第一个函数用于创建笔记,第二个函数用于读取笔记。像往常一样(照例😎),我们将使用 CDK 来创建这些函数。
在my-first-app-stack.ts
文件中,在构造函数中添加两个函数的声明:
//... previous code
const createNote = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'createNote', {
entry: path.join(__dirname, 'createNote', 'handler.ts'),
handler: 'handler',
environment: {
TABLE_NAME: notesTable.tableName, // VERY IMPORTANT
},
});
const getNote = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'getNote', {
entry: path.join(__dirname, 'getNote', 'handler.ts'),
handler: 'handler',
environment: {
TABLE_NAME: notesTable.tableName, // VERY IMPORTANT
},
});
notesTable.grantWriteData(createNote); // VERY IMPORTANT
notesTable.grantReadData(getNote); // VERY IMPORTANT
⚠️ 请注意,与我们上一篇文章中创建的 Lambda 函数有两点不同。这两个不同之处正是表与函数之间关系的本质:
-
我们设置了包含表名称的环境变量。这样,我们的 lambda 表达式(在 handler.ts 中定义)的运行时代码就能通过使用 来判断要与哪个表进行交互
process.env.TABLE_NAME
。 -
我们授予 Lambda 函数与数据库交互的权限。这一点非常重要,否则 Lambda 函数将无法访问数据库。此权限管理是通过 IAM 策略完成的,目前来说还不算太复杂,但请放心,未来会有一篇文章专门讨论这个(庞大的)主题 😉。
将 Lambda 函数链接到 REST API
现在我们已经创建了 Lambda 函数,我们需要将它们链接到 REST API。在 中的表定义下my-first-app-stack.ts
,添加以下代码:
// myFirstApi was already defined in the previous article
const notesResource = myFirstApi.root.addResource('notes').addResource('{userId}');
notesResource.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(createNote));
notesResource.addResource('{id}').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(getNote));
基本上,我们向 REST API 添加了两种资源:
- POST /notes/{userId} 资源,用于触发
createNote
Lambda 函数。该资源的主体部分包含注释内容。 - GET /notes/{userId}/{id} 资源,它将触发
getNote
Lambda 函数。
创建两个 Lambda 函数的代码
在编写代码之前,需要安装本文所需的两个包:@aws-sdk/client-dynamodb
和uuid
。第一个是用于与数据库通信的官方 AWS SDK for DynamoDB,第二个是用于生成唯一 ID 的包。
npm install @aws-sdk/client-dynamodb uuid
npm install --save-dev @types/uuid
让我们为这两个 Lambda 函数创建代码。在createNote
文件夹中创建一个handler.ts
文件,并添加以下代码:
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { v4 as uuidv4 } from 'uuid';
const client = new DynamoDBClient({});
export const handler = async (event: {
body: string;
pathParameters: { userId?: string };
}): Promise<{ statusCode: number; body: string }> => {
const { content } = JSON.parse(event.body) as { content?: string };
const { userId } = event.pathParameters ?? {};
if (userId === undefined || content === undefined) {
return {
statusCode: 400,
body: 'bad request',
};
}
const noteId = uuidv4();
await client.send(
new PutItemCommand({
TableName: process.env.TABLE_NAME,
Item: {
PK: { S: userId },
SK: { S: noteId },
noteContent: { S: content },
},
}),
);
return {
statusCode: 200,
body: JSON.stringify({ noteId }),
};
};
花点时间理解代码:
- 该处理程序是一个函数,其参数为 pathParameters 和 body。(基于 REST API 的配置)
- 我们从 pathParameters 中提取一个 userId,并从解析的主体中提取未来注释的内容。
- 我们使用该库生成一个唯一的 noteId
uuid
,它将成为笔记的 SK。 - 我们使用 AWS SDK 向数据库发送 PutItemCommand。
- PK 是“note”,SK 是 noteId(就像您在文章的第一个模式中看到的那样)
- noteContent 是笔记的内容,它是一个附加键。
- 所有键都使用类型定义
S
,这是一种 AWS 特殊语法,表示存储的值将是一个字符串。 - 我们使用 process.env.TABLE_NAME 来提供表的名称,该名称在 Lambda 函数的环境变量中定义。
- 最后,我们将 noteId 和成功状态代码返回给客户端,以便以后能够检索该注释。
现在,让我们创建 Lambda 函数的代码getNote
。在getNote
文件夹中,创建一个handler.ts
文件,并添加以下代码:
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({});
export const handler = async (event: {
pathParameters: { userId?: string; id?: string };
}): Promise<{ statusCode: number; body: string }> => {
const { userId, id: noteId } = event.pathParameters ?? {};
if (userId === undefined || noteId === undefined) {
return {
statusCode: 400,
body: 'bad request',
};
}
const { Item } = await client.send(
new GetItemCommand({
TableName: process.env.TABLE_NAME,
Key: {
PK: { S: userId },
SK: { S: noteId },
},
}),
);
if (Item === undefined) {
return {
statusCode: 404,
body: 'not found',
};
}
return {
statusCode: 200,
body: JSON.stringify({
id: noteId,
content: Item.noteContent.S,
}),
};
};
这次,代码稍微简单一些:
- 我们从 pathParameters 中提取 userId 和 noteId,没有主体。
- 我们使用 AWS SDK 向数据库发送 GetItemCommand。
- 使用参数,我们得到等于“note”且等于的
Key
项目。PK
SK
noteId
- 我们还使用 process.env.TABLE_NAME 来提供表的名称。
- 使用参数,我们得到等于“note”且等于的
- 最后,我们返回从数据库中检索到的项目的 noteContent(使用
.S
语法获取字符串值)。
代码写完了!🎉
npm run cdk deploy
测试 API
现在 API 已部署完毕,我们可以测试一下了。要获取 API 的 URL,请查看我上一篇文章。为了测试我的新应用,我首先向 /notes/{userId} 发送一个 POST 命令。我选择了 userId“123”,响应中包含了已创建笔记的 noteId,以便稍后检索。
现在,我可以尝试检索该笔记,以确保它已正确保存在数据库中。为此,我向 /notes/{userId}/{noteId} 发送了一个 GET 请求。
一切如预期!🎉
最后,让我们前往 AWS 控制台检查数据是否正确存储在数据库中。
该项目确实存储在数据库中,并且 noteContent 的值也正确!您可以尝试创建更多笔记并检索它们,您将看到数据已正确存储在数据库中。
家庭作业🤓
该应用程序缺少很多功能:
- 我们无法列出用户的所有注释,如果丢失了 noteId,我们就无法检索注释。
- 我们无法更新或删除注释。
- 笔记只有内容,不能添加标题或日期。
你应该能够自己实现这些功能,但如果你需要帮助,我很乐意帮助你!你可以在Twitter上联系我。以下是一些提示:
- 您可以使用
QueryCommand
列出用户的所有注释,并使用KeyConditionExpression和ExpressionAttributeValues来筛选PK等于userId的项目。
QueryCommand({
KeyConditionExpression: 'PK = :userId',
ExpressionAttributeValues: {
':userId': { S: userId },
},
TableName: process.env.TABLE_NAME,
});
- 您可以使用
PutItemCommand
来更新注释(在 DynamoDB 中创建和更新是相同的)。 - 您可以使用
DeleteItemCommand
来删除注释,指定PK和SK,就像在getNote
函数中一样。
结论
我计划每两个月更新一次这个系列的文章。上一期,我介绍了如何创建由 REST API 触发的简单 Lambda 函数。接下来,我将介绍一些新的主题,例如文件存储、创建事件驱动的应用程序等等。如果您有任何建议,请随时联系我!
如果您能回复并分享这篇文章给您的朋友和同事,我将不胜感激。这将极大地帮助我扩大读者群。另外,别忘了订阅,以便及时收到下一篇文章的更新!
如果您想与我保持联系,请访问我的Twitter 账号。我经常发布或转发有关 AWS 和无服务器的有趣内容,欢迎关注我!
文章来源:https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-databases-kkg