DynamoDB:我用例的最佳 hash/sort 键 [与 AppSync/GraphQL 混淆]

DynamoDB: Best hash/sort keys for my use case [confusion with AppSync/GraphQL]

我计划使用 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!
}

就我而言,书籍可以有多种类型 - 例如"Fantasy" 和 "Drama"。书籍也有用户的评论,他们的数据存储在 Cognito 中。我们将在每本书旁边按时间倒序显示评论。

问题 1:如果我非规范化并使用 Drama 作为流派而不是流派 ID 2,那么如果我需要稍后将流派重命名为 [=16] 怎么办=]...我不需要更新每个项目吗?

我至少需要能够回答:

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

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

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

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

我鼓励您阅读 。我之前写过一些关于选择键的一般背景。您还应该打开该答案中的链接,其中提供了 AWS 就该主题提供的大部分关键信息。

在提供答案之前,我认为我还应该说明数据架构通常会考虑很多因素。您在问题中提供了一些非常好的信息,但不可避免地没有足够的信息来提供明确的 'best' 解决方案。事实上,即使有更多的信息,你也会得到不同的意见。

问题 2

也就是说,这就是我在您的情况下会考虑做的事情。我会考虑创建一个名为 Books 的 table 和一个名为 BookReviews 的 table。

Table: Books
Partition Key: ISBN

Table: BookReviews
Partition Key: ISBN
Sort Key: BookReview-id

我不打算创建任何 GSI 或 LSI。

您的大部分查询都涉及查找 'all books' 并以某种方式对它们进行排序。这些列表听起来对时间不敏感。例如,当用户询问最流行的 100 本书时,他们是否需要知道最流行的书,包括直到最后一秒统计的每一票?我对此表示怀疑。此外,这些列表是否特定于个人用户?听起来不像。

我的一般提示是这样的;将原始数据存储在 DynamoDB 中,并实时更新。创建您的常用书籍列表并偶尔(也许每天)更新一次,将这些列表存储在缓存中。您可以选择将这些列表存储在单独的 table 中的 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. If you not already familiar with scan, query and getItem 你绝对应该花一些时间阅读它们。

扫描会评估 table 中的每个项目,因此扫描有时无法在大型 table 上很好地扩展,如果您只检索少数项目,则可能会很昂贵。查询使用分区键 return 一组项目,因此通常快速高效。您可以在查询中使用排序键来快速 return 分区中的一系列项目。 GetItem使用唯一主键,效率很高

如果您的 table 有 100 个项目,您执行的任何扫描都将花费 100 个 RCU。如果您执行查询,并且查询的分区中只有 2 个项目,则将花费您 2 个 RCU。

如果书籍 table 中的很大一部分项目 currently_featured_in_book_stores=true,我会进行扫描。如果 table 中只有少数项目具有 currently_featured_in_book_stores=true 并且这是一个非常频繁的查询,您可以考虑在 Books table 上创建一个分区键为 currently_featured_in_book_stores 和 ISBN 的排序键。

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

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

我不经常进行这种扫描并将结果缓存在临时存储中(即在应用程序内存中)。

将所有书籍按大多数人排序

这里重要的是 'Get all books...'。这会立即告诉您扫描可能是最佳方法。您可以将查询视为只查看一个分区的扫描。你不想看书的分区,你想要所有的书,所以扫描是要走的路。

DynamoDB return 排序项目的唯一方法是对 table 或具有排序键的索引执行查询。在这种情况下,项目将自动 return 根据排序键按排序顺序排列。因此,对于此搜索,您只需要进行扫描以获取所有书籍,然后根据您选择的属性(拇指)客户端对它们进行排序。扫描只是 return 所有书籍,看起来像这样。

 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 个。如果用户在缓存范围之外进行分页,您可能需要进行新的扫描。我认为您更有可能只限制项目的数量并停止用户进一步分页。

获取类型为"Comedy"

的所有书籍

同样,我很可能会不经常进行扫描并缓存列表。您可以考虑添加具有分区键类型和排序键 ISBN 的 GSI。就我个人而言,我将从扫描和缓存方法开始,然后看看您的进展情况。您随时可以在以后添加 GSI。

查询名为 "Harry Potter"

的图书

显然你不能缓存这个。在 title

上使用 filterexpression 进行扫描
 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

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

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

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

问题 3

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

的 DynamoDB 中有大量应用程序特定数据

问题 4

关于图形模式,我会查看 this articles by AWS。不太确定这是否有帮助。