8

我计划使用 AWS Cognito 进行用户身份验证,使用 DynamoDB 进行持久性,并使用 AppSync(以及许多 Mobile Hub)来支持 API -一个书评网站

我很难确定哪个字段应该是我的哈希键,哪个应该是我的排序键,以及我应该创建哪个 LSI/GSI。

我有一个书籍清单,详细信息如下:

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== True]
  • 获取所有“即将出版”的书籍 [ upcoming_release== True]
  • 获取按大多数拇指排序的所有书籍 [按count_thumbsDESC 排序]
  • 获取所有类型为“喜剧”的书籍 [genre_ids包含123或“喜剧”,具体取决于对Q1的回答]
  • 查询名为“哈利波特”的书籍 [ titleLIKE '%Harry Potter%']
  • 获取所有 ISBN 为 1、2、3、4 或 9 的图书 [ isbnIN [1,2,3,4,9] ]

问题 2:在 DynamoDB 中构建图书数据的最佳方式是什么,您会使用哪种哈希/排序/LSI/GSI?

由于我使用的是 Cognito,因此用户配置文件数据存储在 DynamoDB 之外。

问题 3:我是否应该User在 DynamoDB 中有一个表并双重写入新注册,以便在显示评论时使用 AppSync 填充评论的详细信息?如果没有,在填充书评详细信息时如何获取用户的用户名/名字/姓氏?

问题 4:既然我们已经走了这么远,对 graphql 模式有什么建议吗?

4

1 回答 1

8

我会鼓励你阅读这个答案。我之前写过一些关于选择键的一般背景。您还应该打开该答案中的链接,这些链接提供了 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
 });

此操作将检索当前在书店中精选的所有书籍。它使用扫描。如果您还不熟悉scanquerygetItem,那么您绝对应该花一些时间阅读它们。

扫描评估表中的每个项目,因此扫描有时无法在大型表上很好地扩展,并且如果您只检索少数项目,则可能会很昂贵。查询使用分区键返回一组项目,因此通常快速高效。您可以在查询中使用排序键来快速返回分区内的一系列项目。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。

假设您的 books 表有 100 本书,其中 50 本书的 current_featured_in_book_stores=true。进行一次扫描会花费 100 个 RCU,并且不会比一次查询花费更多。现在假设只有一本书的 current_featured_in_book_stores=true,执行一次扫描需要 100 个 RCU,但一次查询只需要 1 个 RCU。但是,您应该在添加 GSI 之前仔细考虑,它们不与基表共享吞吐量,并且您必须为您的 GSI 单独购买 RCU。如果您未配置 GSI,它最终可能会比在配置良好的基表上扫描要慢。

布尔值是一个错误的分区键,我会在这里进行扫描。也就是说,如果您在查询上方创建 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
 });

您可以在此处查看条件运算符

获取所有 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

是的,如果您将流派存储为每个项目的字符串,并且更改流派名称,则必须更新每个项目。或者作为替代方案,您必须在将项目呈现给用户之前更新项目的类型。

如果您希望更改流派名称,使用genre_id 映射的想法似乎是一个好主意。只需有一个流派名称和 ID 表,在您的应用程序启动时加载它并将其保存在应用程序内存中。您可能需要一个管理函数来重新加载类型映射表。

将应用程序参数保存在数据库中是一种很好用的设计。

问题 3

当然,在 DynamoDB 中有一个用户表。这就是我在使用 Cognito 的应用程序中执行此操作的方式。我在 Cognito 中存储了一组与用户注册相关的最小字段,然后我在用户表中的 DynamoDB 中有大量应用程序特定的数据。

问题 4

关于图形模式,我会查看AWS 的这篇文章。不太确定这是否有帮助。

于 2018-05-08T13:00:23.023 回答