我计划使用 AWS Cognito 进行用户身份验证、使用 DynamoDB 进行持久化和使用 AppSync(以及许多 Mobile Hub)来支持 API——一个书评网站。
我很难确定哪个字段应该是我的散列键,哪个应该是我的排序键,以及我应该创建哪个 LSI/GSI。
我有一个书籍 list ,其中包含如下详细信息:
type Book {
isbn: Int!
year: Int!
title: String!
description: String
front_cover_photo_url: String
genre_ids: [Int]
count_thumbs: Int
us_release_date: String
upcoming_release: Boolean
currently_featured_in_book_stores: Boolean
best_seller: Boolean
reviews: [Review]
}
每次用户写关于一本书的评论时,我也有一个评论记录。
type Review {
isbn: Int!
id: ID!
created_at: String!
# The user that submitted the review
user_id: String!
# The number of thumbs out of 5
thumbs: Int!
# Comments on the review
comments: String!
}
就我而言,书籍可以有多种类型——例如“幻想”和“戏剧”。书籍也有用户的评论,其数据存储在 Cognito 中。我们将在每本书旁边按时间倒序显示评论。
问题 1:如果我非规范化并使用
Drama
作为流派而不是流派 ID 2
,那么如果我稍后需要将流派重命名为 Dramatic
怎么办? ...我不需要更新每个项目吗? 我至少需要能够回答:
currently_featured_in_book_stores
== 真] upcoming_release
== 真] count_thumbs
DESC] genre_ids
包含 123
或“喜剧”取决于对 的回答第一季度 ] title
LIKE '%Harry Potter%'] isbn
在 [1,2,3,4,9] ] 问题 2:在 DynamoDB 中构建图书数据的最佳方法是什么,您会使用哪种散列/排序/LSI/GSI?
由于我使用的是 Cognito,因此用户配置文件数据存储在 DynamoDB 之外。
问题 3:我应该有一个
User
DynamoDB 中的表和双重写入新注册,所以我可以在显示评论时使用 AppSync 填充评论的详细信息?如果没有,我将如何在填充书评详细信息时获得用户的用户名/名字/姓氏? 问题 4:既然我们已经走了这么远,对 graphql 模式有什么建议吗?
最佳答案
我鼓励你阅读 this answer .我之前写过一些关于选择键的一般背景。您还应该打开该答案中的链接,其中提供了 AWS 提供的有关该主题的大部分关键信息。
在提供答案之前,我想我还应该说明一下,数据架构通常会考虑很多因素。您在问题中提供了一些非常好的信息,但不可避免地没有足够的信息来提供明确的“最佳”解决方案。事实上,即使有更多的信息,你也会得到不同的意见。
问题2
也就是说,这就是我在你的情况下会考虑做的事情。我会考虑创建一个名为 Books 的表和一个名为 BookReviews 的表。
Table: Books
Partition Key: ISBN
Table: BookReviews
Partition Key: ISBN
Sort Key: BookReview-id
我不希望创建任何 GSI 或 LSI。
您的大多数查询都涉及查找“所有书籍”并以某种方式对其进行排序。这些列表听起来对时间不敏感。例如,当用户询问最受欢迎的 100 本书时,他们是否需要知道最受欢迎的书,包括直到最后一秒计算的每一票?我对此表示怀疑。此外,这些列表是否特定于个人用户?听起来不像。
我的一般提示是这样的;将您的原始数据存储在 DynamoDB 中,并实时更新。创建您的常用书籍列表并不时(可能每天)更新它们,将这些列表存储在缓存中。您可以选择将这些列表存储在 DynamoDB 中的单独表中,并在缓存被破坏时查询它们。
获取当前在书店推荐的所有书籍
var params = {
TableName: "Books",
ExpressionAttributeValues: {
":a": {
BOOL: true
}
},
FilterExpression: "currently_featured_in_book_stores = :a"
};
dynamodb.scan(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
此操作将检索当前在书店中出售的所有书籍。它使用 scan .如果您还不熟悉 scan , query和 getItem你绝对应该花一些时间阅读它们。
扫描会评估表中的每个项目,因此扫描有时无法在大型表上很好地扩展,如果您只检索少数项目,则可能会很昂贵。查询使用分区键返回一组项目,因此通常快速高效。您可以在查询中使用排序键快速返回分区内的一系列项目。 GetItem 使用唯一的主键并且非常高效。
如果您的表有 100 个项目,则您执行的任何扫描都将花费 100 个 RCU。如果您执行查询,并且查询分区中只有 2 个项目,则将花费您 2 个 RCU。
如果 Books 表中很大一部分项目的 current_featured_in_book_stores=true,我会进行扫描。如果表中只有少数项目有 current_featured_in_book_stores=true 并且这是一个非常频繁的查询,您可以考虑在 Books 表上创建一个 GSI,分区键为 current_featured_in_book_stores,排序键为 ISBN。
假设您的图书表有 100 本书,其中 50 本书的 current_featured_in_book_stores=true。进行一次扫描需要 100 个 RCU,而且不会比查询多多少。现在假设只有一本书的 current_featured_in_book_stores=true,执行扫描将花费 100 个 RCU,但查询仅花费 1 个 RCU。但是,您在添加 GSI 之前应该仔细考虑,它们不与基表共享吞吐量,您必须为您的 GSI 单独购买 RCU。如果您未配置 GSI,它最终可能比在配置良好的基表上进行扫描更慢。
bool 值是一个错误的分区键,我会在这里进行扫描。也就是说,如果您在上面创建了 GSI,您的查询将如下所示:
var params = {
TableName: "Books",
IndexName: "Index_Books_In_Stores",
ExpressionAttributeValues: {
":v1": {
BOOL: true
}
},
KeyConditionExpression: "currently_featured_in_book_stores = :v1"
};
dynamodb.query(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
获取所有即将出版的书籍
以上所有内容仍然适用。我会做这样的扫描
var params = {
TableName: "Books",
ExpressionAttributeValues: {
":a": {
BOOL: true
}
},
FilterExpression: "upcoming_release = :a"
};
dynamodb.scan(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
我会不经常执行此扫描并将结果缓存在临时存储中(即在应用程序内存中)。
获取所有图书按最多拇指排序
这里重要的是“获取所有书籍......”。这会立即告诉您扫描可能是最佳方法。您可以将查询视为仅查看一个分区的扫描。您不想查看部分书籍,而是想要查看所有书籍,因此扫描是最佳选择。
DynamoDB 返回排序项的唯一方式是对具有排序键的表或索引执行查询。在这种情况下,项目将根据排序键自动按排序顺序返回。因此,对于此搜索,您只需扫描以获取所有书籍,然后按您选择的属性(拇指)客户端对它们进行排序。扫描只是返回所有书籍,看起来像这样。
var params = {
TableName: "Books"
};
dynamodb.scan(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
同样,我会很少进行此扫描并缓存顶级书籍。您可以对缓存进行排序并只检索您需要的项目数,可能是前 10、100 或 1000。如果用户继续进行超出缓存范围的分页,您可能需要进行新的扫描。我认为您更有可能只是限制项目数量并停止用户进一步分页。
获取“喜剧”类型的所有书籍
同样,我很可能会不经常进行扫描并缓存列表。您可以考虑添加具有分区键类型和排序键 ISBN 的 GSI。就我个人而言,我会从扫描和缓存方法开始,看看你如何进行。您以后可以随时添加 GSI。
查询名为“哈利波特”的书
显然你不能缓存这个。使用过滤器表达式对标题进行扫描
var params = {
TableName: "Books",
ExpressionAttributeValues: {
":a": {
S: "Harry Potter"
}
},
FilterExpression: "title CONTAINS :a"
};
dynamodb.scan(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
您可以查看 condition operators here
获取所有 ISBN 号为 1、2、3、4 或 9 的图书
对于这个,在每个单独的 ISBN 上做一个 GetItem 并将它添加到一个集合中。下面的查询得到一本书。您可以将其放入循环中并遍历您想要获取的 ISBN 集。
var params = {
Key: {
"ISBN": {
S: "1"
}
},
TableName: "Books"
};
dynamodb.getItem(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
问题 1
是的,如果您将流派存储为每个项目的字符串,并且您更改流派名称,则必须更新每个项目。或者作为替代方案,您必须在将项目呈现给用户之前更新项目的类型。
如果您希望更改流派名称,使用流派_id 映射的想法似乎是一个不错的想法。只需有一个流派名称和 ID 表,在您的应用程序启动时加载它并将其保存在应用程序内存中。您可能需要一个管理功能来重新加载流派映射表。
将应用程序参数保存在数据库中是一种很好的设计。
问题 3
当然,在 DynamoDB 中有一个 User 表。这就是我在使用 Cognito 的应用程序中执行此操作的方式。我在 Cognito 中存储了一组与用户注册相关的最小字段,然后我在用户表中的 DynamoDB 中有大量特定于应用程序的数据。
问题 4
关于图形模式,我会查看 this articles by AWS .不太确定这是否有帮助。
关于amazon-web-services - DynamoDB : Best hash/sort keys for my use case [confusion with AppSync/GraphQL],我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50196885/