DynamoDB Local用のViewerをSpring Bootベースで作ってみた

DynamoDB Localって便利ですよね。実際にAmazon DynamoDBにつなぎに行かなくてもCRUD操作をローカル環境で試せるので重宝しています。

```
java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb
```
このコマンドでDynamoDBがシミユレートできてしまうのだから、とても便利。

が、、、投入したデータの確認、となると一手間かかってしまいます。```localhost:8000/shell```（デフォルトの画面URL）で見れるこの画面、ありがたいのですが、今ひとつ物足りないですよね。

<img width="1229" alt="スクリーンショット 2017-01-04 19.54.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/102769/09dae744-4a2d-3ec6-dcde-d787507e59e3.png">

やっぱり本家のこういう画面が欲しいなあ、、、と。こういうテーブル一覧とか、、、

<img width="1329" alt="スクリーンショット 2017-01-04 19.45.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/102769/e6b3f860-678f-a301-6a3e-4114c8cfdb35.png">

各テーブルの詳細画面とか。

<img width="1052" alt="スクリーンショット 2017-01-04 19.56.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/102769/537fc92c-86b1-7dab-c34d-2979d8de8bb4.png">

とりあえず目下自分が困っていたのは投入したデータの確認が容易にできないことだったので、DynamoDB Localにつなぎに行ってデータの中身を可視化してくれるViewerを作成しました。

# 結果
こちらに登録してあります。ご自由にお使いください。起動方法などはREADME.mdをご覧ください。

https://github.com/kojiisd/dynamodb-local-view

かなり力技で実装している部分があります。ソートやフィルタは実装していません。Pull Requestしてくれる方、大歓迎です。またデータ増えた時にパフォーマンス大丈夫かとかその辺は、知らんがな(´・ω・｀)。今回の要件ではそんなにローカルでデータ保持しない想定だからダイジョーブ。

Mavenでのビルドから、Javaコマンドで実行できます。

```
$ mvn clean package
$ java -jar target/dynamodb-view-0.0.1-SNAPSHOT.jar
```

# 前提
- `2016-05-17_1.0`バージョンのDynamoDB Local jarファイルを利用しました。
- Spring Boot + AngularJSで開発しました。

# 機能と画面
`listTables`と`scan`しかしていません。画面も2画面しか作っていません。

## 1. テーブル一覧画面

DynamoDB Localを起動した状態でアプリケーションから接続に行きます。 `localhost:8080` でページが見れます。この画面は現在DynamoDB Localに保存されているテーブルの一覧になります。

![スクリーンショット 2017-01-22 16.35.32.png](https://qiita-image-store.s3.amazonaws.com/0/102769/3089c10a-1a76-ccb4-333e-89c3359d5b76.png)

各テーブルにはリンクを付与しており、クリックするとそのテーブルのデータ一覧が見れます。

## 2. テーブル詳細画面
こんな感じになっています。DynamoDBは列指向ですので、動的に列が増えたとしても、対応していないレコードは空で表示するような仕組みにしています。

![スクリーンショット 2017-01-22 16.35.22.png](https://qiita-image-store.s3.amazonaws.com/0/102769/256305ff-b3b0-5148-bfa0-8051b39ab9ef.png)

この例ではsensor_idカラムとtimestampカラムをそれぞれ「HASH」、「RANGE」にしてテーブル作成しています。

# テーブル作成のサンプルデータ
今回のテーブル（sample_table3）を表示するためのテーブル作成定義とデータサンプルは以下になります。

とりあえずこのままDynamoDB Localの管理画面（localhost:8000/shellでアクセスできるアレ）に投入すれば、テーブルの作成はできます。

```:CreateTable用データ定義
var params = {
    TableName: "sample_table3",
    KeySchema: [
        {
            AttributeName: "sensor_id",
            KeyType: "HASH"
        },
        {
            AttributeName: "timestamp",
            KeyType: "RANGE"
        }
    ],
    AttributeDefinitions: [
        {
            AttributeName: "sensor_id",
            AttributeType: "S"
        },
        {
            AttributeName: "timestamp",
            AttributeType: "S"
        }
    ],
    ProvisionedThroughput: {
        ReadCapacityUnits: 1, 
        WriteCapacityUnits: 1
    }
};

dynamodb.createTable(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response
});
```
データ作成はこんな感じでできます。

```:サンプルデータ用定義
var params = {
    TableName: "sample_table3",
    Item: { 
        sensor_id: "acceleration-sensor01",
        accelerationX: 1.254,
        accelerationY: 0.001,
        accelerationZ: 0.178,
        timestamp: "2017-01-22T10:01:24"
    },
    ReturnValues: "NONE",
    ReturnConsumedCapacity: "NONE",
    ReturnItemCollectionMetrics: "NONE"
};
docClient.put(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response
});
```

# 最後に
今回のツールのおかげで、個人的にDynamoDB Localに保持したデータの確認がかなり容易になりました。このツールを使ってバシバシデータの確認をしてもらえたら、と思います。

# おまけ（少し困ったところ）
今回列指向のデータベースに対するアクセスでしたので、各レコードが持っている異なるカラムを、どのようにJava側でデータ保持して、画面側に渡し、表示するのがいいのか、については少し苦戦しました。

結果として以下のようにしました。

1. [サーバ] 一旦値が空のキーのみのMapを作成する　→　カラムの過不足をなくす
2. [サーバ] データが存在する部分だけ値を入れる　→　データが存在しない箇所は空文字になる
3. [クライアント] ヘッダ作成部とデータ表示部でテーブルの作り方に一工夫

## [サーバ] 一旦値が空のキーのみのMapを作成する
この辺りの実装ですね。少し力技です。画面側の表示を簡単にするためにサーバ側に処理を多めに入れています。

```java
private Map<String, String> createEmptyColumnMap(ScanResult scanResult) {
    Map<String, String> columnMap = new LinkedHashMap<String, String>();
    for (Map<String, AttributeValue> valueMap : scanResult.getItems()) {
        for (Map.Entry<String, AttributeValue> valueMapEntry : valueMap.entrySet()) {
            columnMap.put(valueMapEntry.getKey(), StringUtils.EMPTY);
        }
    }
    return columnMap;
}
```

## [サーバ] データが存在する部分だけ値を入れる
ここも力技。今回いくつかの型には（全部ではない）対応しましたが、これってDynamoDBのAPIを利用してもっとうまくできないですかね？

```java
private String extractColumnValue(AttributeValue value) {
    String result = StringUtils.EMPTY;

    if (value == null) {
        return result;
    }

    if (value.getBOOL() != null) {
        result = value.getBOOL().toString();
    } else if (value.getB() != null) {
        result = value.getB().toString();
    } else if (value.getN() != null) {
        result = value.getN();
    } else if (value.getS() != null) {
        result = value.getS();
    } else if (value.getM() != null) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            result = mapper.writeValueAsString(value.getM());
        } catch (JsonProcessingException ex) {
            // TODO Exception handling
            ex.printStackTrace();
            result = StringUtils.EMPTY;
        }
    } else if (value.getSS() != null) {
        result = String.join(VALUE_SEPARATOR, value.getSS().toArray(new String[0]));
    } else if (value.getNS() != null) {
        result = String.join(VALUE_SEPARATOR, value.getNS().toArray(new String[0]));
    } else if (value.getBS() != null) {
        result = String.join(VALUE_SEPARATOR, value.getBS().toArray(new String[0]));
    }
    return result;
}
```

## [クライアント] ヘッダ作成部とデータ表示部でテーブルの作り方に一工夫
ng-repeatを使ってAngularJS側でパッとテーブル表示したかったのですが、カラム名が動的に変わるのでこんな感じの実装になりました。`limitTo:`を使ってほぼ無理矢理です。これもよりスマートなやり方があればぜひ知りたいです。

```html
<table class="table table-condensed table-bordered table-striped">
    <thead>
        <tr ng-repeat="data in datas | limitTo:1">
            <th>No</th>
            <th ng-repeat="(key, val) in data">{{ key }}</th>
        </tr>
    </thead>
    <tbody>
      <tr ng-repeat="data in datas">
          <td>{{ $index + 1 }}</td>
          <td ng-repeat="(key, val) in data">{{ val }}</td>
      </tr>
    </tbody>
</table>
```