DynamoDBをNode.jsで操作する

Node.jsでDynamoDBを触ってみたので、備忘録として残しておきます。

プライマリーキーとして、パーティションキーのみの方法と、パーティションキー＋ソートキーでユニークにする方法があります。
前者を「ハッシュキーテーブル」、後者を「複合キーテーブル」と呼ぶことにします。

以下、参考にさせていただきました。(ありがとうございました)

　[コンセプトから学ぶAmazon DynamoDB【インデックス俯瞰篇】](https://dev.classmethod.jp/articles/conceptual-learning-about-dynamodb-index/)
　[【詳解】JavascriptでDynamoDBを操作する](https://qiita.com/Fujimon_fn/items/66be7b807a8329496899)
　[DynamoDB での Node.js の開始方法](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.NodeJs.html)
　 [Class: AWS.DynamoDB.DocumentClient](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html)

# 準備

Node.jsのソースコードのどこかに、以下を記載しておきます。

```index.js
const AWS = require("aws-sdk");
AWS.config.update({
  region: "ap-northeast-1",
});
const docClient = new AWS.DynamoDB.DocumentClient();
```

以降で、上記の変数docClientを使います。
それから、AWS Lambdaではなく自身のサーバ等で実行する場合は、以下と、``` aws configure ``` でIAMの認証情報を設定しておきます。
> npm install aws-sdk --save-dev


# ハッシュキーテーブル

プライマリキーがパーティションキーのみのハッシュキーテーブルを扱います。
こんな感じでテーブルを作成します。

テーブル名(tableName)：Hashkey_Table
パーティションキー名：firstkey

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5dd5adf8-2c6d-8ecf-1ffa-846fcb3aadd4.png)

## 登録

項目を登録します。SQLでいうところのInsertです。

```js
    var params_put = {
      TableName: tableName,
      Item:{
        firstkey: "firstvalue_1",
        "attr": "Hello_1",
      },
      ConditionExpression: 'attribute_not_exists(firstkey)',
    };
    result = await docClient.put(params_put).promise();
```

Itemのところに、登録したい項目の値を指定します。スキーマレスなので、任意の値(attr=”Hello_1”)も指定しています。ただし、プライマリキーは必ず指定する必要があります。

以下の指定はオプションです。指定した場合は、プライマリキーが同じ項目が登録されていた場合はエラー(throwが発生)となります。指定しなかった場合は、置き換わります。
　``` ConditionExpression: "attribute_not_exists(firstkey)"```

## 取得

プライマリキーを指定して、項目を取得します。
SQLと違って、1件のみ取得します。

```js
    var params_get = {
      TableName: tableName,
      Key: {
        firstkey: "firstvalue_1"
      }
    };
    result = await docClient.get(params_get).promise();
    if( result.Item ){
      console.log(JSON.stringify(result.Item));
    }else{
      // 見つからない場合はundefined
      console.log('Not found');
    }
```

## 更新

SQLでいうところのUPDATEです。
登録時に一緒に指定したattrの値を変更します。

```js
    var params_update = {
      TableName: tableName,
      Key: {
        firstkey: "firstvalue_1"
      },
      ExpressionAttributeNames: {
        '#attr': 'attr'
      },
      ExpressionAttributeValues: {
        ':attrValue': 'Hello_1_Update_1'
      },
      UpdateExpression: 'SET #attr = :attrValue',
      ConditionExpression: "attribute_exists(firstkey)",
      ReturnValues: "ALL_OLD" // "ALL_OLD" or "ALL_NEW"
    };
    result = await docClient.update(params_update).promise();
```

以下の指定はオプションです。指定した場合は、もし更新対象の項目が存在しなかった場合はエラー(throwが発生)となります。指定しなかった場合で、もし更新対象が項目が存在しなかった場合は、新規に登録されます。
　``` ConditionExpression: "attribute_exists(firstkey)" ```

以下もオプションです。指定すると、更新した項目の全体の値が返ってきます。
　``` ReturnValues:"ALL_NEW" ```

## 削除

指定したプライマリキーに合致する項目を削除します。

```js
    var params_delete = {
      TableName: tableName,
      Key: {
        firstkey: "firstvalue_1"
      },
//      ConditionExpression: 'attribute_exists(firstkey)',
    };
    result = await docClient.delete(params_delete).promise();
```

以下の指定はオプションです。もし指定した場合は、指定したプライマリキーに合致した項目がなかった場合にエラー(throw)となります。
　``` ConditionExpression: "attribute_exists(firstkey)" ```

## 一括取得

先ほどの取得は、1件のプライマリキーを指定して、合致する1件の項目を取得しました。
今度は、複数のプライマリキーを指定して、合致する複数の項目を取得します。

```js
    var params_batchget = {
      RequestItems: {}
    };
    params_batchget.RequestItems[tableName] = {
      Keys: [
        {
          firstkey: "firstvalue_2"
        },
        {
          firstkey: "firstvalue_3"
        },
      ]
    };
    result = await docClient.batchGet(params_batchget).promise();
    if( Object.keys(result.UnprocessedKeys).length > 0 )
      console.log('in process yet');
    for( var i = 0 ; i < result.Responses[tableName].length ; i++ ){
      console.log(i, JSON.stringify(result.Responses[tableName][i]));
    }
```

レスポンスの``` result.Responses[tableName]```に合致した項目が配列で格納されています。
もし、```result.UnprocessedKeys```に何か値が入っていると、全部の検索は完了していないため、再度取得をしないといけないようです。

## 全件取得

全件取得します。

```js
  var params_scan = {
    TableName: tableName
  };

  var list = [];
  while(true){
    var result = await docClient.scan(params_scan).promise();
    if( result.Items )
      Array.prototype.push.apply(list, result.Items);
    if( result.LastEvaluatedKey ){
      params_scan.ExclusiveStartKey = result.LastEvaluatedKey;
    }else{
      break;
    }
  }
  console.log(JSON.stringify(list));
```

# 複合キーテーブル

今度は、プライマリキーがパーティションキー＋ソートキーの複合キーテーブルを扱います。
こんな感じでテーブルを作成します。

テーブル名(tableName)：Complexkey_Table
パーティションキー名：firstkey
ソートキー名：secondkey

こんな感じで作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5e206fc3-c117-3979-c94e-44af668b3e4b.png)

## 登録

項目を登録します。

```js
    var params_put = {
      TableName: tableName,
      Item:{
        firstkey: "firstvalue_1",
        secondkey: "secondvalue_1",
        "attr": "Hello_1",
      },
      ConditionExpression: 'attribute_not_exists(firstkey)',
    };
    result = await docClient.put(params_put).promise();
```

ハッシュキーテーブルと同様、Itemに項目の値を指定します。
プライマリキーとソートキーでユニークとなるため、両方を指定する必要があります。

## 取得

項目を取得します。
1つの項目が返る前提ですので、ユニークに特定するため、プライマリキーとソートキーの両方を指定します。

```js
    var params_get = {
      TableName: tableName,
      Key: {
        firstkey: "firstvalue_1",
        secondkey: "secondvalue_1",
      }
    };
    result = await docClient.get(params_get).promise();
    if( result.Item ){
      console.log(JSON.stringify(result.Item));
    }else{
      // 見つからない場合はundefined
      console.log('Not found');
    }
```

## 一括取得

複数のプライマリキーを指定して、合致する複数の項目を一度に取得します。

```js
    var params_batchget = {
      RequestItems: {}
    };
    params_batchget.RequestItems[tableName] = {
      Keys: [
        {
          firstkey: 'firstvalue_1',
          secondkey: 'secondvalue_1',
        },
        {
          firstkey: 'firstvalue_2',
          secondkey: "secondvalue_2",
        },
        {
          firstkey: "firstvalue_3",
          secondkey: 'secondvalue_3',
        },
      ],
    };
    var result = await docClient.batchGet(params_batchget).promise();
    if( Object.keys(result.UnprocessedKeys).length > 0 )
      console.log('in process yet');
    for( var i = 0 ; i < result.Responses[tableName].length ; i++ ){
      console.log(i, JSON.stringify(result.Responses[tableName][i]));
    }
```

## 更新

項目の値を更新します。
ハッシュキーテーブルの時とほぼ同じです。
プライマリキーとして、パーティションキーとソートキーの両方を指定しています。

```js
    var params_update = {
      TableName: tableName,
      Key: {
        firstkey: "firstvalue_1",
        secondkey: "secondvalue_1",
      },
      ExpressionAttributeNames: {
        '#attr': 'attr'
      },
      ExpressionAttributeValues: {
        ':attrValue': 'Hello_1_Update_1'
      },
      UpdateExpression: 'SET #attr = :attrValue',
      ConditionExpression: 'attribute_exists(firstkey)',
      ReturnValues: "ALL_OLD" // "ALL_OLD" or "ALL_NEW"
    };
    result = await docClient.update(params_update).promise();
```

## 削除

項目の削除です。
ハッシュキーテーブルの時とほぼ同じです。
プライマリキーとして、パーティションキーとソートキーの両方を指定しています。

```js
    var params_delete = {
      TableName: tableName,
      Key: {
        firstkey: 'firstvalue_1',
        secondkey: 'secondvalue_1',
      },
//      ConditionExpression: 'attribute_exists(firstkey)',
    };
    result = await docClient.delete(params_delete).promise();
```

## 検索

条件に合致する項目を検索します。
取得と違って、複数の項目が返ってきます。

まず最初が、パーティションキーが合致する項目の取得です。
複合キーテーブルでは、パーティションキーとソートキーでユニークとするため、同じパーティションキーで、値の異なる複数のソートキーを登録することができます。
よって、パーティションキーのみを条件に指定した場合は、同じパーティションキーの複数の項目を取得できます。ちなみに、ソートキーのみを条件とした検索はできません。

```js
    var params_query = {
      TableName: tableName,
      ExpressionAttributeNames: {
        '#firstkey': 'firstkey',
      },
      ExpressionAttributeValues: {
        ':firstValue': "firstvalue_1",
      },
      KeyConditionExpression: '#firstkey = :firstValue',
    };
    result = await docClient.query(params_query).promise();
    for( var i = 0 ; i < result.Count; i++ ){
      console.log(i, JSON.stringify(result.Items[i]));
    }
    if( result.LastEvaluatedKey ){
        // 全取得できなかったので、追加検索
        params_query.ExclusiveStartKey = result.LastEvaluatedKey;
        // 再検索。LastEvaluatedKeyが返ってこなくなるまで繰り返し
    }
```

```result.Count``` には取得した項目の件数、```result.Items``` に取得した項目が配列で取得できます。

次は、ソートキーにさらに条件を付けて絞り込んで検索します。

```js
    var params_query = {
      TableName: tableName,
      ExpressionAttributeNames: {
        '#firstkey': 'firstkey',
        '#secondkey': 'secondkey',
      },
      ExpressionAttributeValues: {
        ':firstValue': 'firstvalue_1',
        ':secondValue': 'secondvalue_1_',
      },
      KeyConditionExpression: '#firstkey = :firstValue AND begins_with(#secondkey, :secondValue)',
    };
    result = await docClient.query(params_query).promise();
    for( var i = 0 ; i < result.Count; i++ ){
      console.log(i, JSON.stringify(result.Items[i]));
    }
    if( result.LastEvaluatedKey ){
        // 全取得できなかったので、追加検索
        params_query.ExclusiveStartKey = result.LastEvaluatedKey;
        // 再検索。LastEvaluatedKeyが返ってこなくなるまで繰り返し
    }
```

上記では1例として、以下を指定しています。
　``` begins_with(#secondkey, :secondValue)```

これは、ソートキーが、指定した値`secondvalue_1_`で始まる文字列の場合に合致したとみなされます。

(参考) DynamoDB でのクエリの操作
　https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Query.html


## 全項目取得

通常は、パーティションキーの指定が必須のため、例えば、項目を全取得することはできません。
しかし、queryではなく、scanを使えば、パーティションキーの指定は必須ではないため、全取得等、パーティションキーをまたいだ処理ができます。
ただし、scanは、いったんクライアント側に取得してからフィルタリングするため、メモリ使用量や性能にご注意ください。また、ProjectionExpressionで、返される項目を限定することができます。

```js
    var params_scan = {
      TableName: tableName,
      FilterExpression: "secondkey = :secondValue",
      ExpressionAttributeValues: {
        ":secondValue": "secondvalue_1"
      },
      ProjectionExpression: "firstkey, secondkey",
    };
    result = await docClient.scan(params_scan).promise();
    for( var i = 0 ; i < result.Count; i++ ){
      console.log(i, JSON.stringify(result.Items[i]));
    }
    if( result.LastEvaluatedKey ){
        // 全取得できなかったので、追加検索
        params_scan.ExclusiveStartKey = result.LastEvaluatedKey;
        // 再検索。LastEvaluatedKeyが返ってこなくなるまで繰り返し
    }
```

# (参考)テストプログラム

```js
const AWS = require("aws-sdk");
AWS.config.update({
  region: "ap-northeast-1",
});
const docClient = new AWS.DynamoDB.DocumentClient();
const HashkeyTableName = 'Hashkey_Table';
const ComplexkeyTableName = 'Complexkey_Table';

async function hashkey_insertItem(){
  var params_put = {
    TableName: HashkeyTableName,
    Item:{
      firstkey: "firstvalue_1",
      "attr": "Hello_1",
    },
    ConditionExpression: 'attribute_not_exists(firstkey)',
  };
  await docClient.put(params_put).promise();
}

async function hashkey_getItem(){
  var params_get = {
    TableName: HashkeyTableName,
    Key: {
      firstkey: "firstvalue_1"
    }
  };
  var result = await docClient.get(params_get).promise();
  return result.Item;
}

async function hashkey_getAllItem(){
  var params_scan = {
    TableName: HashkeyTableName,
//    ProjectionExpression: "firstkey",
  };

  var list = [];
  while(true){
    var result = await docClient.scan(params_scan).promise();
    if( result.Items )
      Array.prototype.push.apply(list, result.Items);
    if( result.LastEvaluatedKey ){
      params_scan.ExclusiveStartKey = result.LastEvaluatedKey;
    }else{
      break;
    }
  }

  return list;
}

async function hashkey_updateItem(){
  var params_update = {
    TableName: HashkeyTableName,
    Key: {
      firstkey: "firstvalue_1"
    },
    ExpressionAttributeNames: {
      '#attr': 'attr'
    },
    ExpressionAttributeValues: {
      ':attrValue': 'Hello_1_Update_1'
    },
    UpdateExpression: 'SET #attr = :attrValue',
    ConditionExpression: "attribute_exists(firstkey)",
    ReturnValues: "ALL_OLD" // "ALL_OLD" or "ALL_NEW"
  };
  var result = await docClient.update(params_update).promise();
  return result;
}

async function hashkey_deleteItem(){
  var params_delete = {
    TableName: HashkeyTableName,
    Key: {
      firstkey: "firstvalue_1"
    },
    ConditionExpression: 'attribute_exists(firstkey)',
    ReturnValues: "ALL_OLD"
  };
  var result = await docClient.delete(params_delete).promise();
  return result;
}


async function complexkey_insertItem(){
  var params_put = {
    TableName: ComplexkeyTableName,
    Item:{
      firstkey: "firstvalue_1",
      secondkey: "secondvalue_1",
      "attr": "Hello_1",
    },
    ConditionExpression: 'attribute_not_exists(firstkey)',
  };
  await docClient.put(params_put).promise();
}

async function complexkey_insertItem2(){
  var params_put = {
    TableName: ComplexkeyTableName,
    Item:{
      firstkey: "firstvalue_1",
      secondkey: "secondvalue_2",
      "attr": "Hello_2",
    },
    ConditionExpression: 'attribute_not_exists(firstkey)',
  };
  await docClient.put(params_put).promise();  
}

async function complexkey_getItem(){
  var params_get = {
    TableName: ComplexkeyTableName,
    Key: {
      firstkey: "firstvalue_1",
      secondkey: "secondvalue_1",
    }
  };
  var result = await docClient.get(params_get).promise();
  return result.Item;
}

async function complexkey_getAllItem(){
  var params_scan = {
    TableName: ComplexkeyTableName,
    // FilterExpression: "secondkey = :secondValue",
    // ExpressionAttributeValues: {
    //   ":secondValue": "secondvalue_1"
    // },
//    ProjectionExpression: "firstkey, secondkey",
  };
  var list = [];
  while(true){
    var result = await docClient.scan(params_scan).promise();
    if( result.Items )
      Array.prototype.push.apply(list, result.Items);
    if( result.LastEvaluatedKey ){
        params_scan.ExclusiveStartKey = result.LastEvaluatedKey;
    }else{
      break;
    }
  }

  return list;
}

async function complexkey_updateItem(){
  var params_update = {
    TableName: ComplexkeyTableName,
    Key: {
      firstkey: "firstvalue_1",
      secondkey: "secondvalue_1",
    },
    ExpressionAttributeNames: {
      '#attr': 'attr'
    },
    ExpressionAttributeValues: {
      ':attrValue': 'Hello_1_Update_1'
    },
    UpdateExpression: 'SET #attr = :attrValue',
    ConditionExpression: 'attribute_exists(firstkey)',
    ReturnValues: "ALL_OLD" // "ALL_OLD" or "ALL_NEW"
  };
  var result = await docClient.update(params_update).promise();
  return result;
}

async function complexkey_updateItem2(){
  var params_update = {
    TableName: ComplexkeyTableName,
    Key: {
      firstkey: "firstvalue_1",
      secondkey: "secondvalue_2",
    },
    ExpressionAttributeNames: {
      '#attr': 'attr'
    },
    ExpressionAttributeValues: {
      ':attrValue': 'Hello_2_Update_2'
    },
    UpdateExpression: 'SET #attr = :attrValue',
    ConditionExpression: 'attribute_exists(firstkey)',
    ReturnValues: "ALL_OLD" // "ALL_OLD" or "ALL_NEW"
  };
  var result = await docClient.update(params_update).promise();
  return result;
}

async function complexkey_deleteItem(){
  var params_delete = {
    TableName: ComplexkeyTableName,
    Key: {
      firstkey: 'firstvalue_1',
      secondkey: 'secondvalue_1',
    },
    ConditionExpression: 'attribute_exists(firstkey)',
    ReturnValues: "ALL_OLD"
  };
  var result = await docClient.delete(params_delete).promise();
  return result;
}

async function complexkey_deleteItem2(){
  var params_delete = {
    TableName: ComplexkeyTableName,
    Key: {
      firstkey: 'firstvalue_1',
      secondkey: 'secondvalue_2',
    },
    ConditionExpression: 'attribute_exists(firstkey)',
    ReturnValues: "ALL_OLD"
  };
  var result = await docClient.delete(params_delete).promise();
  return result;
}

(async () =>{
  try{
    // for hashkey test
    var list = await hashkey_getAllItem();
    console.log("hashkey_getAllItem", list);

    await hashkey_insertItem();
    console.log('hashkey_insertItem');

    var item = await hashkey_getItem();
    console.log("hashkey_getItem", item);

    var updated = await hashkey_updateItem();
    console.log("hashkey_updateItem", updated);

    var list = await hashkey_getAllItem();
    console.log("hashkey_getAllItem", list);

    var deleted = await hashkey_deleteItem();
    console.log("hashkey_deleteItem", deleted);

    var list = await hashkey_getAllItem();
    console.log("hashkey_getAllItem", list);

    // for complexkey test
    var list = await complexkey_getAllItem();
    console.log("complexkey_getAllItem", list);

    await complexkey_insertItem();
    console.log('complexkey_insertItem');
    await complexkey_insertItem2();
    console.log('complexkey_insertItem2');

    var item = await complexkey_getItem();
    console.log("complexkey_getItem", item);

    var list = await complexkey_getAllItem();
    console.log("complexkey_getAllItem", list);

    var updated = await complexkey_updateItem();
    console.log("complexkey_updateItem", updated);
    var updated = await complexkey_updateItem2();
    console.log("complexkey_updateItem2", updated);

    var list = await complexkey_getAllItem();
    console.log("complexkey_getAllItem", list);

    var deleted = await complexkey_deleteItem();
    console.log("complexkey_deleteItem", deleted);

    var deleted = await complexkey_deleteItem2();
    console.log("complexkey_deleteItem2", deleted);
    
    var list = await complexkey_getAllItem();
    console.log("complexkey_getAllItem", list);
  }catch(error){
    console.error(error);
  }
})();
```

# 終わりに

DynamoDBには、セカンダリインデックスという機能がありますが、また今度。

以上