LoginSignup
3
3

More than 1 year has passed since last update.

DynamoDBをNode.jsで操作する

Last updated at Posted at 2020-06-14

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

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

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

 コンセプトから学ぶAmazon DynamoDB【インデックス俯瞰篇】
 【詳解】JavascriptでDynamoDBを操作する
 DynamoDB での Node.js の開始方法
  Class: AWS.DynamoDB.DocumentClient

準備

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

登録

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

    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件のみ取得します。

    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の値を変更します。

    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"

削除

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

    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件の項目を取得しました。
今度は、複数のプライマリキーを指定して、合致する複数の項目を取得します。

    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に何か値が入っていると、全部の検索は完了していないため、再度取得をしないといけないようです。

全件取得

全件取得します。

  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

登録

項目を登録します。

    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つの項目が返る前提ですので、ユニークに特定するため、プライマリキーとソートキーの両方を指定します。

    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');
    }

一括取得

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

    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]));
    }

更新

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

    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();

削除

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

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

検索

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

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

    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 に取得した項目が配列で取得できます。

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

    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で、返される項目を限定することができます。

    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が返ってこなくなるまで繰り返し
    }

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

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には、セカンダリインデックスという機能がありますが、また今度。

以上

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3