第 2 步:检查数据模型和实施详细信息
2.1:基本数据模型
此示例应用程序重点介绍了以下 DynamoDB 数据模型概念:
-
表 – 在 DynamoDB 中,表是项目(即记录)的集合,而每个项目是称为属性的名称-值对的集合。
在本井字游戏示例中,应用程序将所有游戏数据存储在表
Games
中。应用程序表为每款游戏在表中创建一个项目,并将所有游戏数据存储为属性。井字游戏最多可以移动九次。由于 DynamoDB 表采用的架构并不是只有主键是必需属性,应用程序可以为每个游戏项目存储不同数量的属性。Games
表具有简单主键,由一个字符串类型的属性GameId
组成。应用程序将唯一 ID 分配给每款游戏。有关 DynamoDB 主键的更多信息,请参阅 主键。当用户通过邀请其他用户玩游戏的方式发起井字游戏之后,应用程序会在
Games
表中使用存储游戏元数据的属性创建一个新项目,如下所示:-
HostId
,发起游戏的用户。 -
Opponent
,受邀参加游戏的用户。 -
轮到进行移动的用户。发起游戏的用户首先移动。
-
在面板上使用 O 符号的用户。发起游戏的用户使用 O 符号。
此外,该应用程序创建
StatusDate
连接属性,将初始游戏的状态标记为PENDING
。以下屏幕截图显示了示例项目在 DynamoDB 控制台中的外观:随着游戏继续,游戏中每一次进行移动时,本应用程序都会将一个属性添加到表中。属性名称是面板上的位置,例如
TopLeft
或BottomRight
。例如,移动可能具有值为O
的TopLeft
属性、值为O
的TopRight
属性以及值为X
的BottomRight
属性。属性值为O
或X
,具体取决于进行移动的用户。例如,请考虑以下面板。 -
-
连接值属性 –
StatusDate
属性说明了一个连接值属性。在此方法中,您无需创建单独的属性用于存储游戏状态(PENDING
、IN_PROGRESS
和FINISHED
)以及日期(上一次移动的时间),而是可以将它们复合为单个属性,例如IN_PROGRESS_2014-04-30 10:20:32
。然后,该应用程序在创建二级索引时,通过将
StatusDate
指定为索引的排序键来使用StatusDate
属性。使用StatusDate
连接值属性的优势将在接下来讨论的索引中进一步说明。 -
全局二级索引 – 您可以使用表的主键
GameId
来高效地查询表以查找游戏项目。为了查询表中的属性而不是主键属性,DynamoDB 支持创建二级索引。在本示例应用程序中,您可以构建以下两个二级索引:-
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
索引。
-
-
有关二级索引的更多信息,请参阅 在 DynamoDB 中使用二级索引改进数据访问。
2.2:操作中的应用程序(代码演练)
此应用程序有两个主要页面:
-
主页 – 此页面向用户提供简单的登录界面、用于创建新的井字游戏的创建按钮、正在进行的游戏列表、游戏历史记录以及任何正在等待接受的游戏邀请。
主页不会自动刷新;您必须手动刷新页面才能刷新列表。
-
游戏页面 – 此页面显示用户玩游戏的井字游戏网格。
应用程序每秒自动更新游戏页面。浏览器中的 JavaScript 每秒调用 Python Web 服务器来查询 Games 表中的游戏项目是否有更改。如果有更改,则 JavaScript 触发页面刷新,这样用户可以看到更新后的面板。
让我们详细了解应用程序的工作方式。
主页
用户登录后,应用程序显示以下三个信息列表。
-
邀请 – 此列表显示其他人向登录用户发出的最近 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。注意
UpdateItem
和PutItem
操作之间的不同在于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)
此调用导致浏览器刷新。作为此次刷新的一部分,应用程序检查以查看游戏以获胜还是平手结束。如果已结束,则应用程序将相应地更新游戏项目。