iOSからのDynamoDB使用方法について


AWS Mobile SDK for iOSを使ってDynamoDBを操作する場合の方法を公式ドキュメントを参考にまとめてみました。

# 前提条件
iOSでDynamoDBを使うために、以下の状態を満たす必要があります
* Amazon Mobile SDK for iOSを入れている (Ver2.0.17)
* Cognitoでの認証処理を実装済
* DynamoDBにテーブルが存在する
* DynamoDBのパーミッションの設定済

# 公式ドキュメント
[AWS Mobile SDK Developer Guides](http://docs.aws.amazon.com/mobile/sdkforios/developerguide/dynamodb_om.html "AWS Mobile SDK Developer Guides")

[SDKリファレンス](http://docs.aws.amazon.com/AWSiOSSDK/latest/)

# ポイント
1. DynamoDBのオブジェクトマッパークライアントを使って操作
2. テーブルマッピングクラスでテーブル情報や取得結果を管理


# DynamoDBのオブジェクトマッパークライアント

__『AWSDynamoDBObjectMapper』__クラスを使用すると簡単にDynamoDBを操作できます。
V1までは、低レベルクライアントクラス(AWSDynamoDB)を使って実装する必要がありましたが、
かなり面倒でした。
AWSDynamoDBObjectMapperクラスはその面倒な部分をやってくれています。

[リファレンスはこちら](http://docs.aws.amazon.com/AWSiOSSDK/latest/Classes/AWSDynamoDBObjectMapper.html)


生成方法

```
AWSDynamoDBObjectMapper *dynamoDBObjectMapper = [AWSDynamoDBObjectMapper defaultDynamoDBObjectMapper];
```

このクラスで以下の操作ができます

|操作|メソッド|
|:---|:----|
|GetItem|- (BFTask *)load:(Class)resultClass hashKey:(id)hashKey rangeKey:(id)rangeKey|
|Query|- (BFTask *)query:(Class)resultClass expression:(AWSDynamoDBQueryExpression *)expression|
|Scan|- (BFTask *)scan:(Class)resultClass expression:(AWSDynamoDBScanExpression *)expression|
|PutItem / UpdateItem|- (BFTask *)save:(AWSDynamoDBModel *)model|
|DeleteItem|- (BFTask *)remove:(AWSDynamoDBModel *)model|

逆に以下のことはできません。
低レベルAPI(クラスAWSDynamoDB)を使う必要があります。
※ Ver2.0.17時点

|操作|実装方法|
|:---|:----|
|アトミックカウンタ| AWSDynamoDBUpdateItemInputの __updateExpression__ でアトミックカウンタを指定 <br> AWSDynamoDBのupdateItemで更新実行 |
|条件つき更新| AWSDynamoDBUpdateItemInputの __conditionExpression__ で条件を指定し、<br> AWSDynamoDBのupdateItemで更新実行 |
|BatchGetItem / BatchWriteItem| AWSDynamoDBの __batchGetItem/batchWriteItem__ を使う |

# テーブルのモデルクラス

AWSDynamoDBObjectMapperを使ってDynamoDBを操作する場合、
AWSDynamoDBObjectModelを継承したモデルクラスが必要となります。

このモデルクラスの役割は以下です

* テーブルの定義情報を取得できるようにする
* アイテム取得時の結果オブジェクトになる
* アイテム削除や更新時の対象アイテムを指定する


### モデルクラスを作る時のポイント
* AWSDynamoDBObjectModelの継承
* 属性名のプロパティを用意 (※全く同じ名前である必要がある)
* AWSDynamoDBModelingプロトコルの実装(テーブル名、ハッシュキー、レンジキーを返すメソッドの実装)

Book.h

``` Objective-C
#import <Foundation/Foundation.h>
#import <AWSDynamoDB/AWSDynamoDB.h>

@interface Book : AWSDynamoDBObjectModel <AWSDynamoDBModeling>

@property (nonatomic, strong) NSString *Title;
@property (nonatomic, strong) NSString *Author;
@property (nonatomic, strong) NSNumber *Price;
@property (nonatomic, strong) NSString *ISBN;

@end
```

Book.m

``` Objective-C
#import <AWSDynamoDB/AWSDynamoDB.h>
#import "Book.h"

@implementation Book

+ (NSString *)dynamoDBTableName {
    return @"Books";
}

+ (NSString *)hashKeyAttribute {
    return @"ISBN";
}

@end
```

# 操作

## Load (GetItem)

結果オブジェクトがモデルクラス(Book)の状態で取得できるのがいい

``` Objective-C
[[dynamoDBObjectMapper load:[Book class] hashKey:@"6543210987" rangeKey:nil]
continueWithBlock:^id(BFTask *task) {
        if (task.error) {
                NSLog(@"The request failed. Error: [%@]", task.error);
        }
        if (task.exception) {
                NSLog(@"The request failed. Exception: [%@]", task.exception);
        }
        if (task.result) {
                Book *book = task.result;
                //Do something with the result.
        }
        return nil;
}];
```

## Query

AWSDynamoDBQueryExpressionを使ってキー条件を指定

``` Objective-C
// Queryの条件を指定
// ISBNが"3456789012"のアイテムを取得
AWSDynamoDBQueryExpression *queryExpression = [AWSDynamoDBQueryExpression new];
queryExpression.hashKeyValues = @"3456789012";

[[dynamoDBObjectMapper query:queryClass
                  expression:queryExpression]
 continueWithBlock:^id(BFTask *task) {
     if (task.error) {
         NSLog(@"The request failed. Error: [%@]", task.error);
     }
     if (task.exception) {
         NSLog(@"The request failed. Exception: [%@]", task.exception);
     }
     if (task.result) {
         // 取得するアイテムは複数の場合があるので、結果はAWSDynamoDBPaginatedOutputオブジェクト
         AWSDynamoDBPaginatedOutput *paginatedOutput = task.result;
         for (Book *book in paginatedOutput.items) {
             //Do something with book.
         }
     }
     return nil;
 }];
```

## Scan

AWSDynamoDBScanExpressionを使って、Scanの条件を指定

``` Objective-C
// Scanの条件を指定
AWSDynamoDBScanExpression *scanExpression = [AWSDynamoDBScanExpression new];
scanExpression.limit = @10;

[[dynamoDBObjectMapper scan:[Book class]
                 expression:scanExpression]
 continueWithBlock:^id(BFTask *task) {
     if (task.error) {
         NSLog(@"The request failed. Error: [%@]", task.error);
     }
     if (task.exception) {
         NSLog(@"The request failed. Exception: [%@]", task.exception);
     }
     if (task.result) {
         // 取得するアイテムは複数の場合があるので、結果はAWSDynamoDBPaginatedOutputオブジェクト
         AWSDynamoDBPaginatedOutput *paginatedOutput = task.result;
         for (Book *book in paginatedOutput.items) {
             //Do something with book.
         }
     }
     return nil;
 }];
```

## Save (PutItem / UpdateItem)

``` Objective-C
Book *myBook = [Book new];
myBook.ISBN = @"3456789012";
myBook.Title = @"The Scarlet Letter";
myBook.Author = @"Nathaniel Hawthorne";
myBook.Price = [NSNumber numberWithInt:899];
myBook.ViewCount = [NSNumber numberWithInt:0]; // 初期値を0でセット

[[dynamoDBObjectMapper save:myBook]
 continueWithBlock:^id(BFTask *task) {
     if (task.error) {
         NSLog(@"The request failed. Error: [%@]", task.error);
     }
     if (task.exception) {
         NSLog(@"The request failed. Exception: [%@]", task.exception);
     }
     if (task.result) {
         //Do something with the result.
     }
     return nil;
 }];
```

### オプション

保存方法を指定できる

|オプション名|説明|
|:----|:----|
|AWSDynamoDBObjectMapperSaveBehaviorUpdate(デフォルト)| UpdateItem (Action:Put/Delete) <br> ※ 属性と値を追加し、既存なら上書きする(Put) <br> ※ 主キーの値がNSNullの場合、削除(Delete)|
|AWSDynamoDBObjectMapperSaveBehaviorUpdateSkipNullAttributes| UpdateItem (Action:Put) <br> ※ 主キーの値がNSNullの場合、何もしない |
|AWSDynamoDBObjectMapperSaveBehaviorAppendSet| UpdateItem (Action:Add/Put) <br> ※ 値がSet型の場合のみ要素を後ろに追加(Add) <br> ※ それ以外は上書き(Put) <br> ※ 本来は、Number型もAddの対象だが、このメソッドではできない <br> ※ 主キーの値がNSNullの場合、何もしない |
|AWSDynamoDBObjectMapperSaveBehaviorClobber| PutItem <br> ※ アイテム(行)追加。既存のアイテムがあれば、そのアイテムの全属性が上書きされる|

### オプション指定方法

AWSDynamoDBObjectMapperConfigurationのsaveBehaviorに以下のいずれかを指定する
AWSDynamoDBObjectMapperConfigurationは、引数で渡すか、AWSDynamoDBObjectMapperのconfigurationプロパティに予め指定しておく

``` Objective-C
AWSDynamoDBObjectMapperConfiguration *updateMapperConfig = [AWSDynamoDBObjectMapperConfiguration new];
updateMapperConfig.saveBehavior = AWSDynamoDBObjectMapperSaveBehaviorUpdate_Skip_Null_Attributes;
// Update_Skip_Null_Attributes
```

## Remove (DeleteItem)

``` Objective-C
Book *bookToDelete = [Book new];
bookToDelete.ISBN = @"4456789012";

[[dynamoDBObjectMapper remove:bookToDelete]
 continueWithBlock:^id(BFTask *task) {

     if (task.error) {
         NSLog(@"The request failed. Error: [%@]", task.error);
     }
     if (task.exception) {
         NSLog(@"The request failed. Exception: [%@]", task.exception);
     }
     if (task.result) {
         //Item deleted.
     }
     return nil;
 }];
```

## アトミックカウンタ (インクリメント)
UpdateItemの更新文(UpdateExpression)を使って行います。
以下の例では、ViewCountに+1しています。
#### 注意
アトミックカウントをするとき、属性がない場合、エラーとなりました。
ない場合は追加してくれると思っていましたが、そうはならず。。。
初期値を0で属性は用意しておく必要がありそうです。

####公式ドキュメントによる条件つきアトミックカウンタの説明
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.AtomicCounters

``` Objective-C
// ViewCountに+1する (アトミックカウンタ)
AWSDynamoDB *dynamoDB = [AWSDynamoDB defaultDynamoDB];
AWSDynamoDBUpdateItemInput *updateItemInput = [AWSDynamoDBUpdateItemInput new];

// テーブル名
updateItemInput.tableName = [Book dynamoDBTableName];

// ハッシュキーセット
NSString *hashKeyAttribute = [Book hashKeyAttribute];
AWSDynamoDBAttributeValue *hashKeyValue = [AWSDynamoDBAttributeValue new];
hashKeyValue.S = @"3456789012";
updateItemInput.key = @{hashKeyAttribute : hashKeyValue};

// 更新文
updateItemInput.updateExpression = @"set ViewCount = ViewCount + :cnt";
// 更新文で使う属性値のセット
AWSDynamoDBAttributeValue *expressionAttributeValue = [AWSDynamoDBAttributeValue new];
expressionAttributeValue.N = @"1";
updateItemInput.expressionAttributeValues = @{@":cnt" : expressionAttributeValue};
updateItemInput.returnValues = AWSDynamoDBReturnValueUpdatedNew;

// 更新実行
[[dynamoDB updateItem:updateItemInput]
 continueWithBlock:^id(BFTask *task) {
     if (task.error) {
         NSLog(@"The request failed. Error: [%@]", task.error);
     }
     if (task.exception) {
         NSLog(@"The request failed. Exception: [%@]", task.exception);
     }
     if (task.result) {
         //Do something with the result.
     }
     return nil;
}];
```

## 条件つき更新

UpdateItemの条件文(ConditionExpression)を使って行います。
以下の例では、ViewCountが5より大きい場合、Level属性に1をセットしています。
####注意
条件式にマッチしない場合、エラーとなるようです。

####公式ドキュメントによる条件つき更新の説明
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.AtomicCounters
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Expressions.Modifying.html


``` Objective-C
// ViewCountに+1する (アトミックカウンタ)
AWSDynamoDB *dynamoDB = [AWSDynamoDB defaultDynamoDB];
AWSDynamoDBUpdateItemInput *updateItemInput = [AWSDynamoDBUpdateItemInput new];

// テーブル名
updateItemInput.tableName = [Book dynamoDBTableName];

// ハッシュキーセット
NSString *hashKeyAttribute = [Book hashKeyAttribute];
AWSDynamoDBAttributeValue *hashKeyValue = [AWSDynamoDBAttributeValue new];
hashKeyValue.S = @"3456789012";
updateItemInput.key = @{hashKeyAttribute : hashKeyValue};

// 更新文
updateItemInput.updateExpression = @"set #lvl = :lvl";

// 条件文
updateItemInput.conditionExpression = @"ViewCount > :cnt_level";

// 更新文で使う属性名のセット
updateItemInput.expressionAttributeNames = @{@"#lvl": @"Level"};

// 更新文と条件文で使う属性値のセット
NSMutableDictionary *expAttributeValues = [[NSMutableDictionary alloc] init];
// :lvlのセット
AWSDynamoDBAttributeValue *updateExpAttributeValue = [AWSDynamoDBAttributeValue new];
updateExpAttributeValue.N = @"1";
expAttributeValues[@":lvl"] = updateExpAttributeValue;
// :cnt_levelのセット
AWSDynamoDBAttributeValue *conditionExpAttributeValue = [AWSDynamoDBAttributeValue new];
conditionExpAttributeValue.N = @"5";
expAttributeValues[@":cnt_level"] = conditionExpAttributeValue;

updateItemInput.expressionAttributeValues = expAttributeValues;

// 更新実行
[[dynamoDB updateItem:updateItemInput]
 continueWithBlock:^id(BFTask *task) {
     if (task.error) {
     
         // 条件文にマッチしない場合、エラーとなる
     	　if (task.error.code == AWSDynamoDBErrorConditionalCheckFailed) {
             DEBUG_LOG(@"%@", @"条件式にマッチせず");
         }
     
         NSLog(@"The request failed. Error: [%@]", task.error);
     }
     if (task.exception) {
         NSLog(@"The request failed. Exception: [%@]", task.exception);
     }
     if (task.result) {
         //Do something with the result.
     }
     return nil;
}];
```
## BatchGetItem / BatchWriteItem

``` Objective-C
AWSDynamoDB *dynamoDB = [AWSDynamoDB defaultDynamoDB];

//Write Request 1
AWSDynamoDBAttributeValue *hashValue1 = [AWSDynamoDBAttributeValue new];
hashValue1.S = @"3210987654";
AWSDynamoDBAttributeValue *otherValue1 = [AWSDynamoDBAttributeValue new];
otherValue1.S = @"Some Title";

AWSDynamoDBWriteRequest *writeRequest = [AWSDynamoDBWriteRequest new];
writeRequest.putRequest = [AWSDynamoDBPutRequest new];
writeRequest.putRequest.item = @{
                                 @"ISBN" : hashValue1,
                                 @"Title" : otherValue1
                                 };

//Write Request 2
AWSDynamoDBAttributeValue *hashValue2 = [AWSDynamoDBAttributeValue new];
hashValue2.S = @"8901234567";
AWSDynamoDBAttributeValue *otherValue2 = [AWSDynamoDBAttributeValue new];
otherValue2.S = @"Another Title";

AWSDynamoDBWriteRequest *writeRequest2 = [AWSDynamoDBWriteRequest new];
writeRequest2.putRequest = [AWSDynamoDBPutRequest new];
writeRequest2.putRequest.item = @{
                                  @"ISBN" : hashValue2,
                                  @"Title" : otherValue2
                                  };

AWSDynamoDBBatchWriteItemInput *batchWriteItemInput = [AWSDynamoDBBatchWriteItemInput new];
batchWriteItemInput.requestItems = @{@"Books": @[writeRequest,writeRequest2]};

[[dynamoDB batchWriteItem:batchWriteItemInput]
 continueWithBlock:^id(BFTask *task) {
     if (task.error) {
         NSLog(@"The request failed. Error: [%@]", task.error);
     }
     if (task.exception) {
         NSLog(@"The request failed. Exception: [%@]", task.exception);
     }
     if (task.result) {
         //Do something with the result.
     }
     return nil;
 }];
```