【Node.js】LambdaからDynamoDBテーブルをScanするコードの書き方【前編】

LambdaからDynamoDBに格納されたテーブル情報を取得する方法について、関数の作成からコードの記述方法までを説明します。
なお、Lambda関数はNode.jsで記述します。

後編ではテーブル情報の取得条件を設定する「FilterExpression」について深掘りしていますので、こちらもご確認ください。

https://qiita.com/neko_tech/private/61849889d8e30391162c

<h2>Lambda関数を作成する</h2>

以下の手順でLambda関数を作成します。

<h3>Lambdaのホーム画面で、「関数の作成」を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3137310/5067aadb-b0b4-3d29-c383-2c3bd5b68292.png)

<h3>各種設定を行う

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3137310/61ee0032-d0c1-52e1-0ee3-9d023c14259a.png)

<h4>関数の作成方法の選択

関数の作成方法には、3つの作成方法があります。今回は、「一から作成」を選択します。

<h4>基本的な設定</h4>

- 関数名：関数の名前を設定します。自由に設定してください。
- ランタイム：関数に記述する言語を選択していきます。今回はNode.jsを選択します。
- アーキテクチャ：関数の実行に使用するコンピュータプロセッサのタイプを選択します。
デフォルトのx86_64を選択します。
- アクセス権限:特に設定しません。
- 詳細設定：特に設定しません。

<h1>DynamoDBテーブルを作成する</h1>
今回は、料理検索を行うサイトを想定して料理テーブルを作成します。
条件は以下の通りです。

- テーブル名：dish
- 列：料理ID(dish_id)、料理名(dish_name)、値段(dish_price)

料理テーブル(dish)
|dish_id|dish_name|dish_price|
|--:|--:|--:|
|1|カレー|100|
|2|親子丼|300|
|3|ペペロンチーノ|200|
|4|麻婆豆腐|300|
|5|牛丼|150|

<h1>関数のソースコードを記述する</h1>

本章では、lambdaに記述するソースコードについて説明します。

<h2>(補足)scanとqueryの違いについて</h2>

lambda関数からDynamoDBテーブルを検索する方法は2つ存在します。

- scan：指定したテーブルのレコードを全件取得し、検索条件に合致したレコードのみを抽出
- query：主キーを選択して1件のレコードを抽出

ネット上にはqueryに関する記事が多いのですが、複雑な条件で複数件のレコードを取得できるscanの方が便利なので今回はscanに絞ってご紹介します。

#### データ全件取得

まずは全件取得、つまり、dishテーブルの全レコードを取得します。

```js    
const aws = require('aws-sdk'); //①
const dynamo = new aws.DynamoDB.DocumentClient(); //②

exports.handler = async (event) => {
    let res = await dynamoscan(event);
    return res;
}; //③

function dynamoscan(event) {
    return new Promise((resolve, reject) => { //⑤

        let params = {
            "TableName": "dish",
        };

        dynamo.scan(params, function(err, data) {
            if (err) {
                reject(err);
            }
            else {
                resolve(data.Items);
            }

        });
    });
}
```
<br>

それぞれの行について役割を解説します。

```js
const aws = require('aws-sdk');
```

AWSサービスをプログラムから操作するために必要です。

```js
const dynamo = new aws.DynamoDB.DocumentClient();
```

DynamoDBにアクセスするためのオブジェクトを作成しています。

```js
exports.handler = async (event) => {
    let res = await dynamoscan(event);
    return res;
};
```
lambda関数を実行した際に一番最初に実行するようにするメソッドです。
今回の場合は、dynamoscanという関数を実行し、戻り値を返します。

dynamoscanの処理は以下のようになります。
```js
    return new Promise((resolve, reject) => {
        let params = {
            "TableName":"food",
            "ReturnConsumedCapacity": "TOTAL"
        };
        
        dynamo.scan(params,function(err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(data.Items);
            }
        });
    });

```

paramsの中身を除き、お約束だと考えてかまいません。
それぞれ説明を説明すると以下のようになります。

- return new Promise((resolve, reject) => {
プロミスという記述方法を使用しています。詳しいことはさておき、resolveまたはrejectの引数が戻り値になります。
-  let params = {
paramsという変数にscan時の検索条件を設定します。dynamo.scanを呼び出す際の引数として設定します。
- dynamo.scan(params,function(err, data) {
paramsに格納された値に基づいてDynamoDBテーブルのscanを行います。
正常終了した場合はdataという変数のItemsを返します。
異常終了した場合はエラーの内容を返します。
※どちらもjson形式

#### 一致検索

続いては一致検索について説明します。dishテーブルの指定したdish_idを持つレコードを取得します。

```js
const aws = require('aws-sdk'); //①
const dynamo = new aws.DynamoDB.DocumentClient(); //②

exports.handler = async (event) => {
    let res = await dynamoscan(event);
    return res;
}; //③

function dynamoscan(event) {
    return new Promise((resolve, reject) => { //⑤

        const params = {
            TableName: 'dish',
            KeyConditionExpression: '#id = :value', //①
            ExpressionAttributeNames: { //②
                "#id": "dish_id"
            },
            ExpressionAttributeValues: { //③
                ":value": Number(event.dishId)
            }
        };

        dynamo.scan(params, function(err, data) {
            if (err) {
                reject(err);
            }
            else {
                resolve(data.Items);
            }

        });
    });
}
```

paramsの設定内容を除けばデータ全件取得と変わりませんので、paramsの中身のみご説明します。

```js
KeyConditionExpression: '#id = :value',
```

scan時の抽出条件です。"#id"と":value"が一致した場合のみ値を抽出します。
"#id"と":value"はバインドされており、以下のように定義します。

```js
ExpressionAttributeNames: {"#id": "dish_id"}
```

"ExpressionAttributeNames"は'#'から始まるバインド値を定義します。
今回の場合、検索対象の列名をバインド値に格納しています。

```js
ExpressionAttributeValues: {":value": Number(event.dishId)}
```
"ExpressionAttributeValues"は':'から始まるバインド値を定義します。
今回の場合、検索する値をバインド値に格納しています。

#### 範囲検索
料理テーブルから、指定した金額の範囲内の料理を抽出します。

```js
const aws = require('aws-sdk'); //①
const dynamo = new aws.DynamoDB.DocumentClient(); //②

exports.handler = async (event) => {
    let res = await dynamoscan(event);
    return res;
}; //③

function dynamoscan(event) {
    return new Promise((resolve, reject) => { //⑤

        const params = {
            TableName: "dish", //テーブル名を指定
            FilterExpression: "#dish_price between :minPrice and :maxPrice", //①
            ExpressionAttributeNames: {
                "#dish_price": "dish_price",
            },

            ExpressionAttributeValues: {
                ":minPrice": Number(event.minPrice),
                ":maxPrice": Number(event.maxPrice)
            }
        };

        dynamo.scan(params, function(err, data) {
            if (err) {
                reject(err);
            }
            else {
                resolve(data.Items);
            }

        });
    });
}
```

今回もparams以外は特に変わりませんので、paramsの値のみを解説します。

```js
 FilterExpression: "#dish_price between :minPrice and :maxPrice",
 ExpressionAttributeNames: {
     "#dish_price": "dish_price",
 },
 ExpressionAttributeValues: {
     ":minPrice": Number(event.minPrice),
     ":maxPrice": Number(event.maxPrice)
 }
```
between_functionを使用することにより、"#dish_price"に格納した列の値が、
":minPrice"から":maxPrice"の範囲にあるレコードを検索します。
今回は、dishテーブルの"dish_price"列の値が、"event.minPrice"以上かつ、"event.maxPrice"以下のレコードを検索しています。
"ExpressionAttributeValues"では、2つのバインドを解決しています。

#### 部分一致検索
「～を含む場合」で検索します。
今回は例として、料理名に「丼」を含むレコードをdishテーブルから検索します。

```js
var aws = require('aws-sdk');
var dynamo = new aws.DynamoDB();

exports.handler = async (event) => {
    let res = await dynamoscan(event);
    return res;
};

function dynamoscan(event) {
    return new Promise((resolve, reject) => {
        let params = {
            TableName: "dish",
            FilterExpression: "contains(#dish_name, :dishName)",
            ExpressionAttributeNames: {
                "#dish_name": "dish_name",
            },
            ExpressionAttributeValues: {
                ":dishName": { "S": event.dishName }
            },
        };

        dynamo.scan(params, function(err, data) {
            if (err) {
                reject(err, err);
            }
            else {
                resolve(data.Items);
            }
        });
    });
}
```

以降、コードを説明していきます。

```js
var aws = require('aws-sdk');
var dynamo = new aws.DynamoDB();

exports.handler = async (event) => {
    let res = await dynamoscan(event);
    return res;
};
```
全件検索と同じくおまじないのなので脳死でコピーしてください...と言いたいところですが、
1ヶ所だけ異なります。
2行目の末尾が"DynamoDB.DocumentClient()"ではなく、ただの"DynamoDB()"です。
このように記載した場合、文字の前に属性をつけなければいけません。
例えば、以下のように記載することになります。
```js
"N": event.minPrice
"N": event.maxPrice
"S": event.dishName
```
もし仮に"DynamoDB.DocumentClient()"と記載した場合、
```js
event.minPrice
event.maxPrice
event.dishName
```
これだけで良いので、基本的には"DynamoDB.DocumentClient()"を使うと良いです。
今回は紹介のために"DynamoDB()"を使用しました。

```js
let params = {
    TableName: "dish",
    FilterExpression: "contains(#dish_name, :dishName)",
    ExpressionAttributeNames: {
        "#dish_name": "dish_name",
    },
    ExpressionAttributeValues: {
        ":dishName": { "S": event.dishName }
    },
};
```
こちらはscanの条件を指定しています。
paramsをdynamo.scanの引数として渡します。
それぞれの値は以下の通り。

- TableName：検索対象のテーブル名。dishテーブルをscanの対象とする
- FilterExpression：#dish_nameという列に:dishNameという値が含まれているレコードを検索
なお、#dish_nameと:dishNameの部分はバインド必須
- ExpressionAttributeNames：('#'から始まる)バインド値の設定
- ExpressionAttributeValues:(':'から始まる)バインド値の設定
先ほど説明した通り、今回の設定では値の前に属性をつける必要がある("S": のこと)

その他の箇所は上記と違いありません。

<h1>後編へ続く</h1>
lambda関数からDynamoDBテーブルのレコードを取得する方法についての説明は以上です。

しかし、より自由に、複雑な検索条件を指定したい場合は、FilterExpressionについて理解を深める必要があります。

これからは少し発展的な内容になりますので、ご興味のある方は後編をご覧下さい。

https://qiita.com/neko_tech/private/61849889d8e30391162c

<h1>参考文献</h1>
<a herf="https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html">AWS Lambda の概要</a>

<a herf="https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Introduction.html">Amazon DynamoDB とは</a>
<a herf="https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/welcome.html">AWS Cloud9 とは?</a>