本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。
步驟 2:檢查資料模型和實作詳細資訊
2.1:基本資料模型
此範例應用程式的 DynamoDB 資料模型概念重點如下:
-
Table (資料表):在 DynamoDB 中,資料表為項目 (即紀錄) 的集合,而每一個項目都是名稱/值對的集合,稱為屬性。
在此 Tic-Tac-Toe範例中,應用程式會將所有遊戲資料儲存在資料表 中
Games
。應用程式會為每一場遊戲在資料表中建立一個項目,並將所有遊戲資料以屬性的形式存放。 tic-tac-toe 遊戲最多可以有九個移動。因為 DynamoDB 資料表沒有結構描述,在必要屬性僅為主索引鍵的情況下,應用程式可針對每個遊戲項目存放不同數目的屬性。Games
資料表具有由單一屬性GameId
組成的簡易主索引鍵,其類型為字串。該應用程式會為每一場遊戲指派唯一的 ID。如需 DynamoDB 主索引鍵的詳細資訊,請參閱 主索引鍵。當使用者透過邀請其他使用者進行遊戲來啟動 tic-tac-toe遊戲時,應用程式會在
Games
資料表中建立新的項目,其中包含儲存遊戲中繼資料的屬性,例如:-
HostId
,起始遊戲的使用者。 -
Opponent
,獲邀玩遊戲的使用者。 -
輪到玩遊戲的使用者。起始遊戲的使用者先開始玩遊戲。
-
在棋盤上使用 O 符號的使用者。起始遊戲的使用者使用 O 符號。
此外,應用程式也會建立
StatusDate
串連屬性,將初始遊戲狀態標記為PENDING
。下列螢幕擷取畫面顯示範例項目在 DynamoDB 主控台中的樣子:隨著遊戲的進行,應用程式會為遊戲中的每步移動,新增一個屬性到資料表。屬性名稱為棋盤位置,例如
TopLeft
或BottomRight
。例如,移動可能是值為TopLeft
的O
屬性、值為TopRight
的O
屬性,及值為BottomRight
的X
屬性。屬性值不是O
就是X
,取決於移動的使用者。例如,請參閱下列棋盤。 -
-
Concatenated value attributes (串連值屬性):
StatusDate
屬性為串連值屬性的範例。在此方法中,並非透過建立個別屬性來存放遊戲狀態 (PENDING
、IN_PROGRESS
和FINISHED
) 及日期 (最後一步的移動時間),而是將其合併為單一屬性,例如IN_PROGRESS_2014-04-30 10:20:32
。然後應用程式會使用
StatusDate
屬性,將StatusDate
指定為索引的排序索引鍵,來建立次要索引。使用StatusDate
串連值屬性的好處會在接下來討論的索引中進一步說明。 -
Global secondary indexes (全域次要索引):您可以使用資料表的主索引鍵 (
GameId
) 有效率地查詢資料表,來尋找遊戲項目。若要查詢資料表上主索引鍵屬性以外的屬性,DynamoDB 支援建立次要索引。在此範例應用程式中,您會建置下列兩個次要索引:-
HostId-StatusDate-index 。此索引的分割區索引鍵為
HostId
,排序索引鍵為StatusDate
。您可以使用此索引查詢HostId
,例如尋找由特定使用者發起的遊戲。 -
OpponentId-StatusDate-index 。此索引的分割區索引鍵為
OpponentId
,排序索引鍵為StatusDate
。您可以使用此索引查詢Opponent
,例如尋找對手為特定使用者的遊戲。
這些索引稱為全域次要索引,因為這些索引中的分割區索引鍵與資料表主索引鍵所使用的分割區索引鍵 (
GameId
) 不同。請注意,這兩種索引皆指定
StatusDate
做為排序索引鍵。這可讓您進行下列作業:-
您可以使用
BEGINS_WITH
比較運算子查詢。例如,您可以尋找由特定使用者發起且屬性為IN_PROGRESS
的所有遊戲。在此案例中,BEGINS_WITH
運算子會檢查開頭為StatusDate
的IN_PROGRESS
值。 -
DynamoDB 會根據排序索引鍵值,將項目依排序存放於索引中。因此,如果所有狀態字首都相同 (例如
IN_PROGRESS
),則用於日期部分的ISO格式會從最舊到最新的項目排序。此方法可讓特定查詢有效率地執行,例如下列查詢:-
擷取最多 10 個最近由登入使用者發起的
IN_PROGRESS
遊戲。針對此查詢,您要指定HostId-StatusDate-index
索引。 -
擷取最多 10 個最近登入使用者為對手的
IN_PROGRESS
遊戲。針對此查詢,您要指定OpponentId-StatusDate-index
索引。
-
-
如需次要索引的詳細資訊,請參閱「使用 DynamoDB 中的次要索引改善資料存取」。
2.2:應用程式的實際運作 (程式碼演練)
此應用程式有兩個主要頁面:
-
首頁 – 此頁面為使用者提供簡單的登入、建立新 tic-tac-toe遊戲的CREATE按鈕、進行中遊戲的清單、遊戲歷史記錄,以及任何作用中的待定遊戲邀請。
首頁不會自動重新整理,您必須重新整理頁面才能重新整理清單。
-
遊戲頁面 – 此頁面顯示 tic-tac-toe使用者播放的網格。
應用程式會每秒自動更新遊戲頁面一次。瀏覽器 JavaScript 中的 每秒呼叫 Python Web 伺服器,以查詢遊戲資料表中的遊戲項目是否已變更。如果有, JavaScript 會觸發頁面重新整理,讓使用者看到更新的討論板。
讓我們了解應用程式的詳細運作方式。
首頁
在使用者登入後,應用程式會顯示下列三項資訊清單。
-
Invitations (邀請):此清單會顯示最多 10 則最近其他使用者發送並正等待登入使用者接受的邀請。在先前的螢幕擷取畫面中,user1 有來自 user5 和 user2 的待定邀請。
-
Games In-Progress (正在進行的遊戲) - 此清單會顯示最多 10 個最近正在進行的遊戲。這些遊戲是使用者正在玩的遊戲,其狀態為
IN_PROGRESS
。在螢幕擷取畫面中,User1 會主動使用 user3 和 user4 tic-tac-toe玩遊戲。 -
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)
應用程式 Tic-Tac-Toe主要定義 Game
類別,以存放從 DynamoDB 擷取的遊戲資料。這些函數會傳回 Game
物件的清單,讓您可將應用程式剩餘的部分與有關 Amazon DynamoDB 項目的程式碼隔離。因此,這些函數可協助您將應用程式程式碼和資料存放層的詳細資訊分離。
此處所述的架構模式也稱為 model-view-controller(MVC) UI 模式。在此情況下,Game
物件執行個體 (代表資料) 是模型,而HTML頁面是檢視。控制器則分為兩個檔案。application.py
檔案具有 Flask 框架的控制器邏輯,商業邏輯則隔離在 gameController.py
檔案中。也就是說,應用程式會將與 DynamoDB 相關的所有內容儲存在dynamodb
資料夾中SDK的個別檔案中。
讓我們檢閱三個函數,及其如何使用全域次要索引查詢 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 個最近的遊戲。
使用 getGamesWith狀態取得具有特定狀態的遊戲清單
對手接受遊戲邀請後,遊戲狀態會變更為 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)
-
函數接著會合併兩個清單、排序,然後為前 0 到 10 個項目建立
Game
物件的清單,並將清單傳回呼叫函數 (即 index)。games = self.mergeQueries(hostGamesInProgress, oppGamesInProgress) return games
遊戲頁面
遊戲頁面是使用者玩 tic-tac-toe遊戲的地方。它會顯示遊戲方格和有關遊戲的資訊。下列螢幕擷取畫面顯示正在進行的範例遊戲:
應用程式會在下列情況中顯示遊戲頁面:
-
使用者建立遊戲,並邀請另一名使用者玩遊戲。
在此案例中,頁面會顯示使用者為發起人,且遊戲狀態為
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 } } }
-
條件必須為 true 才能更新:
-
遊戲必須正在進行。即
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,如下列範例所示。
redirect("/game="+gameId)
此呼叫會使瀏覽器重新整理。在此重新整理中,應用程式會檢查遊戲是否以獲勝或平手結束。若遊戲已結束,則應用程式會根據結果更新遊戲項目。