銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

DynamoDBをSQLで操作するNodeモジュール

Amazon DynamoDBSQL的な記述言語(SQL-ish)で操作できるNodeモジュールのご紹介。

f:id:takamints:20180206230625p:plain

目次

www.npmjs.com



概要

このモジュールからはDynamoDBのテーブルをSQL的な文(SQL-ish)で操作するステートメントクラスを提供しています。

最新版は v0.8.8 。SQLに小数点を含む数値を指定した時、正しく解釈できなかったバグを修正

SQL-ishなステートメントクラスは以下の4つ:

  • awsNodeUtil.dynamodb.ScanStatement - テーブルをスキャンします。
  • awsNodeUtil.dynamodb.QueryStatement - テーブルをクエリします。
  • awsNodeUtil.dynamodb.PutItemStatement - 項目を追加、またはキーを指定して上書きできます。
  • awsNodeUtil.dynamodb.DeleteItemStatement - キーを指定して項目を削除。

それぞれ、引数にSQL(みたいな文)を与えてステートメントを生成する関数です。 各ステートメントは後からパラメータを与えて実行可能です。 (※ただしPutItemStatementとDeleteItemStatementは現在パラメタライズが出来ません)

具体的な使い方は、下のサンプルプログラムをご覧ください。簡単な構文定義は更に下。

SQL-ish Statement クラスの使用例

ちょっと長いですが、4つのクラスを全部使ったサンプルコードです。 実際に動作させるには、DynamoDBにstarsというテーブルがあり、HASHキーがString型のmainStar、RANGEキーがNumber型のorbitOrderとなっている必要があります。

"use strict";
const awsNodeUtil = require("aws-node-util");
const ScanStatement = awsNodeUtil.dynamodb.ScanStatement;
const QueryStatement = awsNodeUtil.dynamodb.QueryStatement;
const PutItemStatement = awsNodeUtil.dynamodb.PutItemStatement;
const DeleteItemStatement = awsNodeUtil.dynamodb.DeleteItemStatement;
const ResultSet = awsNodeUtil.dynamodb.ResultSet;
 
// Connect (change each value for your account)
awsNodeUtil.dynamodb.connect(
//    {
//        accessKeyId: 'AKID',
//        secretAccessKey: 'SECRET',
//        region: 'us-west-2'
//    }
);
 
// Handler to print result of scan / query
function printResult(err, result) {
    if(err) {
        console.error("Error:", err.stack);
    } else {
        ResultSet.printScanResult(result);
    }
}
 
// Prepare 'PutItem' statement
var putItemStatement = PutItemStatement(
    ["INSERT INTO stars (",
        "mainStar, role, orbitOrder, name",
    ") VALUES (",
        "'SUN', 'planet', 10, 'X'",
    ")"].join(" "));
 
// Prepare 'Scan' statement
var scanStatement = ScanStatement(
        "FROM stars WHERE name=:name");
 
// Prepare 'Query' statement
var queryStatement = QueryStatement(
        "SELECT mainStar, orbitOrder, name " +
        "FROM stars " +
        "WHERE mainStar=:mainStar");
 
// Prepare 'DeleteItem' statement
var deleteItemStatement = DeleteItemStatement([
        "DELETE FROM stars",
        "WHERE mainStar = 'SUN' AND",
            "orbitOrder = 10",
        ].join(" "));
 
// Run the statements
putItemStatement.run({}, (err, resp) => {
    if(err) {
        console.error(err.stack);
        return;
    }
    scanStatement.run({ ":name": "X" }, (err, resp) => {
        console.log("-------------------");
        console.log("SCAN stars named X");
        console.log("-------------------");
        printResult(err, resp);
 
        queryStatement.run({
            ":mainStar": "SUN"
        }, (err, resp) => {
            console.log("----------------------------");
            console.log("QUERY child stars of the SUN");
            console.log("----------------------------");
            printResult(err, resp);
 
            queryStatement.run({
                ":mainStar": "EARTH"
            }, (err, resp) => {
                console.log("------------------------------");
                console.log("QUERY child stars of the EARTH");
                console.log("------------------------------");
                printResult(err, resp);
 
                deleteItemStatement.run({
                    ":mainStar": "SUN",
                    ":orbitOrder": 10
                }, (err, resp) => {
                    if(err) {
                        console.error(err.stack);
                        return;
                    }
                    scanStatement.run({
                        ":name": "X"
                    }, (err, resp) => {
                        console.log("-------------------");
                        console.log("SCAN stars named X");
                        console.log("-------------------");
                        printResult(err, resp);
                    });
                });
            });
        });
    });
});

実行時の出力例:

$ node sample/sqlish-sample.js
-------------------
SCAN stars named X
-------------------
Count: 11
ROWNUM role   name orbitOrder mainStar
     1 planet X            10 SUN
ScannedCount: 11
----------------------------
QUERY child stars of the SUN
----------------------------
Count: 10
ROWNUM name    orbitOrder mainStar
     1 MERCURY          1 SUN
     2 VENUS            2 SUN
     3 MARS             3 SUN
     4 MARS             4 SUN
     5 JUPITER          5 SUN
     6 SATURN           6 SUN
     7 URANUS           7 SUN
     8 NEPTUNE          8 SUN
     9 PLUTO            9 SUN
    10 X               10 SUN
ScannedCount: 10
------------------------------
QUERY child stars of the EARTH
------------------------------
Count: 1
ROWNUM name orbitOrder mainStar
     1 MOON          1 EARTH
ScannedCount: 1
-------------------
SCAN stars named X
-------------------
Count: 10
ROWNUM
ScannedCount: 10

SQL-ishの構文

各Statementの構文を以下にざっくり説明します。 []は省略可能を意味します。<> は後述します。

/* Scan */
[SELECT <projection-expression>]
FROM <table-name>
[WHERE <filter-expression>]
[LIMIT <limit>]

/*Query*/
[SELECT <projection-expression>]
FROM <table-name>
WHERE <key-condition-expression>
[FILTER <filter-expression>]
[LIMIT <limit>]

/*PutItem*/
INSERT INTO <table-name> ( <attribute-list> )
VALUES ( <value-list> )
[WHERE <key-condition-expression>]

/*DeleteItem*/
DELETE FROM <table-name>
[WHERE <key-condition-expression>]

SELECT句

SELECT句には<projection-expression>にカンマ区切りで選択する属性名を指定します。 通常のSQLと違ってSELECT句は省略可能です。省略時は全属性が選択されます。 (「属性」は一般的なDBでは列、またはカラムに相当します)

FROM句

FROM句は必須です。 <table-name>に検索を行うテーブル名を指定します。JOINなどは出来ません。

WHERE句

WHERE句はScanとその他のステートメントでは意味が違います。

ScanStatementでは、フィルター条件式(<filter-expression>)を指定します。これは省略可能です。省略すると全件取得。 フィルタ条件式にはキー属性を記述できません。これが必要な場合はQueryを使用します。

他のステートメントではキーの条件式(<key-condition-expression>)を指定します。 QueryStatementでは必ず記述しなければなりません。 キーの条件式には、キー以外の属性を記述できません。 QueryStatementでキー以外の属性で絞り込みたい時はFILTER句を使用します。

その他、キーの条件式には、以下のような制限もあります。これらは全てDynamoDBのKeyConditionExpressionの制限です。

  • パーティションキーの条件は必須で、 = で一致させなくてはなりません。
  • ソートキーの条件をANDで追加できます。この条件式で使用できる比較演算子=, <, <=, >, >=, BETWEEN ~ ANDだけです。INは使用できません。関数はbegins_withだけ使用できます。
  • 詳細はQuery - Amazon DynamoDB(英文)に書いてあります。

FILTER句

FILTER句は、QueryStatementにフィルタを設定するために記述します。 省略可能。省略時はフィルタはかかりません。

ScanStatementにFILTER句は指定できません(WHERE句に指定)。

LIMIT句

LIMIT句は<limit>にScanする件数(数値)を指定します。

省略可能ですが注意が必要。

LIMIT句を省略するとScanStatementではWHERE句(フィルタ)の指定に関わらず、全件をスキャンすることになります。 QueryStatementでは必ずパーティションキーの条件が入っていますのである程度件数は絞られます。 いずれにせよ項目数の多いテーブルでLIMIT句を省略すると処理速度や料金に影響しますので注意して下さい。 これはDynamoDBの仕様です。

VALUES句

PutItemStatementのVALUES句は、( <attribute-list> ) VALUES ( <attribute-list> )という形式で、 追加・上書きする項目の属性(RDBでのカラム名に相当)と値をそれぞれカンマ区切りで並べたものです。 属性と値の数は一致しなければなりません。 各値の型は表記から自動的に判断されます。

プレースホルダーについて

DynamoDBを使う上でちょっと邪魔くさい「名前のプレースホルダー」、「値のプレースホルダー」について気にする必要はありません。 本モジュール内で構文解析時に自動的に識別して変換します。

ステートメントのパラメタライズ

上の例に示しているように、パラメータ化する箇所には半角コロン:で始まる識別子を直接記述しておけば、実行時にパラメータを与えて処理できるようになります。

DynamoDBについての基本事項

DynamoDBはNoSQL

DynamoDBはAWSAmazon Web Service ― のNoSQL型データベース。 リレーショナルではありませんので複数テーブルの突き合わせ(JOIN)とかは出来ません。

DynamoDBのScanとQueryの違いについて

ScanもQueryもDynamoDBのテーブルから項目を読み込む機能ですが、読み込み方に違いがあります。

Scanはキーによる絞り込みが出来なくて、項目の並び順に順次読み込み(=スキャン)する感じ。 Queryはスキャンの前にキーの条件で絞り込めます。

どちらもスキャンしたあとに「フィルター」をかけられ、スキャンする項目数を指定できます。 また、続きから読むという操作も可能です。

あとがき

DQLというPython製ですが似たコンセプトのモジュールがあるらしいですね。 使ったことがないのでよくわからないですが、シェルから実行できる対話式のクライアントという感じでしょうか。

当npmをリリースした後「似たようなのって既にあるんじゃないの?」って検索して知ったのでワタシは無罪。 しかし、こちらが後発なのは明らかなので今後構文的にDQLに寄せていくのもありかなと思っています。BNFもドキュメントで規定されているし。