SEワンタンの独学備忘録

IT関連の独学した内容や資格試験に対する取り組みの備忘録

【AWS】(番外編)LambdaでNode.jsを用いてDynamoDBを操作する

LambdaからDynamoDBの操作を行いましたが、その際にサービス設定部分よりNode.jsの実装部分で少しハマりました(悪い方の意味)ので、コード部分の備忘録です。
js自体は多少のプログラミング経験がありますが、Node.jsに関しては本学習で初めて触った素人なのでそもそもNode.jsが分かっていなくて、Node.jsとして必要な部分、Lambdaに必要な部分、DynamoDBに必要な部分など混ざってる可能性もあるので参照の際は多少注意してください。
RDBMS感覚の小言が多くなりました。

前提とか

元々は以下の記事で行ったものです。
www.wantanblog.com

言語は「Node.js 10.x」、構成は再掲になりますが、以下の図の通りです。

f:id:wantanBlog:20191027235720p:plain

Node.jsの実装コード

全般

以前実装したコードにコメントを付けてみました。
プログラミングの実装センスがない感じなのと、インデントがずれてるのはご愛敬。

// 拡張機能
var aws = require('aws-sdk');
// リージョンの指定(東京リージョン)
var docClient = new aws.DynamoDB.DocumentClient({region: 'ap-northeast-1'});

exports.handler = async (event) => {
    
    var params = {
        // テーブル名
        TableName : 'demotable',
        // 条件の指定
        KeyConditionExpression: "#hash = :str",
        // 項目の置換
        ExpressionAttributeNames:{
            "#hash": "id"
        },
        // 条件の置換
        ExpressionAttributeValues: {
            ":str": "wantan"
        }
    };
    // DynamoDBにクエリを発行し戻り値を取得する
    var resultJSON = await docClient.query(params, function(err, data) {  
      if (err) {
        // エラーの場合、コンソールにエラー情報を表示
        console.log(err, err.stack);
      }else{
          // 取得できた場合、データをコンソールに表示
          console.log(data);
      }
    }).promise();
    
    var items = "miss";
    if(resultJSON != null){
        // 取得値からItemsを取り出す
        items = resultJSON.Items;
    }

    // レスポンスを設定する
    const response = {
        // ヘッダを設定
        headers: {
            'Content-type': 'application/json;charset=UTF-8'
        },
        statusCode: 200,
        body: JSON.stringify(items),
    };
    // 戻り値を設定する
    return response;
};

注意点はDynamoDBの接続処理が非同期で呼び出されるため、以下のようにただそのまま書くと望んだ結果をうまく取得できないこと。
テスト実行してもいまいち仕組みが分かりにくかった。

    // DynamoDBにクエリを発行し戻り値を取得する
    var resultJSON = docClient.query(params, function(err, data) {  
      if (err) {
        console.log(err, err.stack);
      }else{
        console.log(data);
      }
    })

なので、実装例のように同期処理にする必要がありました。

あと、DynamoDBの戻り値の扱い方がいまいち分からなかった。恥ずかしながらjsは使ってるくせにJSONの理解度が極めて低いんですねこれが。Itemsの中に取得値が格納されている感じなんですね。
複数レコードを取得した場合。
・そのまま(Itemsに内包)

resultJSON

・取得レコードのみ

resultJSON.Items

・特定行を取り出す

resultJSON.Items[0]

・特定項目を取り出す

resultJSON.Items[0].id


あとは操作の種類ごとにちょっぴりザックリかいとく。
関数以外のパラメータ設定とかも微妙に違う箇所があるので注意か。

全件のレコードを取得「scan」

関数の抜粋

   var params = {
        // テーブル名
        TableName : 'demotable',
    };
    var resultJSON = await docClient.scan(params, function(err, data) {  
    }).promise();

取得結果

{ Items:
   [ { id: 'ai', sortkey: '1', date: '2019/10/27' },
     { date: '2019/10/26', free: 'フリー', id: 'wantan', sortkey: '1' },
     { id: 'wantan', sortkey: '2', date: '2019/10/25' },
     { id: 'wantan', sortkey: '3', comment: 'DynamoDB' } ],
  Count: 4,
  ScannedCount: 4 }

事前に「query」を使用しているとスムーズに使えました。
条件を指定しないでいいのでテーブル名のみ指定します。
「query」とほぼ同じような使い方ができるっぽいけど、検索の仕組みが違うみたい?
「query」でも条件を指定しなければ全件取得できるけど、取得データ量に制限があったりとか
TABLE FULLSCANとINDEX使用するのみたいな感覚で使い分ければいいのかしら?遊びでやるにしてもさすがに全件と条件付きで使い分けておいた方がよさそう。

1件のレコードを取得「get」

関数の抜粋

    var params = {
        TableName : 'demotable',
        Key: {
        'id': 'wantan',
        'sortkey': '1'
        }
    };
    var resultJSON = await docClient.get(params, function(err, data) {  
    }).promise();

取得結果

{ Item: { date: '2019/10/26', free: 'フリー', id: 'wantan', sortkey: '1' } }

getを使用するときは「Key: 」で条件を指定するらしい。
あくまで一件のデータを取得することから注意が2点
・条件は必ず一件のレコードが取得できる条件を指定する
ソートキーが設定されていないテーブルの場合には1項目の指定だけでよい。
・戻り値は「Item」の中にレコードが格納されている
複数レコードの場合複数系の「Items」になるのでそことの違いですね。

queryと同じ要領でやろうとしたらうまくいかなかった。

複数のレコードを取得「query」

関数の抜粋

    var params = {
        TableName : 'demotable',
        KeyConditionExpression: "#hash = :str",
        ExpressionAttributeNames:{
            "#hash": "id"
        },
        ExpressionAttributeValues: {
            ":str": "wantan"
        }
    };
    var resultJSON = await docClient.query(params, function(err, data) {  
    }).promise();

取得結果

{ Items:
   [ { date: '2019/10/26', free: 'フリー', id: 'wantan', sortkey: '1' },
     { id: 'wantan', sortkey: '2', date: '2019/10/25' },
     { id: 'wantan', sortkey: '3', comment: 'DynamoDB' } ],
  Count: 3,
  ScannedCount: 3 }

一番最初の実装例でも使用している。
まず最初に「KeyConditionExpression: 」で条件を指定してその後に「ExpressionAttributeNames:」とか「ExpressionAttributeValues: 」でプレースホルダ的に置換する。この感覚がどうも慣れない。
RDBのSQL感覚でいうとこれが一番使用頻度が高そうでこればっか使う感じするけどどうなんだろう?NoSQLだとまたなんか違うのかな?

レコードを追加する「query」

関数の抜粋

    var params = {
        // テーブル名
        TableName : 'demotable',
        Item :{
            id : 'ai',
            sortkey: '2',
            free: 'putrecord'
        }
    };
    var resultJSON = await docClient.put(params, function(err, data) {
    }).promise();

テーブル名を指定はこれまで通りで、prams内で「Item」を指定する。
なんとなくだがINSERT感があり、RDB出身者にも感覚的に分かりやすい。
正常終了した場合には、戻り値はなにも設定されない??ちょっとうまく確認できませんでした。
Itemsで複数レコードの追加ができるのかとおもったらできませんでした。他にちゃんとした方法があるのかな?

レコードを更新する「update」

関数の抜粋

    var params = {
        TableName : 'demotable',
        Key :{
            id : 'ai',
            sortkey :'2'
        },
        UpdateExpression: "set new_key = :val",
        ExpressionAttributeValues:{
        ":val" :'afterUpdate'
        }
    };
    var resultJSON2 = await docClient.update(params, function(err, data) {
    }).promise();

対象のカラムを「Key :」で指定して(RDBでいうwhere句に相当)、UpdateExpressionで更新を行う内容を記述する。
わざわざプレースホルダみたいな感じで書かなくても大丈夫かと思ったけど対象項目が増えたときとか変数で指定すること考えると、今みたいな形式の方が見やすいかもね。
私が確認した範囲では、対象カラムは一意に絞らないとエラー、存在しない項目を指定した場合は勝手に追加される。
勝手に追加してくれるのは便利だけどちゃんとしないと間違いに気づかない場合もありそう。

レコードを削除する「dalete」

関数の抜粋

    var params = {
        TableName : 'demotable',
        Key :{
            id : 'AI',
            sortkey :'2'
        }
    };
    var resultJSON2 = await docClient.delete(params, function(err, data) {
    }).promise();

ここまで、やってくるとなんとなく予想がつくようになってくる。
「Key :」で削除するレコードの条件を指定する。だけ
複数のレコードが該当する条件の場合はエラーになるが、対象のレコードが存在しない場合(例えば同じ条件で二重実行)、空振りしてるはずだが正常終了している様子。

Lambdaでのテスト実行

f:id:wantanBlog:20191030004120p:plain

対象関数画面の右上のテストボタンで実行できる。
引数をJSON形式で指定できるが、引数を気にしない場合はデフォルトのままテストを作成して関数保存後にテストボタンを押下すれば普通に実行できる。
但しテストでも正常終了した場合はDynamoDBの値は更新されるので注意。


とりあえずこんなとこまで、今後の役に立て!!!
次の記事はAWSから一旦離れる可能性もあります。