JavaScript
AWS
DynamoDB
クラウド
React

AWS+Reactアプリ作成入門(DynamoDB編)

AWS+Reactアプリ作成入門(Cognito編)
AWS+Reactアプリ作成入門(S3編)
AWS+Reactアプリ作成入門(DynamoDB編)
AWS+Reactアプリ作成入門(IAM Role編)
AWS+Reactアプリ作成入門(ログイン後のAdmin編)

今回作成したアプリ ==>久喜SNS

 DynamoDBはAWSが提供するDBサービスです。MongoDBと比べるとちょっと癖がありますし、私も全体的な理解が十分ではないので、説明は今回アプリの利用に限定することとして、断定的に記述していきたいと思います。偏見や偏りがあるかもしれないという事ですが、ご容赦ください。

  1. テーブルの検索はqueryとscanがあります。
  2. scanは無条件に全テーブルをサーチしコストが高いので使わないことにします。<== 7. の制限があるからindexキーの設計によってはscanも必要かな?
  3. queryはindexに対して検索条件を与えて検索するものです。
  4. テーブル作成時にprimary indexを設定するのが必須です
  5. indexキーは2つの項目を指定します。partition key とsort keyと呼びます(partition keyのみでもok)
  6. partition keyはテーブルを分割して、検索時に分割された領域だけをサーチするようにする、イメージですかね。
  7. query検索時にはpartition keyを指定して(equal条件)、sort keyでフィルタします。 <==partition keyはequal条件でないとダメ、それ以外を指定するとエラーで嵌ります。
  8. query検索時は自動的にsort keyでソートされます。
  9. primary keyでないものを条件として検索したい時は、secondary indexを明示的に作成します(課金される)
  10. secondary indexでも検索したい条件に合わせて、partition key とsort keyを定義します。

1.投稿のpost

 以下のようなコードで、画像掲示板のテーブルへを投稿します。

src/views/Admin.jsの一部
    var docClient = new AWS.DynamoDB.DocumentClient();
    var params = {
        TableName: tablename,
        Item:{
             identityId: identityId, // ★prime partition key
             email: _self.state.email,
             username: _self.state.username,
             filename: filepath,
             thumbnail: thumbnail,
             type: fileType,
             title: title,
             story: story,
             imageOverwrite: _self.state.imageOverwrite,
             mapUse: _self.state.mapUse,
             position: _self.state.position,
             uploadTime: uploadTime, // ★prime & secondary sort key
             uploadDate: uploadDate,
             partitionYear: partitionYear, //★secondary partition key
             refCounter: 0
        }
    };
    docClient.put(params, function(err, data) {
        if(err) {
            console.log("Err: table put :" +err);
        } else {
            console.log("Success: table put ok");
        }
    });

 docClient.put()で投稿をテーブルに挿入します。primary keyとsecondary keyはコメントで示した通りです。これは検索に使われますが、以下に説明します。

2.投稿の検索

 このテーブルは、トップ画面で最新投稿を検索するのと、管理画面で自分の最新投稿を検索する2つの種類の検索があります。どちらも最新順のリストを取得します。

1. 管理画面で自分の最新順の投稿リストを検索

primary indexを以下のキーで作成
partition key : identityId (文字列) 
sort key : uploadTime (数値)

 identityIdはユーザIDとして使っているもので、Cognitoでのログイン時にAWS.config.credentials.identityIdに値が設定されるものです。uploadTimeは投稿時間でunixtimeです。

2. トップ画面で最新順の投稿リストを検索

secondary indexを以下のキーで作成
partition key: partitionYear (数値)
sort key : uploadTime (数値)

 まず「1. 管理画面で自分の最新順の投稿リストを検索」をみてみます。partitionYearはuploadTimeを年数字に変換したものです。2017とかの数字です。uploadTimeは投稿時間です。このpartitionYearというpartition keyは年によってテーブルを分割するものです。最初はテーブルは2017しかありませんが、来年以降2018,2019,2020..と分割されていきます。検索時にpartitionYear=2018と指定したら、2018の部分だけを検索してくれます。大雑把すぎる場合は年月を指定して201805とか指定するようにpartition keyの定義を変更すれば良いと思われます。

 ちなみに昔はAWS.DynamoDB()が使われていたようですが、検索結果に不要の型(SとかNとか)が含まれとても使いにくいので、ここではAWS.DynamoDB.DocumentClient()を使っています。

 以下にAdminページでユーザID(identityId)毎の最新記事をテーブルから取得するコードを示します。

src/views/Admin.jsの一部
    var dynamo = new AWS.DynamoDB.DocumentClient();

    var param = {
      TableName : tablename,
      ScanIndexForward: false, //queryには効くが、scanには効かない
      KeyConditionExpression : "identityId = :identityId",
      ExpressionAttributeValues : {":identityId" : identityId}
    };
    dynamo.query(param, function(err, data) {
        if (err) {
            console.log("### Error="+err);
        } else {
            //console.log("### data="+JSON.stringify(data.Items));
            _self.setState({items: data.Items});
        }
    });

 KeyConditionExpressionではpartition keyを指定しているだけですが、sort keyの条件を加えて、検索結果を絞り込むことが可能です。また取得は暗黙的にsort keyでソートされますが、デフォルトで昇順になってしまいます。ここでは最新順で降順なのです、ScanIndexForward: false を指定しています。

次に。「2. トップ画面で最新順の投稿リストを検索」をみてみます。
以下はIndexNameを指定しsecondary indexを使います。partition keyがpartitionYearなので、KeyConditionExpressionでEqual条件で絞り込みます。

src/views/Home.jsの一部
 var param = { 
    TableName : tablename,
    IndexName: "partitionYear-uploadTime-index",
    ScanIndexForward: false, //queryには効くが、scanには効かない
    ExpressionAttributeNames : {'#k' : 'partitionYear'},
    ExpressionAttributeValues : {':partitionYear' : 2017},
    KeyConditionExpression : '#k = :partitionYear',
    Limit: 20
  };
  dynamo.query(param, function(err, data) {
      if (err) {
          console.log("### Error="+err);
      } else {
          _self.setState({items: data.Items});
          //--- cache update
          var params = { 
              Bucket: bucketname,
              Key: cachepath,
              ContentType: 'application/json',
              Body: JSON.stringify(data.Items)
          };
          s3.putObject(params, function(err, data) {
            if (err) console.log("### cache upload error",err, err.stack);
            else     console.log("### cache upload successful"+data);
          });
      }
  });

3.投稿削除

 以下のようなコードで、画像掲示板のテーブルから投稿を削除します。

src/views/Admin.jsの一部
    const docClient = new AWS.DynamoDB.DocumentClient();
    const params3 = {
        TableName: tablename,
        Key: {
           identityId: item.identityId, // ★partition key
           uploadTime: item.uploadTime  // ★sort key
        }
    };
    docClient.delete(params3, function (err, res) {
      if (err) {
          console.log("### delete table err:"+err); // an error occurred
      } else{
          console.log("### delete table ok"); // successful response
          if(addCallback) { //編集 => 削除 then 追加
              addCallback();
          }
      }
    });

 partition keyとsort keyを指定し、docClient.delete()で投稿を削除しています。

4.投稿編集

 編集は、古いものを削除して新しいものを挿入する、という考えで実装しています。上の削除のコードで、削除が成功した時にaddCallback()を呼んでいるのがそれに当たります。単に削除だけを行いたい場合はaddCallback=nullとしてこの関数を呼びます。

5.カウンター

 投稿のページにはそれぞれカウンターを設け参照数をカウントしています。投稿のpostで示したコードのrefCounterがそれに当たります。今回はReactのCounterコンポーネントを作成し、参照されるごとにアトミックにインクリメントするコードを書きました。

src/views/Counter.js
import React from 'react';
import AWS from "aws-sdk";
import appConfig from '../appConfig';
import {bucketname,tablename}  from '../appConfig';

//http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.NodeJs.03.html
export default class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        counter: 0
    };
  }

  componentWillMount() {
    const _self=this;
    const dynamo = new AWS.DynamoDB.DocumentClient();
    var params = {
        TableName:tablename,
        Key:{
            "identityId": this.props.identityId,
            "uploadTime": this.props.uploadTime
        },
        UpdateExpression: "set refCounter = refCounter + :val",
        ExpressionAttributeValues:{
            ":val":1
        },
        ReturnValues:"UPDATED_NEW"
    };
    console.log("Updating the item...");
    dynamo.update(params, function(err, data) {
        if (err) {
            console.error("Unable to update item. Error JSON:", JSON.stringify(err, null, 2));
        } else {
            console.log("UpdateItem succeeded:", JSON.stringify(data, null, 2));
            _self.setState( {counter: data.Attributes.refCounter} );
        }
    });
  }

  render () {
    return (
        <div>{this.state.counter}</div>
    );
  }
}

 Counterコンポーネントは親コンポーネントからidentityIdとuploadTimeを渡され、this.propsで参照しています。カウンターは dynamo.update()でアトミックにインクリメントされます。

 今回はこれで終わりです。次回以降にIAMのRoleについて述べたいと思います。