AWS LambdaからDynamoDBをQueryする
photo credit: Violet electric lightning versus turquoise storm - 09072012 via photopin (license)
JavaScript(Node.js)で記述した AWS Lambda から AWS-SDK を使って DynamoDBのテーブルをQueryするサンプルコードをご紹介。
ところでAWSのAPIのリファレンスって検索しにくいですよねえ。 Lambdaがいろんな言語に対応しているため、REST APIと各言語向けの説明が別れていてチュートリアルも検索結果にたくさん出てきます。
※ 本記事ではDynamoDBのQueryするときのパラメータを説明します。 関連する記事として、検索条件を入力してQueryパラメータを生成するWebアプリは以下になります。合わせて参照してみてください。
さらに、 DynamoDBをSQL(的な文)で扱う方法はコチラに書いています。
DynamoDBのAPIは「Welcome Amazon DynamoDB」で説明されていますが、こちらはREST APIの説明で、各言語から呼び出せるメソッドについて直接記述されているわけではありません。だから定型的な読み替えと試行錯誤と推測が必要です。
日本語に翻訳されている文書もありますが、おおかた機械翻訳のようでして、かなり不可解な表現があったりします。 はなから英語で読んだほうが理解しやすいところもあったり。
などと、愚痴ってばかりでは始まらないので、ここはひとつ怒りに任せて(笑)Node.jsのLambda関数からDynamoDBを操作するサンプルコードをご紹介。
※ ここではAmazon DynamoDBと AWS Lambdaについて、ひと通り基本的な知識を持っているって前提で書いてます。ワタシもAWS初心者なんですけどエラそうで申し訳ない。
※ フロントエンドは Amazon API Gatewayを利用している前提です。このため、実行結果はcontext.doneでJSON文字列を出力しています。
KZ-WORKS AWS関連記事
DynamoDB API Query - キー属性で項目を検索
DynamoDBのテーブルから検索条件に合致した項目を問い合わせるためには、Query を使用します。条件無しで全項目を得るには、Queryではなく Scan を使用します。
Queryの検索条件には、少なくともプライマリーキーの条件が指定されなくてはならず、これに加えて、プライマリーソートキーやセカンダリーインデックスの条件も AND / OR で連結して記述できます。
さらに、プライマリーキー以外の項目によるフィルター条件も指定できますが、これは、あくまでも上記のプライマリーキーによる検索結果を得た後に対して絞りこむものですから、クエリ自体のパフォーマンスには、ほぼ無関係でしょう。(レスポンスが少なくなるので、通信速度には影響すると思われます)
サンプルコード(Node.js)
以下のように少しややこしい。初めて見たら軽くめまいを覚えそうです。
var AWS = require('aws-sdk'); var dynamo = new AWS.DynamoDB({ region: 'ap-northeast-1' }); var tableName = "myTable"; exports.handler = function(event, context) { //パラメータ定義 var params = { "TableName": tableName, //キー、インデックスによる検索の定義 "KeyConditionExpression": "id = :Id AND #Ts BETWEEN :T0 AND :T1", //プライマリーキー以外の属性でのフィルタ "FilterExpression": "#Fn > :Fn", //属性名のプレースホルダの定義 "ExpressionAttributeNames": { "#Ts": "timestamp", "#Fn": "value.floor" }, //検索値のプレースホルダの定義 "ExpressionAttributeValues": { ":Id" : { "S" : event.id }, ":T0" : { "S" : event.T0 }, ":T1" : { "S" : event.T1 } ":Fn" : { "S" : event.Fn } } }; //クエリ実行 dynamo.query(params, function(err, data) { if (err) { context.fail( new Error("Fail. err:" + err)); } else { var response = JSON.stringify(data); context.done(null, response); } }); };
ポイントは、params
。
ここではテーブルの指定以外に以下の3つのことをやっています。
以降で、これらパラメータについて、詳細に説明します。
パラメータ
パラメータは第一引数に、objectとして与えられます。 下表に、ここで使用しているパラメータの概要です。
No. | キー | 型 | 説明 |
---|---|---|---|
1 | TableName | string | テーブル名を指定する。 |
2 | KeyConditionExpression | string | キー属性での検索定義 |
3 | ExpressionAttributeValues | object | 検索値のプレースホルダを定義する。 |
4 | ExpressionAttributeNames | object | 属性名のプレースホルダを定義する。 |
5 | FilterExpression | string | 検索後のフィルター条件を記述します。 |
- 1~3は必須のはずです。
APIの説明では
TableName
だけ必須と書いてありますが、QUERYでは少なくともKeyConditionExpression
とExpressionAttributeValues
を指定しないといけません。 - 「4. ExpressionAttributeNames」は、検索する属性名に予約語が使われている場合に、読み替えを行うため必要となるパラメータ。
- 他のパラメータについては、Query - Amazon DynamoDBで御確認ください。
KeyConditionExpression - キー、インデックスによる検索を定義する
KeyConditionExpressionではテーブル作成時に指定したキーによる検索条件を記述します。キー以外の条件は使えません。キー以外で絞り込みたい場合は、フィルターを使用する必要があります(フィルターはキーに関する制約を除けばKeyConditionExpressionと同じように条件を記述します)。
DynamoDBのテーブルのキーはパーティションキーとレンジキーの二種類があり、このうちパーティションキーは必ず検索条件に指定しなければなりません。レンジキーはオプションです。
検索条件の値は直接記述できません。値は検索値のプレースホルダを使って記述しなければなりません。
検索値のプレースホルダは、:
(コロン)で始まる名称です。値に対する仮引数みたいなものですね。
具体的な値をプレースホルダに紐づけるには、次項で説明する ExpressionAttributeValues
を使用します。
以下の :placeholderName
が検索値のプレースホルダ。
{ "KeyConditionExpression": "keyAttrName = :placeholderName", }
KeyConditionExpression についてまとめると以下のようになります。
- キーによる検索条件を記述しなければならない。
- パーティションキーは必ず指定しなければならない。
- 値は検索値のプレースホルダで指定する。
- キーの属性名がDynamoDBの予約語である場合は、属性名のプレースホルダを使用して記述しなくてはならない(予約語でなくても使用可)。
ExpressionAttributeValues - 検索値のプレースホルダを定義する
ExpressionAttributeValuesには、KeyConditionExpressionやFilterExpressionに記述されているプレースホルダの値を型とともに指定します。
{ "KeyConditionExpression": "keyAttrName = :placeholderName", "FilterExpression": "attrName > :placeholderName2", "ExpressionAttributeValues": { ":placeholderName": { "S" : "11" }, ":placeholderName2": { "N" : "12" } } }
※ チュートリアルでは値だけが指定されていますが「structureじゃないとダメですよ」という例外が投入されます。
ExpressionAttributeNames - 属性名のプレースホルダを定義する
DynamoDBのテーブルの属性名に予約語が使われている場合、検索条件を記述できません(「属性名がキーワードだ」との例外投入) ので、ExpressionAttributeNamesで置き換えます。
以下の例では、予約語timestampと同じ属性名を、読み替えています。(#Tm
の部分)
"KeyConditionExpression": "#Tm = :T0", "ExpressionAttributeNames": { "#Tm": "timestamp" }, "ExpressionAttributeValues": { ":T0" : { "S" : "" + event.t0 } }
他のAPI
テーブルの項目を操作するAPIは以下の様なものがあります。
- 項目の問い合わせ - Query
- 全項目取得 - Scan
- 特定項目を取得 - GetItem
- 項目上書き - PutItem
- 項目変更 - UpdateItem
- 項目削除 - DeleteItem
- バッチ項目取得 - BatchGetItem
バッチ項目更新 - BatchWriteItem
PutItemとUpdateItemは紛らわしいけど、上書きしてしまうか、特定の属性の変更が行えるかの違いらしい(詳細不明)
- バッチ系はちょっと使い勝手が違うかもしれませんが、一度にやっちゃう感じでしょう(詳細不明)
- その他、テーブルを操作するAPIは DynamoDBのAPIの説明ページ に書いてあります。
AWS関連記事
- DynamoDB Query APIのパラメータを生成しましょ(プレースホルダをぶっ飛ばせ) - 銀の弾丸
- DynamoDBをSQLで操作するNodeモジュール - 銀の弾丸
- DynamoDB:条件式のプレースホルダを自動生成してみましょう - 銀の弾丸
- AWS Lambdaの関数アップロードをお手軽に - 銀の弾丸
Node.js / npm 関連記事
- package-lock.jsonの潜在的セキュリティ脆弱性を解消しました - 銀の弾丸
- WHATWG Fullscreen API を仕様通りに使えるモジュール「fullscrn」 - 銀の弾丸
- SVGの重なり順序をJavaScriptで制御する「svg-z-order」 - 銀の弾丸
- npm「list-it」― コンソールへ列を揃えてデータを表示 - 銀の弾丸
- npm 「hash-arg」 ― コマンドラインパラメータに名前でアクセス - 銀の弾丸
その他、一般事項
Node.jsから呼び出すAPIのメソッド名
DynamoDBのAPIの説明ページ に掲載されている API の名称をキャメルケースに変換すれば、Node.jsのメソッド名になると思います(多分)。
DynamoDB APIの基本形
Node.jsでのDynamoDB APIは、以下のような形式で呼び出します。
dynamo.apiName(params, function(err, data) { if(err) { //エラー; } else { //dataにAPIの実行結果が入っています。 } });
(↑)apiNameをAPIの名前に書き換え。
JavaScriptのWebWorkerでスレッド間のリモートプロシジャコール(RPC)を実装する
photo credit: Pallet via photopin (license)
ブラウザのJavaScriptでWebアプリをマルチスレッド化できる Web Workers API の基本と、Worker側を使ってスレッド間でのリモートプロシジャコールを実装して、ワーカースレッドのメソッドを呼び出して戻り値をコールバック関数で受け取れるようにする方法を書いています。
- Workerの基本1:ワーカースレッドとの通信
- Workerの基本2:スレッド間メッセージはコピーされる
- しかしメソッド(関数オブジェクト)はコピーされない(送信できない)
- 同じくクラスオブジェクトのメソッドも無理ですよ
- コピーではなく参照を渡す方法もある(らしい)
- スレッド間リモートプロシジャコール(RPC)の実装
- npm transworkerのご紹介
- おまけ1:スクリプト内でコンテキストを判断するには?
その他ワーカーについては、MDNの「Web Worker を使用する」を参考にしてください。
- 以降で使用しているWorkerはDedicatedWorker (専用ワーカー)というもので、サブスレッドを生成したスレッドと一対一の通信を行えるものです。他にSharedWorkerというのもあります。
カットシステム
売り上げランキング: 481,974
Workerの基本1:ワーカースレッドとの通信
メインスレッドとワーカースレッドの間での通信は、メッセージを使用します。
メインスレッド側スクリプト:
var wkr = new Worker("dedicated/worker/thread.js"); //ワーカーへメッセージ送信 var data = {key1:value1, ... }; wkr.postMessage(data); //ワーカーからのメッセージを受信 wkr.onmessage = function(e) { console.log("main thread onmessage e:" + JSON.stringify(e)); }
ワーカー側スクリプト:
//メインスレッドのメッセージを受信する onmessage = function(e) { console.log("worker thread onmessage e:" + JSON.stringify(e)); } //メインスレッドへメッセージを送信する。 var data = {key1:value1, ... }; postMessage(data);
簡単ですね。
メインスレッドでは、生成したワーカーのpostMessageとonmessageで、送受信が行えます。
ワーカー側では、グローバルコンテキストでpostMessageとonmessageを使用できます。
Workerの基本2:スレッド間メッセージはコピーされる
postMessage ~ onmessageでやり取りされるメッセージは、コピーされます。 スレッド間で同じオブジェクトを共有していないため、それぞれ安心して自由に使用できます。 (ただし、特別な方法でオブジェクトの参照を渡すことも可能(↓)です。)
しかしメソッド(関数オブジェクト)はコピーされない(送信できない)
少なくともChromeでは関数オブジェクトを送信できませんでした。 他のブラウザでは試していませんが、多分出来ないんじゃないかな。 関数オブジェクトを一旦文字列化してからevalすればよいと思われますが、パフォーマンス上は望ましくないと思います。
むしろ、関数オブジェクトを送信しなくてもよいように設計するべきだと思います。
同じくクラスオブジェクトのメソッドも無理ですよ
関数オブジェクトが送信できないので、当然のようにクラスオブジェクトのメソッドも送信できません。
しかし、データメンバーは送信されます。
送信できないオブジェクトをシリアライズするときに、あえて排除しているのだとすると、 パフォーマンス的にデメリットがあるかもしれません。 特に詳しく確認していませんが、そのうち調べてみようとは思っています。
コピーではなく参照を渡す方法もある(らしい)
Transferableインターフェースを実装するデータは、 コピーではなくオブジェクトそのものを送信できるようです。(⇒所有権の譲渡 (Transferable Objects) によるデータの引き渡し|MDN)) ただし、送信元では、それ以降そのオブジェクトを使えなくなります。文字通り渡してしまうようなイメージになります。
// 32MB の "file" を作成して埋めます。 var uInt8Array = new Uint8Array(1024*1024*32); // 32MB for (var i = 0; i < uInt8Array.length; ++i) { uInt8Array[i] = i; } worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
上のコードは、MDNのサンプルをそのまま引用しています。postMessageの引数が違っていますね。 第2引数は配列で、その要素は第一引数ということのようですが、これに実質的にどんな意味があるのかはよくわかりません。あまり直感的でないよう・・・。
- Transferableインターフェースを実装しているクラスは、
ArrayBuffer
とMessagePort
だけとのことです。
スレッド間リモートプロシジャコール(RPC)の実装
これまで書いたように、スレッド間メッセージは、それぞれが好きなときに送信でき、非同期で処理されますが、関数オブジェクトは転送できません。 そして、スレッド間のリモートプロシジャコールの機構も提供されていないようなので自前で用意しなくてはならないようです。
以下のフローは、サブスレッドで定義されるメソッドを呼び出し、戻り値をコールバックで受け取る処理を示しています。
(要求元)
1. 送信する要求メッセージとコールバック関数のセットに対して、新たなIDを割り当てます。
2. 送信元で、このコールバック関数を、IDをキーとしたハッシュに保存
3. 送信先へ要求メッセージとIDを送信
(ワーカー側)
4. メッセージを受信し、要求に対する応答メッセージを生成
5. 受信メッセージ内のIDと応答を返却
(再び要求元)
6. 応答を受信し、そこに含まれるIDでコールバックを検索。
7. 検索したコールバックのパラメータに応答データを指定して呼び出します。
npm transworkerのご紹介
上記フローを実装したモジュール transworker をnpmで公開しています。 概要は下図参照。詳細はREADMEに書いていますので、よろしくどうぞ。
TransWorkerの概念図
TransWorkerクラスは、下図(?)のように、シングルスレッドでのクラスメソッド呼び出しモデルの間に入って、そのクラスの処理を丸ごとサブスレッド側へ持っていくことです。 メソッドの戻り値が、コールバックで得られるということを覚えておけば、利用者側は細かいことを気にする必要はありません。
Single thread model =================== +-----------+ call methods +-----------+ |Application|---------------------------->|ClientClass| +-----------+ +-----------+ ^ | | return | +-----------------------------------------+ Multi thread model(using TransWorker) ===================================== | Main (UI) thread | Web Worker (sub) thread | call methods | call methods | | | | +---------------+ | | | message | | | | ------> | | +-----------+ V | | V +-----------+ |Application|----->| TransWorker |----->|ClientClass| +-----------+ | | +-----------+ ^ | message | | | | <------ | | | +---------------+ | | | | ^ | | callback | | | return | +---------------+ | +---------------+ |
おまけ1:スクリプト内でコンテキストを判断するには?
上のクラスでやっているように、グローバルスコープの this.constructor.name
でコンテキストが判断できます。
これが、'Window'
なら、ブラウザのUI-スレッド(メインスレッド)で読み込まれていることを示しており、'DedicatedWorkerGlobalScope'
なら、Workerだということです。
おまけ2:関数定義は常にグローバルスコープなんだな
Worker無関係ですが、グローバルコンテキストで処理を振り分け、ThenとElseブロックの両方で、同名の関数定義を行うと、後に定義された関数しか参照できません。
説明が分かりにくいって人、ごめんなさい。以下のスクリプトを見てください。
var context = this.constructor.name; if(context == 'Window') { function ComWorker() { console.log("ComWorker for UI-thread"); } } else if(context == 'DedicatedWorkerGlobalScope') { function ComWorker() { console.log("ComWorker for Worker-thread"); } }
このスクリプトをメインスレッド側で読み込んだ場合でも、new ComWorker()
を実行すると、コンソールに、"ComWorker for Worker-thread"
が表示されていました。
関数定義の評価はスクリプト読み込み時に処理されて、グローバルコンテキストに定義されるんでしょうね。勘違いしやすいので気をつける。
あとがき
ブラウザで動作する往年の8ビットマイコン MZ-700のエミュレータを作っており、 当初、インターバルタイマーを使って単一スレッドで実装したら速度が遅くて不安定。ブラウザが操作不能に陥ったりしていました。
そこで、小耳に挟んだWeb Workers APIを調査開始。 ワーカースレッド側でタイマーを使えば動作が安定。マシンスペックに応じて可能な限り高速に動作するようになりました。
現在エミュレータでは、Transferableインターフェースでデータを移譲する方法を使用していませんが、 サブスレッド側でクローンしてからメイン側へ移譲するなどすれば、 ある程度の速度向上に役に立ちそう。
関連サイト:
C++の参照型の落とし穴:クラスメンバに参照型は使わないほうが良さそうだ
先日来、C++のデストラクタで、おかしな動きにぶち当たり「おかしい!バグか?」と大騒ぎした後、最終的に自分のミスに気がつきました。
タイトル通り、クラスにおける参照型に関する落とし穴です。
経緯とともにホントにお恥ずかしい限りですが、忘れた頃に同じことを繰り返しそうなので、恥を忍んで書いておきます。
photo credit: The Arm of Destruction via photopin (license)
経緯はこう
- クラスのインスタンスメンバに参照型を使ったシンプルなクラスを定義。
- そのメンバをデストラクタで使っていたが正しく動作してないようだ。
- 「デストラクタが動いてないってどういうこと?!」と大騒ぎ(←最初の勘違い)
- 落ち着いて、確認用のコードを書いてみたら、デストラクタは動いていた
- しかしデストラクタ内では、そのメンバの値が正しくない。コンストラクタでは正しかったが?
- 「おいおいデストラクタで参照型が使えないってどういうこと?!」と大騒ぎ(←2つめの勘違い)
- ここまで全てVisual C++で動かしていたけど、G++では想定通りに動いていた。
- 「ほうらやっぱり Visual Studio おかしいぞ!」と大声出したら急に自信がなくなって、、、
- じっくりコードを眺めてみたら、結局コンストラクタに単純ミスが。。。(自分か)
これが問題のクラス定義だ
スレッド間での排他処理のために、以下の様なクラスを定義したのです。
class MutexLocker { HANDLE& mutex; public: MutexLocker(HANDLE mutex) : mutex(mutex) { WaitForSingleObject(mutex, INFINITE); } virtual ~MutexLocker() { ReleaseMutex(mutex); } };
コンストラクタでミューテックスをロックして、そのスコープから抜けるときにデストラクタで開放するというものですね。 (※ Visual Studio 2012以降では、スレッド間排他処理のために、CriticalSectionというクラスがあるようですので、そちらを使うほうが良いらしい。それ以前のバージョンではCRITICAL_SECTION構造体を使用。Mutexはプロセス間でも排他処理が可能なロックオブジェクトです)
ところが
以下のように使用しても、ミューテックスが開放されません。 デストラクタ内ではmutexを正しく参照できなくて、どこの馬の骨ともしれないHANDLEを開放している。 なぜだ。
class PacketLogger : Thread { private: HANDLE mutex; std::deque<Packet*> packet_queue; public: // ・ // ・ // ・ void AddPacket(Packet* p) { MutexLocker lock(mutex); //←ココ { packet_queue.push_back(p); } } // ・ // ・ // ・ }
こうすればちゃんと動いていた
デストラクタで使用するメンバを参照でなく実体にすれば問題なかった
class MutexLocker { //HANDLE& mutex; //←参照でなく HANDLE mutex; //←実体に public: MutexLocker(HANDLE mutex) : mutex(mutex) { WaitForSingleObject(mutex, INFINITE); } virtual ~MutexLocker() { ReleaseMutex(mutex); } };
確認コードを書いたはいいがむしろ勘違いを補強する
確認用に以下のコードを書いてみて、Visual Studio の3つのバージョン、2010、2013、2015で確認しても(2012は手元にない)、結果はすべて同じでした。DtorTestClassA のデストラクタでは、コンストラクタで出力した値を出力できない。 最初に書きましたが、MinGW上のG++では、想定通りに動いていましたので、「VisualStudioでは、デストラクタで参照が壊れている」と考えちゃって大騒ぎ。
#include <iostream> class DtorTestClassA { const int& number; public: DtorTestClassA(int number) : number(number) { std::cerr << "construction DtorTestClassA #" << number << std::endl; } virtual ~DtorTestClassA() { std::cerr << "destruction DtorTestClassA #" << number << std::endl; } }; class DtorTestClassB { const int number; public: DtorTestClassB(int number) : number(number) { std::cerr << "construction DtorTestClassB #" << number << std::endl; } virtual ~DtorTestClassB() { std::cerr << "destruction DtorTestClassB #" << number << std::endl; } }; int main(void) { DtorTestClassA a1(1); DtorTestClassB b1(1); { DtorTestClassA a2(2); DtorTestClassA b2(2); } for(int i = 0; i < 3; i++) { DtorTestClassA a3(i + 3); DtorTestClassA b3(i + 3); } }
「Visual Studio のバグだろコレ!」って、んなわけないし。
ここで再び、じっくりコードを見なおしてみると、、、別のところがおかしいよと。
class MutexLocker { HANDLE& mutex; public: MutexLocker(HANDLE mutex) : mutex(mutex) { WaitForSingleObject(mutex, INFINITE); } virtual ~MutexLocker() { ReleaseMutex(mutex); } };
「コンストラクタおかしくないかい?」と。
コンストラクタの引数リストが、参照型になってないよと。
以下のようになっていないとダメじゃないかと?
class MutexLocker { HANDLE& mutex; public: MutexLocker(HANDLE& mutex) : mutex(mutex) { //←ココだ WaitForSingleObject(mutex, INFINITE); } virtual ~MutexLocker() { ReleaseMutex(mutex); } };
元のコードでは、メンバがコンストラクタのパラメータを参照していて、それってつまりコンストラクタの処理が終われば消滅しているオブジェクトですがな。そらあかんわ。
まとめと所感・教訓および反省文
検証大切
最初のコードを見た段階で気付ける人は幸せです。私は無理でしたが。 気付けないのはまあ、アタマの程度の問題なので仕方がないけど、「ああ、こういうことなんだろうな?」という推測で進んだのがダメダメですな。お恥ずかしい。
人って基本的に、他者を疑うようになっていると思うけど、疑念を公言する前に各方面から確実に検証しなければ、こんな恥ずかしいことになるって事例です。大反省。
未来永劫クラスメンバで参照型は使わない
絶対にそうしなくちゃならないという理由なく、クラスメンバに参照型は使わないほうが良いかもしれないですね。
対象が構造体やクラスオブジェクトの場合はポインタで、完全になんの問題もなく代用可能ですから、そういう理由は今のところ見当たりません。
そもそもなんで最初に参照型にしたのかというと、明確に「そうしなくては」と思ったわけではなく「そのオブジェクトの生成と消滅には直接関わりたくない」という感覚的なものだった。
ところでG++の実装や如何に?
それから、むしろ、G++の動きのほうが怪しいという結果になったけど、もしかすると気を利かせて、こちらの意図に沿うように解釈してくれていたのかも? いや、単にスタックフレームの使い方が違うのかもしれませんね。
無視しないから警告出してよ
いやしかし、自分のミスを棚に上げてでも言っておきたいのだが、クラスメンバがコンストラクタのパラメータを参照しちゃっているのは明らかに間違いなんだから、せめてコンパイル時に警告出してほしいわ。静的解析したら警告なのかな?というか出てた?(←これがよくない)
んなわけないし→デストラクタで参照型のメンバ変数が使えない?
この記事無効です。自分の単純ミスでした。
売り上げランキング: 28,637
気を取り直して自分のミスの暴露記事は下記参照。何卒よろしくお願いいたします。
photo credit: The Arm of Destruction via photopin (license)
大間違い→デストラクタが自動的に呼ばれない
この記事無効です。自分の単純ミスでした。
売り上げランキング: 28,637
気を取り直して自分のミスの暴露記事は下記参照。何卒よろしくお願いいたします。
photo credit: The Arm of Destruction via photopin (license)