第 2 步:检查数据模型和实施详细信息 - Amazon DynamoDB
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

第 2 步:检查数据模型和实施详细信息

2.1:基本数据模型

此示例应用程序重点介绍了以下 DynamoDB 数据模型概念:

  • – 在 DynamoDB 中,表是项目(即记录)的集合,而每个项目是称为属性的名称-值对的集合。

    在本井字游戏示例中,应用程序将所有游戏数据存储在表 Games 中。应用程序表为每款游戏在表中创建一个项目,并将所有游戏数据存储为属性。井字游戏最多可以移动九次。由于 DynamoDB 表采用的架构并不是只有主键是必需属性,应用程序可以为每个游戏项目存储不同数量的属性。

    Games 表具有简单主键,由一个字符串类型的属性 GameId 组成。应用程序将唯一 ID 分配给每款游戏。有关 DynamoDB 主键的更多信息,请参阅 主键

    当用户通过邀请其他用户玩游戏的方式发起井字游戏之后,应用程序会在 Games 表中使用存储游戏元数据的属性创建一个新项目,如下所示:

    • HostId,发起游戏的用户。

    • Opponent,受邀参加游戏的用户。

    • 轮到进行移动的用户。发起游戏的用户首先移动。

    • 在面板上使用 O 符号的用户。发起游戏的用户使用 O 符号。

    此外,该应用程序创建 StatusDate 连接属性,将初始游戏的状态标记为 PENDING。以下屏幕截图显示了示例项目在 DynamoDB 控制台中的外观:

    
                            属性表的控制台屏幕截图。

    随着游戏继续,游戏中每一次进行移动时,本应用程序都会将一个属性添加到表中。属性名称是面板上的位置,例如 TopLeftBottomRight。例如,移动可能具有值为 OTopLeft 属性、值为 OTopRight 属性以及值为 XBottomRight 属性。属性值为 OX,具体取决于进行移动的用户。例如,请考虑以下面板。

    
                            显示以平局结束的已完成的井字游戏的屏幕截图。
  • 连接值属性StatusDate 属性说明了一个连接值属性。在此方法中,您无需创建单独的属性用于存储游戏状态(PENDINGIN_PROGRESSFINISHED)以及日期(上一次移动的时间),而是可以将它们复合为单个属性,例如 IN_PROGRESS_2014-04-30 10:20:32

    然后,该应用程序在创建二级索引时,通过将 StatusDate 指定为索引的排序键来使用 StatusDate 属性。使用 StatusDate 连接值属性的优势将在接下来讨论的索引中进一步说明。

  • 全局二级索引 – 您可以使用表的主键 GameId 来高效地查询表以查找游戏项目。为了查询表中的属性而不是主键属性,DynamoDB 支持创建二级索引。在本示例应用程序中,您可以构建以下两个二级索引:

    
                            显示在示例应用程序中创建的 hostStatusDate 和 oppStatusDate 全局二级索引的屏幕截图。
    • HostId-StatusDate-index。此索引将 HostId 作为分区键,StatusDate 作为排序键。您可以使用此索引查询 HostId,例如,用于查找特定用户发起的游戏。

    • OpponentId-StatusDate-index。此索引将 OpponentId 作为分区键,StatusDate 作为排序键。可以使用此索引查询 Opponent,例如寻找某个用户作为对手的游戏。

    这些索引称为全局二级索引,因为这些索引中的分区键与表主键中使用的分区键 (GameId) 并不相同。

    请注意,这两个索引均指定 StatusDate 作为排序键。执行此操作可以实现:

    • 您可以使用 BEGINS_WITH 比较运算符进行查询。例如,您可以使用 IN_PROGRESS 属性查找特定用户启动的所有游戏。在这种情况下,BEGINS_WITH 运算符会检查以 IN_PROGRESS 开头的 StatusDate 值。

    • DynamoDB 会按排序键值的排序顺序存储索引中的项目。因此,如果前缀的所有状态均相同(例如均为 IN_PROGRESS),则日期部分使用的 ISO 格式将项目按照从最早到最新排序。此方法使得特定查询可以高效执行,如下例:

      • 检索登录用户最近发起的最多 10 款 IN_PROGRESS 游戏。对于此查询,需要指定 HostId-StatusDate-index 索引。

      • 检索登录用户作为对手参加的最近 10 款 IN_PROGRESS 游戏。对于此查询,需要指定 OpponentId-StatusDate-index 索引。

有关二级索引的更多信息,请参阅 使用二级索引改进数据访问

2.2:操作中的应用程序(代码演练)

此应用程序有两个主要页面:

  • 主页 – 此页面向用户提供简单的登录界面、用于创建新的井字游戏的创建按钮、正在进行的游戏列表、游戏历史记录以及任何正在等待接受的游戏邀请。

    主页不会自动刷新;您必须手动刷新页面才能刷新列表。

  • 游戏页面 – 此页面显示用户玩游戏的井字游戏网格。

    应用程序每秒自动更新游戏页面。浏览器中的 JavaScript 每秒调用 Python Web 服务器来查询 Games 表中的游戏项目是否有更改。如果有更改,则 JavaScript 触发页面刷新,这样用户可以看到更新后的面板。

让我们详细了解应用程序的工作方式。

主页

用户登录后,应用程序显示以下三个信息列表。


                        显示包含 3 个列表:待定邀请、正在进行的游戏和最近历史记录的应用程序的屏幕截图。
  • 邀请 – 此列表显示其他人向登录用户发出的最近 10 个邀请,这些邀请处于等待接受的状态。在上述屏幕截图中,user1 具有来自 user5 和 user2 的邀请等待接受。

  • Games in-progress(正在进行的游戏)– 此列表显示最近 10 款正在进行的游戏。这些是用户当前正在玩的游戏,其状态为 IN_PROGRESS。在屏幕截图中,user1 正在与 user3 和 user4 玩井字游戏。

  • Recent history(最近历史记录)– 此列表显示用户最近完成的 10 款游戏,其状态均为 FINISHED。在屏幕截图显示的游戏中,user1 以前和 user2 一起玩过游戏。对于每一局已完成的游戏,列表中会显示游戏结果。

在代码中,index 函数(application.py 中)进行以下三个调用来检索游戏状态信息:

inviteGames = controller.getGameInvites(session["username"]) inProgressGames = controller.getGamesWithStatus(session["username"], "IN_PROGRESS") finishedGames = controller.getGamesWithStatus(session["username"], "FINISHED")

这些调用中的每个调用都将返回一个 DynamoDB 中由 Game 对象包装的项目列表。可以很方便地从视图中的这些对象提取数据。索引函数将这些对象列表传递到视图以呈现 HTML。

return render_template("index.html", user=session["username"], invites=inviteGames, inprogress=inProgressGames, finished=finishedGames)

井字游戏应用程序定义 Game 类,主要用于存储从 DynamoDB 检索到的游戏数据。这些函数返回 Game 对象的列表,使您可以将应用程序的剩余部分与 Amazon DynamoDB 项目的相关代码隔离开。因此,这些函数帮助您将应用程序代码与数据存储层的详细信息分离开。

此处所说的架构模式也称为模型-视图-控制器 (MVC) UI 模式。在这种情况下,Game 对象示例(表示数据)是模型,HTML 页面是视图。控制器分为两个文件。application.py 文件具有 Flask 框架的控制器逻辑,而业务逻辑单独保存在 gameController.py 文件中。也就是说,应用程序将与 DynamoDB SDK 相关的所有内容存储到 dynamodb 文件夹中自己的独立文件内。

现在,让我们回顾一下这三种功能,以及它们如何使用全局二级索引检索相关数据以查询 Games 表。

使用 getGameInvites 获取处于等待接受邀请状态的游戏列表

getGameInvites 函数检索最近 10 个处于等待接受状态的邀请列表。这些游戏由用户创建,但对手尚未接受游戏邀请。对于这些游戏,状态保持为 PENDING,直至对手接受邀请。如果对手拒绝了邀请,应用程序会从表中删除对应的项目。

该函数指定查询如下所示:

  • 它指定 OpponentId-StatusDate-index 索引使用以下索引键值和比较运算符:

    • 分区键是 OpponentId,获取索引键 user ID

    • 排序键是 StatusDate,获取比较运算符和索引键值 beginswith="PENDING_"

    使用 OpponentId-StatusDate-index 索引来检索登录用户接到邀请的游戏—这种情况下登录用户将成为对手。

  • 查询将结果限制为 10 个项目。

gameInvitesIndex = self.cm.getGamesTable().query( Opponent__eq=user, StatusDate__beginswith="PENDING_", index="OpponentId-StatusDate-index", limit=10)

在索引中,对于每个 OpponentId(分区键),DynamoDB 均保存按 StatusDate(排序键)排序的项目。因此,查询将返回最近的 10 款游戏。

使用 getGamesWithStatus 来获取具有特定状态的游戏的列表

对手接受游戏邀请之后,游戏状态将变为 IN_PROGRESS。游戏完成后,状态将变为 FINISHED

查找正在进行及已完成游戏时,所用查询仅状态值不同。因此,应用程序定义 getGamesWithStatus 函数,其中采用状态值作为参数。

inProgressGames = controller.getGamesWithStatus(session["username"], "IN_PROGRESS") finishedGames = controller.getGamesWithStatus(session["username"], "FINISHED")

以下部分讨论了正在进行的游戏,不过这些说明也适用于已完成的游戏。

给定用户正在进行的游戏列表包括以下内容:

  • 由用户发起的正在进行的游戏

  • 用户作为对手参加的正在进行的游戏

getGamesWithStatus 函数运行以下两个查询,每次使用相应的二级索引。

  • 此函数使用 HostId-StatusDate-index 索引查询 Games 表。对于索引,查询指定两个主键值— 分区键 (HostId) 和排序键 (StatusDate) 值,以及比较运算符。

    hostGamesInProgress = self.cm.getGamesTable ().query(HostId__eq=user, StatusDate__beginswith=status, index="HostId-StatusDate-index", limit=10)

    请注意比较运算符的 Python 语法:

    • HostId__eq=user 指定相等比较运算符。

    • StatusDate__beginswith=status 指定 BEGINS_WITH 比较运算符。

  • 此函数使用 OpponentId-StatusDate-index 索引查询 Games 表。

    oppGamesInProgress = self.cm.getGamesTable().query(Opponent__eq=user, StatusDate__beginswith=status, index="OpponentId-StatusDate-index", limit=10)
  • 然后,函数将两个列表组合并排序,对前 10 个项目创建 Game 对象列表,然后将列表返回到调用函数(即索引)。

    games = self.mergeQueries(hostGamesInProgress, oppGamesInProgress) return games

游戏页面

游戏页面是玩井字游戏的位置,其中显示游戏网格以及与游戏相关的信息。以下屏幕截图显示了正在进行的游戏示例:


                        显示正在进行的井字游戏的屏幕截图。

应用程序在以下情况下显示游戏页面:

  • 用户创建游戏,邀请其他用户一起玩。

    在这种情况下,页面将用户显示为发起人,游戏状态为 PENDING,等待对手接受。

  • 用户在主页上接受一个等待接受的邀请。

    在这种情况下,页面会将用户显示为对手,并且游戏状态为 IN_PROGRESS

用户在面板时进行选择时可生成一个对应用程序的表单 POST 请求。即,Flask 使用 HTML 表单数据调用 selectSquare 函数(application.py 中)。此函数随之调用 updateBoardAndTurn 函数(gameController.py 中)以更新游戏项目,如下所示:

  • 它添加特定于移动的新属性。

  • 它将 Turn 属性值更新为应该走下一步的用户。

controller.updateBoardAndTurn(item, value, session["username"])

如果项目更新成功,则函数返回 true,否则返回 false。请注意有关 updateBoardAndTurn 函数的以下内容:

  • 此函数调用 SDK for Python 的 update_item 函数,用于对现有项目进行有限数量的更新。该函数映射到 DynamoDB 中的 UpdateItem 操作。有关更多信息,请参见 UpdateItem

    注意

    UpdateItemPutItem 操作之间的不同在于 PutItem 替换整个项目。有关更多信息,请参见 PutItem

对于 update_item 调用,代码标识以下内容:

  • Games 表的主键(即 ItemId)。

    key = { "GameId" : { "S" : gameId } }
  • 要添加的新属性,特定于当前用户移动及其值(如 TopLeft="X")。

    attributeUpdates = { position : { "Action" : "PUT", "Value" : { "S" : representation } } }
  • 必须满足以下条件才能进行更新:

    • 游戏必须在进行中。也就是说,StatusDate 属性值必须以 IN_PROGRESS 开头。

    • 当前的轮次必须是 Turn 属性指定的有效用户轮次。

    • 用户选择的方框必须可用。也就是说,与方框对应的属性必须不存在。

    expectations = {"StatusDate" : {"AttributeValueList": [{"S" : "IN_PROGRESS_"}], "ComparisonOperator": "BEGINS_WITH"}, "Turn" : {"Value" : {"S" : current_player}}, position : {"Exists" : False}}

现在,函数调用 update_item 以更新项目。

self.cm.db.update_item("Games", key=key, attribute_updates=attributeUpdates, expected=expectations)

此函数返回之后,由 selectSquare 函数调用重定向,如以下示例中所示。

redirect("/game="+gameId)

此调用导致浏览器刷新。作为此次刷新的一部分,应用程序检查以查看游戏以获胜还是平手结束。如果已结束,则应用程序将相应地更新游戏项目。