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} 资源,用于触发
createNoteLambda 函数。该资源的主体部分包含注释内容。 - GET /notes/{userId}/{id} 资源,它将触发
getNoteLambda 函数。
创建两个 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项目。PKSKnoteId - 我们还使用 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
后端开发教程 - Java、Spring Boot 实战 - msg200.com





