AWS MobileHub Swift 版 のチュートリアル -NoSQL Database (DynamoDB)編-


Mobile Hub の NoSQL Database (DynamoDB)を使ってみます。
(Mobile Hubってなに？ という人はまず[こちら](http://qiita.com/tfutada/items/9428eef0ea3b67922f3d)をご覧ください。)

## 設定

Mobile Hub の Configure から、NoSQL Database を選択します。
サンプルのスキーマがいくつか用意されてますので、その中から `Notes` を選択します。

![Screen Shot 2016-06-08 at 9.19.44 AM.png](https://qiita-image-store.s3.amazonaws.com/0/75231/a3fb4e04-67ac-5204-2599-c615e9da9f80.png)

## 簡単な 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側でフィルタリング等の処理することになります。

![Screen Shot 2016-06-08 at 5.41.35 PM.png](https://qiita-image-store.s3.amazonaws.com/0/75231/ba6e1372-af0b-2781-7615-d94cc0e0c002.png)


## サンプルアプリの説明

基本的には、２つのTableViewControllerで構成されます。左側の画面で、どんなクエリーを投げるか選択します。タップすると、DynamoDBにクエリを投げ、検索結果を右側の画面にリストで表示します。
スクショの例では、Partition Keyが指定の値(userId, Cognito IdentityID)にマッチするアイテムをすべて取得します。
(注: 実行前に、左画面下の`Insert Sample Data`をタップして、アイテムをインサートしておきます。)
![Screen Shot 2016-06-09 at 8.07.03 AM.png](https://qiita-image-store.s3.amazonaws.com/0/75231/fe5c05a6-2b9b-8e24-075a-d9672d1ef730.png)

## ざっくり全体像

なんちゃってクラス図で描くとこんなイメージです。 :sweat_drops: 
左側の`TableView NoSQLTableViewController`上で特定のセルをタップすると、`didSelectRowAtIndexPath:`で`Dao NotesTable`の`getItemWithCompletionHandler:`を呼び出します。このファンクション内部でDynamoDBにアクセスし、結果データ `Entity [Notes]`をコールバックで渡します。
`prepareForSegue:`にて結果データを`TableView NoSQLQueryResultViewController`にセットしてセグエイする。
といった流れになります。
`NotesTable`がCRUD処理を実装しています。Mobile Hubが、DaoとEntityを、よしなに生成してくれてますので、参考にしてね :grinning: という感じでしょうか？

![Screen Shot 2016-06-09 at 7.59.51 AM.png](https://qiita-image-store.s3.amazonaws.com/0/75231/4959e625-f3ff-7eaa-ade3-42fd3e8a01cf.png)

## コードの説明

### NoSQLTableViewController

タップ時に呼ばれる `tableView:didSelectRowAtIndexPath:`の処理です。
何行目のセルがタップされたかみて、それに応じたクエリーをDynamoDBに投げます。
一番上のセルをタップすると、`getItemWithCompletionHandler:`を呼びます。
これは、プライマリキー検索で、指定した partition & sortキーにマッチするアイテムを一つ返します。
(サンプルではキーはハードコードしてあり、UIから指定はできません。面倒ですし。)
検索結果をブロックで処理し、セグエイを呼びます。

```swift:NoSQLTableViewController.swift
    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

```swift:NoSQLTableViewController.swift
    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(ハードコード)をセットします。
検索結果をブロックで処理します。

```swift:NotesTable.swift
    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に上書きされてしまいます。
プロトコル拡張でもう少し汎用的な作りにできないかなとか感じました。