Mobile Hub の NoSQL Database (DynamoDB)を使ってみます。
(Mobile Hubってなに? という人はまずこちらをご覧ください。)
設定
Mobile Hub の Configure から、NoSQL Database を選択します。
サンプルのスキーマがいくつか用意されてますので、その中から Notes
を選択します。
簡単な DynamoDB の説明
DynamoDBは、NoSQLデータベースです。MongoDBのようにJSONオブジェクトを格納します。
リレーショナルDB的に説明すると、
RDBのレコード
Item 項目 (アマゾンの訳では、
項目
です。ままアイテムでよくない?)
RDBのカラム
Attribute 属性
RDBのプライマリキー
Partition Key + Sort Key (コンポジットキー)
DynamoDB では、Partition
と Sort
キーを使用して各アイテムを物理ストレージにうまい具合に保持します。
このキーに対してインデックスを作成しクエリーに使用します。
Partitionキーは、ハッシュ値で、どの物理ストレージにアイテムを格納するか決まります。
Sortキーを使用してアイテムをソートして格納します。
サンプルアプリでは、PartitionキーにuserId
を指定してます。ここには、ログインユーザの Cognito IdentityID が入ります。
そして、SortキーにnoteId
を指定してます。
インデックス Secondary Index Queries
プライマリの partition
& sort
キーとは別に、サブの partition
& sort
のインデックスをいくつも作成できます。
クエリーを実行するときは、スクショにあるように、インデックスを使用する必要があります。
そうでなければ、全アイテムをガサッと取得し、Swift側でフィルタリング等の処理することになります。
サンプルアプリの説明
基本的には、2つのTableViewControllerで構成されます。左側の画面で、どんなクエリーを投げるか選択します。タップすると、DynamoDBにクエリを投げ、検索結果を右側の画面にリストで表示します。
スクショの例では、Partition Keyが指定の値(userId, Cognito IdentityID)にマッチするアイテムをすべて取得します。
(注: 実行前に、左画面下のInsert Sample Data
をタップして、アイテムをインサートしておきます。)
ざっくり全体像
なんちゃってクラス図で描くとこんなイメージです。
左側のTableView NoSQLTableViewController
上で特定のセルをタップすると、didSelectRowAtIndexPath:
でDao NotesTable
のgetItemWithCompletionHandler:
を呼び出します。このファンクション内部でDynamoDBにアクセスし、結果データ Entity [Notes]
をコールバックで渡します。
prepareForSegue:
にて結果データをTableView NoSQLQueryResultViewController
にセットしてセグエイする。
といった流れになります。
NotesTable
がCRUD処理を実装しています。Mobile Hubが、DaoとEntityを、よしなに生成してくれてますので、参考にしてね という感じでしょうか?
コードの説明
NoSQLTableViewController
タップ時に呼ばれる tableView:didSelectRowAtIndexPath:
の処理です。
何行目のセルがタップされたかみて、それに応じたクエリーをDynamoDBに投げます。
一番上のセルをタップすると、getItemWithCompletionHandler:
を呼びます。
これは、プライマリキー検索で、指定した partition & sortキーにマッチするアイテムを一つ返します。
(サンプルではキーはハードコードしてあり、UIから指定はできません。面倒ですし。)
検索結果をブロックで処理し、セグエイを呼びます。
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let showQueryResultSeque = "NoSQLShowQueryResultSegue"
let cell = tableView.cellForRowAtIndexPath(indexPath) as! NoSQLTableCell
activityIndicator.startAnimating()
// Get item
if indexPath.section == 0 {
table?.getItemWithCompletionHandler?({(response: AWSDynamoDBObjectModel?, error: NSError?) -> Void in
self.activityIndicator.stopAnimating()
if let error = error {
self.showAlertWithTitle("Error", message: "Failed to load an item. \(error.localizedDescription)")
}
else if let response = response {
self.performSegueWithIdentifier(showQueryResultSeque, sender: [response])
}
遷移先の画面 NoSQLQueryResultViewController
の、results : [AWSDynamoDBObjectModel]?
に、検索結果のリストをセットします。
結果が複数アイテムの場合は、AWSDynamoDBPaginatedOutputを返します。一つの場合は、AWSDynamoDBObjectModel
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let indexPath = tableView.indexPathForSelectedRow
let cell = tableView.cellForRowAtIndexPath(indexPath!) as! NoSQLTableCell
if let queryResultViewController = segue.destinationViewController as? NoSQLQueryResultViewController {
queryResultViewController.queryType = cell.queryTypeLabel.text!
queryResultViewController.queryDescription = cell.queryDescriptionLabel.text!
queryResultViewController.table = self.table
if let sender = sender as? AWSDynamoDBPaginatedOutput {
let paginatedOutput: AWSDynamoDBPaginatedOutput = sender
queryResultViewController.results = paginatedOutput.items
queryResultViewController.paginatedOutput = paginatedOutput
}
else {
queryResultViewController.results = (sender as? [AWSDynamoDBObjectModel])
}
}
}
NotesTable
NotesTable
クラスは、作成したDynamoDB.Notes
テーブルへの Daoクラスになっています。
getItemWithCompletionHandler:completionHandler:
で、objectMapper.loadを呼び、DynamoDBにクエリを投げます。
hashKey(partitionキー)とrangeKey(sortキー)に、ログインユーザのCognito IdentityIDと、noteId(ハードコード)をセットします。
検索結果をブロックで処理します。
func getItemWithCompletionHandler(completionHandler: (response: AWSDynamoDBObjectModel?, error: NSError?) -> Void) {
let objectMapper = AWSDynamoDBObjectMapper.defaultDynamoDBObjectMapper()
objectMapper.load(Notes.self, hashKey: AWSIdentityManager.defaultIdentityManager().identityId!, rangeKey: "demo-noteId-500000", completionHandler: {(response: AWSDynamoDBObjectModel?, error: NSError?) -> Void in
dispatch_async(dispatch_get_main_queue(), {
completionHandler(response: response, error: error)
})
})
}
ちなみにこちらのファンクションは、パーティションキーだけを指定して、複数アイテムを取得します。AWSDynamoDBPaginatedOutput
内部でobjectMapper.query
を呼んでます。
func queryWithPartitionKeyWithCompletionHandler(completionHandler: (response: AWSDynamoDBPaginatedOutput?, error: NSError?) -> Void) {
let objectMapper = AWSDynamoDBObjectMapper.defaultDynamoDBObjectMapper()
let queryExpression = AWSDynamoDBQueryExpression()
queryExpression.keyConditionExpression = "#userId = :userId"
queryExpression.expressionAttributeNames = ["#userId": "userId",]
queryExpression.expressionAttributeValues = [":userId": AWSIdentityManager.defaultIdentityManager().identityId!,]
objectMapper.query(Notes.self, expression: queryExpression, completionHandler: {(response: AWSDynamoDBPaginatedOutput?, error: NSError?) -> Void in
dispatch_async(dispatch_get_main_queue(), {
completionHandler(response: response, error: error)
})
})
}
まとめ
DynamoDBのスキーマ変更をする場合、Mobile Hubのコンソール画面から再度Dao、Entityをジェネレートする必要があります。
Teraformなどでプロビジョニングを自動化できれば良いですが、まだそのあたりが整備されてないようです。
また、Daoクラスが、汎用性のある作りではなく、あくまでサンプルコードですので、修正をした場合、Mobile Hubに上書きされてしまいます。
プロトコル拡張でもう少し汎用的な作りにできないかなとか感じました。