[AWS.5] DynamoDB プログラミング概要

# 1. 概要
AWS SDKは、アプリケーションから DynamoDBへアクセスするためのインタフェースを三つ用意しています。
* 低レベルインタフェース
* ドキュメントインタフェース
* 高レベルインタフェース
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90456/ac3cdee4-3fb5-7007-cb39-d119866478e9.png" />

プログラミング言語によって利用できるインタフェースは異なりますが、.NET では３種すべてを利用可能です。

:::note info
AWS DynamoDB プログラミングのオンラインマニュアルの図では「高レベルインタフェース（High-Level Interface）」と記述されていますが、解説上は「オブジェクト永続性インタフェース」とされています。
:::
:::note info
AWS DynamoDB プログラミングのオンラインマニュアルでは、「ドキュメントインタフェース」と「オブジェクト永続性インタフェース」が .NET向けの「高レベルインタフェース」として紹介されています。
混乱してしまうため、以後 本記事では「高レベルインタフェース」という呼称は使用しないことにします。
:::

# 2. プログラミングインタフェースの種類
AWS のオンラインマニュアルでは、各インタフェースのメリット/デメリットは明確に記載されていません。（できること/できないこと、は記載されています）
ここでは一般的な見地から「恐らく、そうであろう」という事柄を記載しておきます。

:::note warn
<font color="red">本項の記載内容は**私見に基づく内容**です。
各インタフェースの仕様や挙動を保証するものではありません。
（列挙した点を覆す仕様が用意されている可能性もあります）</font>
実際に採用するインタフェースを検討する際は、各インタフェースの特性をマニュアルなどで十分に確認してください。
:::

#### (1) 低レベルインタフェース
DynamoDB へアクセスするための低レベルAPIに近い操作が可能で、テーブルの作成や削除などを実装する必要がある場合は、このインタフェースを採用する必要があります。

　参考： [テーブルの操作](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/LowLevelDotNetWorkingWithTables.html)
　　　　 [項目の操作](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/LowLevelDotNetItemCRUD.html)

* テーブル自体の操作（作成・削除など）が可能。
* 操作対象を特定の属性値（列値）だけに絞り込み、不要な（無駄な）データアクセスを回避することが可能。
* 永続化オブジェクトクラスは不要。
* 取り扱う全ての属性値（列値）の「データ型の変換処理」が必要。
* 取り扱う全ての属性値（列値）の「抽出・格納処理」が必要。

|機能・要件|可否・要否|
|:---|:----|
|テーブルの作成・削除|可|
|永続化オブジェクトクラスの用意|不要|
|実装コード量|多い (注１)|
|実装難易度|高|
|メモリ使用効率|普通 (注２)|

　注１）値の抽出・格納処理を属性値（列値）ごとに実装しなければならない。
　注２）複数件のデータを取り扱う場合、１件毎にデータ格納クラスを作成することがある。
　　　　その場合は一度に取り扱う件数分だけメモリ消費量が増大する。

#### (2) ドキュメントインタフェース
低レベルインタフェースの一部を取りまとめるドキュメントモデルクラスを提供します。
「Table」と「Document」というプライマリクラスがあり、各テーブルへのCRUD操作を簡潔に実装することができます。

　参考： [.NETドキュメントモデル](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DotNetSDKMidLevel.html)

* テーブル自体の操作（作成・削除など）は不可能。
* 操作対象を特定の属性値（列値）だけに絞り込み、不要な（無駄な）データアクセスを回避することが可能。
* 永続化オブジェクトクラスは不要。
* 取り扱う全ての属性値（列値）の「データ型の変換処理」が必要。
* 取り扱う全ての属性値（列値）の「抽出・格納処理」が必要。
（データ格納クラスとDocumentクラスの相互変換により省略できるケースあり）

|機能・要件|可否・要否|
|:---|:----|
|テーブルの作成・削除|不可|
|永続化オブジェクトクラスの用意|不要|
|実装コード量|普通 (注１)|
|実装難易度|中|
|メモリ使用効率|普通 (注２)|

　注１）値の抽出・格納処理を属性値（列値）ごとに実装しなければならないが、データ
　　　　格納クラスを介することで実装量を抑えられるケースもある。
　注２）複数件のデータを取り扱う場合、１件毎にデータ格納クラスを作成することがある。
　　　　その場合は一度に取り扱う件数分だけメモリ消費量が増大する。

#### (3) オブジェクト永続性インタフェース
DynamoDBテーブルへマッピング可能なオブジェクト永続性モデルによるインタフェースを提供します。
同オブジェクト内のプロパティが対応するテーブルの各項目にマッピングされるため、CRUD操作において個々の属性値（列値）への特別な操作（データ変換や代入操作など）は不要となります。

　参考： [.NETオブジェクト永続性モデル](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DotNetSDKHighLevel.html)

* テーブル自体の操作（作成・削除など）は不可能。
* 操作対象を特定の属性値（列値）だけに絞り込むことは不可能。
* 永続化オブジェクトクラスが必要。
* 取り扱う全ての属性値（列値）の「データ型の変換処理」は不要。
* 取り扱う全ての属性値（列値）の「抽出・格納処理」は不要。

|機能・要件|可否・要否|
|:---|:----|
|テーブルの作成・削除|不可|
|永続化オブジェクトクラスの用意|必要|
|実装コード量|少ない|
|実装難易度|低|
|メモリ使用効率|悪い (注１)|

　注１）操作対象のデータを全て永続化オブジェクトでメモリ上に保持する必要がある。
　　　　また、操作対象外の属性値（列値）も保持しておかねばならず、一度に取り扱う
　　　　件数分だけ、メモリ消費量が肥大化する恐れがある。


# 3. .NET でのプログラミング　
特徴のある３種のインタフェースですが、どれを採用するかは利用用途に依存します。
メリット・デメリットを理解した上で適切なものを選択すべきですが、「お勉強」という観点では 他のインタフェースに通じるところのある「ドキュメントインタフェース」から着手しても良いかもしれません。

以下、クエリ操作コード のサンプルを記述します。
とても単純なサンプルなので知識を深めることはできませんが、何かの役には立つかもしれません。

## サンプルの説明
一つのテーブルから条件に合致するデータ１件を特定し、その中の属性値（列値）を取り出す Lambda関数を作成する。

* ３種のインタフェースを Lambda関数の引数で切り替える。（"LOW", "DOC", OBJ"）
* パーティションキーとソートキーの両方を指定して（固定値）データを特定する。
* データの中から取り出した属性値（列値）を Lambda関数の返却値とする。
* 3種のインタフェースによるクエリ処理は個々のクラスに実装し、Lambda 関数から共通のインタフェースメソッドを介して呼び出す。
* クエリ処理からの返却値は専用のデータクラスへ格納し、Lambda 関数内では共通のプロパティインタフェースで格納値を取り出す。

#### テーブル定義
テーブル名：　**Sample1**
|属性名|データ型|補足|
|:----:|:----:|:----|
|DATA|文字列|パーティションキー|
|TIME|文字列|ソートキー|
|ACTUAL|数値||
|PREDICT|数値||
|SUPPLY|数値||
|USE-RATE|数値||

**テストデータ**
|属性名|１件目|２件目|３件目|
|:----:|:----:|:----:|:----:|
|DATA|2022/7/5|2022/7/5|2022/7/5|
|TIME|0:00|1:00|2:00|
|ACTUAL|269|265|276|
|PREDICT|266|262|275|
|SUPPLY|90|78|91|
|USE-RATE|297|340|340|


#### 共通のインタフェース定義
```C#
    public interface IDynDbInterface
    {
        /// <summary>
        /// 指定条件に合致する要素を取得する。
        /// </summary>
        /// <param name="partitionKey">パーティションキー</param>
        /// <param name="sortKey">ソートキー</param>
        /// <returns>条件に合致する要素データ</returns>
        ISampleTableInterface? GetItem(string partitionKey, string sortKey);
    }

    /// <summary>
    /// サンプルテーブル要素へアクセスするためのインタフェース。
    /// </summary>
    public interface ISampleTableInterface
    {
        int Actual { get; set; }
    }
```

#### Lambda 関数の実装
```C#
public class Function
{
    private AmazonDynamoDBClient dBClient;
    private IDynDbInterface? dynDb = null;
    private ObjIfDb? objIf = null;
    private DocIfDb? docIf = null;
    private LowIfDb? lowIf = null;

    public Function()
    {
        this.dBClient = new AmazonDynamoDBClient();
    }

    public string FunctionHandler(string input, ILambdaContext context)
    {
        string actual;

        switch(input)
        {
            case "OBJ":
                this.objIf ??= new ObjIfDb(this.dBClient);
                this.dynDb = this.objIf;
                break;

            case "DOC":
                this.docIf ??= new DocIfDb(this.dBClient);
                this.dynDb = this.docIf;
                break;

            case "LOW":
                this.lowIf ??= new LowIfDb(this.dBClient);
                this.dynDb = this.lowIf;
                break;

            default:
                actual = $"未サポートのパラメータ {input} が指定された。";
                return actual;
        }

        ISampleTableInterface? item2 = this.dynDb.GetItem("2022/7/5", "1:00");
        actual = item2.Actual.ToString();

        return actual;
    }
}
```

## 低レベルインタフェース

* データを特定するキー情報を ```GetItemRequest.Key``` に指定する。
（パーティションキーとソートキーを「データ型」と「値」の組み合わせで指定）
* 抽出する属性（列）を ```GetItemRequest.ProjectionExpression``` で特定する。
* データ格納クラスへ取得値を格納する際は、```GetItemResponse.Item``` から取り出した AttributeValue（文字列データ）を数値型へ変換する。

```C#
    public class SampleItem3 : ISampleTableInterface
    {
        public int Actual { get; set; }
    }

    public class LowIfDb : IDynDbInterface
    {
        private AmazonDynamoDBClient dBClient;
        private DynamoDBContext dBContext;

        public string TableName { get; set; }

        public LowIfDb(AmazonDynamoDBClient client)
        {
            this.dBClient = client;
            this.dBContext = new DynamoDBContext(dBClient);
            this.TableName = "Sample1";
        }

        /// <summary>
        /// 指定条件に合致する要素を取得する。
        /// </summary>
        /// <param name="partitionKey">パーティションキー</param>
        /// <param name="sortKey">ソートキー</param>
        /// <returns>取得した要素データ</returns>
        public ISampleTableInterface? GetItem(string partitionKey, string sortKey)
        {
            var request = new GetItemRequest()
            {
                TableName = this.TableName,

                // 項目検索キーを指定する。
                //  　　データ型(S)と検索値を明示する形で指定する。
                Key = new Dictionary<string, AttributeValue>()
                {
                    { "DATE", new AttributeValue { S = partitionKey } },
                    { "TIME", new AttributeValue { S = sortKey} }
                },

                // 指定した属性名の値のみを取得する。
                // （省略時は全属性名の値を取得）
                ProjectionExpression = "ACTUAL",

                // 強力な整合性のある読み込み を実施する。
                // （以前に成功したすべての書き込み結果-最新のデータ-を返却する）
                ConsistentRead = true
            };

            // 指定条件に合致する項目データを取得する。
            Task<GetItemResponse> task;
            task = this.dBClient.GetItemAsync(request);
            task.Wait();
            GetItemResponse response = task.Result;

            // 取得値を返却用オブジェクトに格納する。
            SampleItem3 item = new SampleItem3()
            {
                // 数値型の属性値を文字列として取得し、int型に
                // 変換してから格納。
                //  　AttributeValue から「.N」で数値を取り出すが、
                // 　　　取り出した際のデータ型は 文字列型。
                // 　　　よって、int型への形変換が必要となる。
                Actual = int.Parse(response.Item["ACTUAL"].N)
            };

            return item;
        }
    }
```

## ドキュメントインタフェース

* データを特定するキー情報を ```Table.GetItemAsync``` の引数として指定する。
* 抽出する属性（列）を ```GetItemOperationConfig.AttributesToGet``` で特定する。
* クエリ結果（```Table.GetItemAsync```の返却値）である ```Document``` クラスオブジェクトは、 ```DynamoDBContext.FromDocument``` メソッドを呼び出してデータ格納オブジェクトへ変換する。

```C#
    public class SampleItem2 : ISampleTableInterface
    {
        [DynamoDBProperty("ACTUAL")]
        public int Actual { get; set; }
    }

    public class DocIfDb : IDynDbInterface
    {
        private AmazonDynamoDBClient dBClient;
        private DynamoDBContext dBContext;

        public string TableName { get; set; } = string.Empty;

        public DocIfDb(AmazonDynamoDBClient client)
        {
            this.dBClient = client;
            this.dBContext = new DynamoDBContext(dBClient);
            this.TableName = "Sample1";
        }

        /// <summary>
        /// 指定条件に合致する要素を取得する。
        /// </summary>
        /// <param name="partitionKey">パーティションキー</param>
        /// <param name="sortKey">ソートキー</param>
        /// <returns>取得した要素データ</returns>
        public ISampleTableInterface? GetItem(string partitionKey, string sortKey)
        {
            // テーブル操作オブジェクトの作成
            Table table = Table.LoadTable(this.dBClient, this.TableName);

            GetItemOperationConfig config = new GetItemOperationConfig
            {
                // 指定した属性名の値のみを取得する。
                // （省略時は全属性名の値を取得）
                AttributesToGet = new List<string> { "ACTUAL" },

                // 強力な整合性のある読み込み を実施する。
                // （以前に成功したすべての書き込み結果-最新のデータ-を返却する）
                ConsistentRead = true
            };

            // 指定条件に合致する項目データを取得する。
            Task<Document> task = table.GetItemAsync(partitionKey, sortKey, config);
            task.Wait();
            Document doc = task.Result;

            // Document型オブジェクトを、実際の要素データクラス型に変換する。
            //  　　クラスメンバへの代入 および データ型の変換処理を
            // 　　　　明示的に記述する必要はない。
            SampleItem2 item;
            item = dBContext.FromDocument<SampleItem2>(doc);

            return item;
        }
    }
```

## オブジェクト永続性インタフェース

* データを特定するキー情報を ```DynamoDBContext.LoadAsync``` の引数として指定する。
* クエリ結果が データ格納オブジェクト そのものなので、そのまま返却する。

```C#
    [DynamoDBTable("Sample1")]
    public class SampleItem : ISampleTableInterface
    {
        [DynamoDBHashKey]
        [DynamoDBProperty("DATE")]
        public string? Date { get; set; }

        [DynamoDBRangeKey]
        [DynamoDBProperty("TIME")]
        public string? Time { get; set; }

        [DynamoDBProperty("ACTUAL")]
        public int Actual { get; set; }

        [DynamoDBProperty("PREDICT")]
        public int PredictValue { get; set; }

        [DynamoDBProperty("SUPPLY")]
        public int SupplyValue { get; set; }

        [DynamoDBProperty("USE-RATE")]
        public int UseRateValue { get; set; }
    }

    public class ObjIfDb : IDynDbInterface
    {
        private AmazonDynamoDBClient dBClient;
        private DynamoDBContext dBContext;

        public ObjIfDb(AmazonDynamoDBClient client)
        {
            this.dBClient = client;
            this.dBContext = new DynamoDBContext(dBClient);
        }

        /// <summary>
        /// 指定条件に合致する要素を取得する。
        /// </summary>
        /// <param name="partitionKey">パーティションキー</param>
        /// <param name="sortKey">ソートキー</param>
        /// <returns>取得した要素データ</returns>
        public ISampleTableInterface? GetItem(string partitionKey, string sortKey)
        {
            DynamoDBOperationConfig config = new DynamoDBOperationConfig
            {
                // 特定の属性「だけを」抽出するように指定することはできない。

                // 強力な整合性のある読み込み を実施する。
                // （以前に成功したすべての書き込み結果-最新のデータ-を返却する）
                ConsistentRead = true
            };

            // 要素クラス「SampleItem」の「DynamoDBTable」属性に指定した
            // テーブルから、キー値に合致する要素データを取り出す。
            Task<SampleItem> task;
            task = this.dBContext.LoadAsync<SampleItem>(partitionKey, sortKey, config);
            task.Wait();

            // 取得結果自体がすでに実際の要素データ型なので、
            // そのまま返却するだけで良い。
            return task.Result;
        }
    }
```

# 4. 終わりに
３種類のインタフェースはどれも一長一短ありで、「とりあえず、これを選んどけば良い」と結論づけることは困難です。
その時々に応じて、適切な「モノ」を採用することが求められるでしょう。

---

ところで、DynamoDB には「ローカル版」というものが存在し、開発PC上で動かしてアクセスすることが可能なようです。
サンプルのような小規模プログラムならともかく、ある程度の規模のプログラムを開発しようとするときは、ローカル環境でデータベースが稼働していた方が何かと便利かもしれません。

次回は、この 「DynamoDB ローカル版」について確認してみようと思います。