DynamoDB DataMapperをTypeScriptで使ってみた

# 概要
2017年11月 AWSより、[DynamoDB DataMapper](https://github.com/awslabs/dynamodb-data-mapper-js)というライブラリがDeveloper Previewで公開されました。
このDynamoDB DataMapperは、Javaで言うDynamoDB Mapperに近い感じで、クラスとDynamoDBに保存する型をマッピングすることができます。
またJavascriptの新しい記法を押さえているいい感じのライブラリなのですが、あまり日本語で紹介している記事がないので、ここでは**TypeScriptでの使い方**を簡単に紹介していきます。

※本記事で紹介しているソースコードはTypeScript 2.9.2を使用しています。

# DynamoDB DataMapper
DynamoDB DataMpperのGitHubには「DataMapperはアプリケーションのドメインクラスとDynamoDBに保存される型の相互運用を容易にします」と記載されています。

現在はv0.7.3まで公開されています。※2018年10月29日現在

# クラス宣言
まずはドメインクラスを宣言します。
[@aws/dynamodb-data-mapper-annotations](https://github.com/awslabs/dynamodb-data-mapper-js/tree/master/packages/dynamodb-data-mapper-annotations)パッケージを利用すると、`Decorators`を用いて、DynamoDBのTableとクラスのマッピングや、HashKeyやRangeKey、Attribute等とクラスプロパティをマッピングすることができます。
以下の例では`id`, `createdAt`, `completed`の3つのプロパティを持つドメインクラスMyModelを宣言しています。

```typescript:MyModel.ts
import {
  attribute,
  hashKey,
  rangeKey,
  table,
} from '@aws/dynamodb-data-mapper-annotations'

@table('MyModels')
export default class MyModel {
  @hashKey()
  id: string

  @rangeKey({ defaultProvider: () => new Date() })
  createdAt: Date

  @attribute()
  completed?: boolean
}
```

※dynamodb-data-mapper-annotationsを使う際は、tsconfig.jsonで`experimentalDecorators`と`emitDecoratorMetadata`オプションをtrueにする必要があります。

`createdAt`はDate型で宣言していますが、DynamoDBに格納する際はDataMapperがNumber型に変換して格納します。逆にGetなどで取得する際は、DataMapperがDate型に変換します。
また、`defaultProvider: () => new Date()`は、DynamoDBにオブジェクトを格納する際、プロパティが`undefined`であれば指定したfunctionの結果を代入してくれます。

@aws/dynamodb-data-mapper-annotationsは上記以外にも、`autoGeneratedHashKey`や`versionAttribute`などの機能を提供しています。詳細は[GitHub](https://github.com/awslabs/dynamodb-data-mapper-js/tree/master/packages/dynamodb-data-mapper-annotations)をご参照下さい。

# オペレーション
DynamoDBの書き込み、読み込みオペレーションの一部を紹介します。

putやGetなどのオペレーションには`DataMapper`インスタンスを利用します。

```typescript
import { DataMapper } from '@aws/dynamodb-data-mapper'
const DynamoDB = require('aws-sdk/clients/dynamodb')

const mapper = new DataMapper({
  client: new DynamoDB(),
  tableNamePrefix: 'dev_'
})
```

DataMapperインスタンスの生成時に`tableNamePrefix`オプションを指定すると、DataMapperは対象のクラス(この記事の例では`MyModel.ts`)で宣言しているTable NameにPrefixを付けたものを利用します。

## Get
```typescript
const date = new Date(2018, 9, 1)
const result: MyModel = await mapper.get(Object.assign(new MyModel, { 
  id: 'A',
  createdAt: date,
}))
```

`get`の戻り値はPromiseなので、`await`で受け取れます。
第一引数は`Object.assign`を使っていますが、`new`したオブジェクトにプロパティを設定したものを渡しても大丈夫です。
`get`の第二引数には、読み込み整合性などのオプションが指定できます。
## Put
```typescript
const result: MyModel = await mapper.put(Object.assign(new MyModel,  { 
  id: 'A',
  completed: true,   
}))
```

`get`と同様にPromiseが返ってきます。
`put`の第二引数には、条件式などのオプションが指定できます。

## Query
Queryは少し特殊で、ES2018の機能である`Async Iteration`を利用します。
まずはサンプルコードを見てみましょう。

```typescript
const items = []
const query = mapper.query(MyModel, { id: 'A' })

for await (const item of query) {
  items.push(item)
}

```

`query`の返り値はAsync Iteratorのため、中身を取得する際は`for-await-of`構文を利用することで一つずつアイテムを取得することができます。

第二引数にはKeyConditionの指定が可能です。上記の例では`id: A`であるアイテムを取得しています。
さらにソートキーを[KeyConditionExpression](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions)で指定できます。
下記の例ではソートキーである`createdAt`を範囲指定で検索しています。

```typescript
const items = []
const query = mapper.query(MyModel, {
        id: 'A',
        createdAt: between(1514732400, 1543590000) // 2018-01-01 00:00:00 ~ 2018-12-01 00:00:00を指定
      }
);
for await (const item of query) {
  items.push(item)
}
```
ここでは`@aws/dynamodb-expressions`パッケージに含まれる`between`という関数を利用しています。

DynamoDB.DocumentClientの場合だと、

```js
const params = {
  TableName: 'MyModels'
  KeyConditionExpression: 'id = :id and createdAt between :startTime and :endTime',
  ExpressionAttributeValues: {
    ':id': 'A',
    ':startTime': 1514732400,
    ':endTime':  1543590000
  }
}
```

のように`KeyConditonExpression`で式を記述したり`ExpressionAttributeValues`で値を指定しなければならなかったのですが、`between`関数を利用するとかなり簡素になりますね。
`@aws/dynamodb-expressions`には他にも`equals`, `notEquals`, `lessThan`, `greaterThan`など色々な関数が用意されています。

### Pagination
Queryにページネーションが発生する可能性がある場合は注意が必要です。
DynamoDBのQueryは1度に取得できるサイズは1MBまでという制限があるため、検索結果がそれを超える場合はレスポンスから取得できる`lastEvaluatedKey`を利用して再度クエリをする必要があります。

```typescript
const items = []
const paginator = mapper.query(MyModel, { id: 'A' }).pages();
for await (const page of paginator) {
  for (const item of page) {
    items.push(item)
  }
}

const newPaginator = mapper.query(MyModel, { id: 'A'},
  { startKey: paginator.lastEvaluatedKey })
.pages()
```
上記の例では1回目のqueryで取得した`lastEvaluatedKey`を、2回目のqueryの`startKey`オプションに渡して、queryを再開しています。

## まとめ
今回はDynamoDB DataMapperを利用したDynamoDBの基本的な操作方法の一部をご紹介しました。
これから使おうと思っている方の一助になれば幸いです。

DynamoDB DataMapperは未だDeveloper Previewなので、正式リリースが待ち遠しいです！