[TypeScript]AWS DynamoDBを初心者でも簡単に使うためのライブラリを作ってみた

## はじめに
普段RDSを主に利用する私が、初めてDynamoDBを使ってwebサービス開発をする時に、DynamoDB特有のクセのあるクエリ組み立てに苦労したので、自分と同じような境遇の人がもっと簡単に使うための設計を考えてみました。
今回はAWS Lambda + API Gatewayで簡単なバックエンドのAPIを作り、その上でDynamoDBとやり取りをする形を取りました。
## この記事の対象者
・TypeScriptでDynamoDBを使ってみたい方
・DynamoDBに接続する実装を楽にしたい方
・AWS APIGateway + Lambda + DynamoDBの構成を触ってみたい方

AWS API GatewayやLambdaは実際のAWS上に作る前提になっていますが、DynamoDBに対するクエリを簡単に使えることを体験して欲しいので環境構築はTypeScriptを用いたIaCを採用していて、コマンド1つで開発環境が整います。

## なぜ考えることになったのか
私が初めてDynamoDBを利用し始めた時は、開発中に次のようなつらみと出会いました。

### 動かせるようになるまで苦労する
いきなりDynamoDBを扱うにしても、どこから学習すれば良いかわからない上、思った通りに動かせるようになるまでに時間がかかる。
きちんと動いている状態を見ると理解が早まることは多いですが、そもそもそこまで到達することが大変。


### 実装の共通化がやりにくい
単純に利用する場合、TypeScriptのDynamoDB用のSDKを利用することになると思います。
このSDKは自由度が高く、実装者によって実装の仕方に統一感がうまく出せないことがあります。また、クエリの組み立てをよしなにいい感じにすることはやってくれないので、場合によっては業務のユースケースごとに専用のクエリを実装することになります。
こうなると稼働性や保守性が下がり、コードの読む量も増えてしまうので開発者として開発が苦しいものになっていきます。

## こうなっていると嬉しい
苦労したことに対してこうなっていると扱いやすく、開発もしやすいだろうということを考えました。
こちらを設計の方針にしています。

・インターフェースがシンプルで、直感的に扱えること
・DynamoDBのSDK(クエリ組み立て)の知識が深くなくても扱えること
・誰が使っても大体同じような実装になること

## 自分で作ってみた

今回は自分で「便利な道具」を作ってみました。
一般的にDBへアクセスする際に要求される操作、データ取得(READ)、データ作成(CREATE)、データ更新(UPDATE)、データ削除(DELETE)の呼び出し方を誰が扱っても同じような形になるような設計を考えています。

[こちら](https://github.com/is-tech-kitakatsu/DynamodbAccessable)からコードを取得することができます。
(README.meにも使用方法を記載しています。)

## 使い方
1. 以下を用意する。 src/lib/dynamodb にそれぞれ規定クラスを用意しているのでそれを継承すること
リポジトリのREADME.meを参照にしてください
・テーブルスキーマを表現するEntityクラス
・DynamoDBとやりとりさせるRepositoryクラス
・DynamoDBから取得した一覧データの格納するCollectionクラス
・業務ロジックを定義するServiceクラス
2. Serviceクラスに対してDynamodbAccessableをミックスインする
これで準備はOKです。あとは拡張したこのクラスのインスタンスから、CRUD操作用の関数が使えるようになるので利用します。
    ```TypeScript
    import { ServiceBase } from '../dynamodb/services/serviceBase';
    import { Todo as Entity } from '../entities/todo';
    import { TodoCollection as Collection } from '../collections/todoCollection';
    import { TodoRepository as Repository } from '../repositories/todoRepository';
    import { DynamodbAccessable } from '../dynamodb/concerns/dynamodbAccessable';
    import { Constructor } from '../dynamodb/concerns/Constructor';
    import { dynamodbClient } from '../dynamodb/clients/dynamodb';
    
    
    export class TodoService extends DynamodbAccessable(
      ServiceBase<Entity, Collection, Repository> as Constructor<
        ServiceBase<Entity, Collection, Repository>
      >,
    ) {
      constructor(repository: Repository = new Repository(dynamodbClient)) {
        super(repository);
      }
    }
    ```
3.利用する
DynamodbAccessableをミックスインすることで利用できるようになる関数は以下です
- findBy(データ取得)
- findAllBy(データコレクション取得)
- create(データ作成)
- update(データ更新)
- delete(データ削除)

##### データ取得
findByを呼び出し、DynamnoDBテーブルに定義したキーとその値を渡す。
以下は、userIdとcreatedAtの複合キーのため2つ渡しています。
これでDBから所得したデータを、TodoのEntityオブジェクトとして扱えます。
```TypeScript
const todoService = new TodoService();
const todo = await todoService.findBy({ 
  userId:xxxxx,
  createdAt:xxxxx,
});
```
##### データコレクション取得
findByを呼び出し、DynamnoDBテーブルに定義したキーとその値を渡す。
以下は、userIdに紐づいたtodoのコレクションの取得です。
```TypeScript
const todoService = new TodoService();
const todo = await todoService.findAllBy({ 
  userId:xxxxx
});
```

##### データ作成
createを呼び出し、保存した値がセットされたEntityクラスのオブジェクトを渡すとその内容で保存します。
createの中ではEntityクラスで実装するバリデーションの関数がコールされるので、不正な値をセットしたオブジェクトは保存できないようになっています。
```TypeScript
const todo = new Todo(xxxxx, yyyyy, zzzzz);
const todoService = new TodoService();
const res = await todoService.create(todo);

// バリデーションエラーの場合はエラー内容を参照可能
console.log(todo.errors);
```
##### データ更新
更新対象のデータを取得したら、今回はassignAttributeという関数を用意していますが、Entityオブジェクトの属性値を更新。create同様update関数の引数にオブジェクトを渡すことでその時の属性値でDBを更新することができます。
```TypeScript
const todoService = new TodoService();
// 更新対象のDBのデータを取得
const todo = await todoService.findBy({ 
  userId: xxxx,
  createdAt: yyyy,
});
// 属性値の上書き
todo.assignAttribute({
  title: "updated title",
  describe: "updated describe",
});
// 保存の実行
const res = await todoService.update(todo);
バリデーションエラーの場合はエラー内容を参照可能
console.log(todo.errors);

```
##### データ削除
deleteを呼び出し、Entityクラスのオブジェクトを渡すとDBから削除されます。
```TypeScript
const todoService = new TodoService();

const todo = await todoService.findBy({ 
  userId: request.pathParameters.userId,
  createdAt: request.pathParameters.createdAt,
});
await todoService.delete(todo);
```

## 動かすために必要なもの
こちら自分でも動かしてみたい場合は以下を準備いただくとOKです。
実際のクラウド環境での利用になるので、多少なりとも課金が発生することがありますので、使い終わったら削除するなどご注意ください。


・Dockerアプリケーション
・AWSのアカウント


## 最後に
ここまでお読みいただき、ありがとうございます。
今回は、私がDynamoDBを利用するならこうやって実装するだろうという実装を形にしてみました。設計や実装方法は色んなものに左右されるものだと思います。
今回の記事は数ある方法のうちの1つとして参考にいただけると幸いです。
私自身、知識経験を積み上げている最中ですので、記事の至らぬ点などありましたらコメント等お待ちしております。


＊今回の内容はこれが正解だというわけではなく、１つの考え方ですのであくまで参考にする程度でご参考いただけるとちょうど良いと思います。