【初心者向け】DynamoDBを理解する〜Pythonを用いた取得パターン特化編〜

##DynamoDBとは

>Amazon DynamoDBは、フルマネージド型のNoSQLデータベースサービスで、
高速で予測可能なパフォーマンスとシームレスなスケーラビリティを特長としています。

大切なことは全部公式ドキュメントが言ってるので、
簡単にまとめると管理不要のNoSQLデータベースサービスのこと。

##そもそもNoSQLって?
>リレーショナルモデルの代わりに、キーと値のペアやドキュメントストレージなど、データ管理のための代替モデルを使用します。

RDB(リレーショナルデータベース)はデータを保持するテーブル間に関係性があり、
それぞれのトランザクションがデータの一貫性(ACID)を保つように設計されています。

具体的には物理的なレコードのロック(他のトランザクションからの取得・更新などを一時的に待機させる)やロールバック・コミットなどのトランザクション制御指示などによりACIDが実現されます。
しかし、実行速度や拡張性に難が有ります。

実行速度や拡張性に関わる要件が、需要を増して生まれたのがNoSQLです。

NoSQLには様々な要件ごとにソフトが開発され、それはRDBでできた機能を実現したりしなかったり、それまでになかった機能を実現したりと、
多岐にわたるバリエーションのソフトがあります。(当たり前ですが、全てのNoSQLサービスが同一の機能を有してません)
ユースケースとしては、堅実な一貫性を取るならRDB、実行速度や拡張性(フェーズによるデータ増加や、ACIDを犠牲にしたデータ保持)を重視するならNoSQLと考えれば良いでしょう。

##DynamoDBの特徴
キー・バリュー形式の単純なデータを表形式で保持し、高速で拡張性(スケールインなど)の高いDBです。
メインの構成は、データを保持するテーブル・テーブル内に定義された項目・項目に定義する属性でできており、RDBと似たテーブル構造になります。
※RDBの構造化データに対し、DynamoDBは半構造化データのテーブルを作成できます。
そのため、主キーやインデックス以外はテーブル作成時に定義不要です

しかし、検索に制限が多く、基本的には後述するプライマリキー(主キー)を使用した検索になると考えてください。

###DynamoDB用語解説
####「プライマリキー」
テーブルのレコードを一意に識別するもので、テーブル作成時に以下の2種類から選択する。

*・パーティションキー*
→1つのキーで主キーとなる場合に採用

*・パーティションキーとソートキー(別名：複合プライマリキー)*
→2つのキーで主キーとなる場合に採用
※ソートキーに指定した項目は範囲検索が可能になる。

####「セカンダリインデックス」
プライマリキー以外での検索に対応したい場合に設定する。
以下の2種類から選択する。

*・グローバルセカンダリーインデックス(GSI)*
→プライマリキーとは異なるキーで検索したい場合に採用
※設定したソートキーをパーティションキーにするようなことも可能

*・ローカルセカンダリインデックス(LSI)*
パーティションキーとLSI(任意の項目)の組み合わせで検索したい場合に採用
※複合プライマリキー設定時のみ設定可能
※テーブル作成時にのみ設定可能

##データ取得パターン
以下の3パターンが存在する

・getitem(主キー検索)
・query(複合プライマリキー設定時の範囲検索・セカンダリインデックスでの検索)
・scan(全レコード取得)

##Pythonからの検索

PythonからDynamoDBにアクセスする際は、AWS SDK for Python (Boto 3)を使用する。
以下のサンプルはlambdaを使用したサンプル例

###準備
```sample.py
#boto3のimport
import boto3

#lambdaのためのハンドラ(今回の解説対象外)
def lambda_handler(event, context):

    #boto3からDynamoDBアクセスのためのオブジェクト取得
    dynamodb = boto3.resource('dynamodb')

    #事前に作成したテーブル(今回は"sample1")へのアクセスオブジェクト取得
    table = dynamodb.Table('sample1')
```


###getitem(主キー検索)
複合プライマリキー設定時の例
ちなみに、ソートキーを検索パラメータに設定しなかった場合、エラーになる。

```sample.py
import boto3

def lambda_handler(event, context):

    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('sample1')

    #getItemメソッドの呼び出し(主キー検索)
    response = table.get_item(
        #パラメーターとして主キー情報(辞書型)を渡す
        #Keyという変数名?は固定(違う名前だとエラーになる)
        Key={
            #主キー情報を設定
            #今回はテーブルにid(プライマリーキー)・sex(ソートキー)を定義した
            'id': '001',
            'sex': 'man'
        }
    )
    #responseの正体は、Itemなどのキーが定義された辞書型オブジェクト
    print(response)

    #結果の取得
    item = response['Item']

    #辞書型オブジェクトとして取得できる(テーブルのカラムが定義されている)
    #キーに一致するものがない場合、エラーとなる
    print(item)
```

###query
```sample.py
import boto3
#Keyオブジェクトを使用できるようにする(これがないとエラーになる)
#謎エラーで詰まったら、importの有無を確認した方が良い
from boto3.dynamodb.conditions import Key

def lambda_handler(event, context):

    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('sample1')

    #複合プライマリキー設定時、プライマリーキーのみの検索
    #以下の記載でプライマリーキー以外を指定した場合はエラーとなる
    response = table.query(
        KeyConditionExpression=Key('id').eq('001')
    )
    #getItem同様、辞書型の情報が取得される
    print(response)

    #取得件数も取れる
    print(response['Count'])

    #取得レコードは"item"ではなく"items"!
    items = response['Items']

    #複数件取れるインターフェイスのためlistオブジェクトが格納されている
    #取得結果なしの場合は、0件のlistが取得される
    print(type(items))

    #その他検索パターン

    #ソートキーの範囲検索
    #条件が複数ある場合は、"&"or"|"を使用する
    response = table.query(
        KeyConditionExpression=Key('id').eq('001') & Key('sex').begins_with('m')
    )

    #LSIを使用した検索(基本的なやり方は複合プライマリキー時の検索と同じ)
    #LSIはでの検索は1件に絞れる保証がないため、getItemは使用不可
    response = table.query(
        #設定したLSIのインデックスNameを指定する
        IndexName='id-name-index',
        KeyConditionExpression=Key('id').eq('001') & Key('name').eq('mama')
    )

    #GSIを使用した検索(基本的なやり方はLSIの検索と同じ)
    response = table.query(
        #設定したGSIのインデックスNameを指定する
        IndexName='location-index',
        KeyConditionExpression=Key('location').eq('Tokyo')
    )
```

###Scan(全件検索)

```sample.py
import boto3

def lambda_handler(event, context):

    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('sample1')

    #シンプルにscanメソッドの呼び出しをする
    response = table.scan()

    #getItem同様、辞書型の情報が取得される
    print(response)

    #取得件数も取れる
    print(response['Count'])

    #取得レコードは"item"ではなく"items"!
    items = response['Items']

    #複数件取れるインターフェイスのためlistオブジェクトが格納されている
    #取得結果なしの場合は、0件のlistが取得される
    print(type(items))
```
また、scanでの全件取得結果をもとにフィルターをかけ、該当データのみ抽出することも可能です。
※キー設定でどうしようもなくなった場合の最後の手段としましょう。

##まとめ
テーブルのキー設定のバリエーション
・プライマリキー × GSI
・複合プライマリキー
・複合プライマリキー × LSI
・複合プライマリキー × GSI
・複合プライマリキー × LSI × GSI

それぞれのキー設定で行える検索パターン

| キー設定|scan|getItem |query|
|:-----------|:--:|:--:|:--|
| プライマリキー|○|○|×|
| 複合プライマリキー|○|○|○ (PK(必須)＋SK(任意・範囲指定化))|
| LSI |-|-|○(複合プライマリキーのquery検索パターンのソートキーを切り替えた検索)|
| GSI |-|-|○(任意の項目を用いた検索)|

上記を意識して、適切なキー設定を行えるとRDB脳からの切り替えがうまくいくと思います。