こんにちは。無事AZ-900に合格してホッとしている木村です。
今回は、Azure Functionsの入力バインドでCosmos DBを使う際に、SqlQueryでちょっとはまったところを解決した手順を書こうと思います。
バインドとは
Azure Functionsは、設定されたイベントトリガーに基づいて起動される小さなプログラム(関数:Function)を実行するための基盤を提供する、FaaS(Function as a Service)です。 Azure Functionsには、関数アプリを起動するためのトリガー(例えばHTTPアクセスやEventGridからのイベントなど)とは別に、関数への入出力として「バインド」というものを設定できるという便利な機能があります。
これは、関数アプリと別のサービスを紐付ける時、サービスに接続してデータを入出力する部分を自前で関数内にコーディングせず、より抽象化して扱えるようにするための機能です。 サービスに接続してAPIを叩いてデータの入出力して・・と関数内にコードを書かないでも、変数への読み書きをするとそれがそのまま接続先のサービスへの入出力になるので大変便利です。
SqlQueryとは
Cosmos DBの入力バインドでは、トリガーに含まれるパラメータでドキュメントIDを指定して、該当する1つのドキュメントを取得して関数に渡す機能と別に、より複雑な検索を行って複数のドキュメントを渡す「SqlQuery」という機能があります。
前者の場合、functions.jsonでのバインドの記述例は以下の通りです。
{ "name": "inputDocument", "type": "documentDB", "databaseName": "MyDatabase", "collectionName": "MyCollection", "id" : "{tirigger_param_id}", "partitionKey": "", "connection": "MyAccount_COSMOSDB", "direction": "in" }
このように書いておくと、HTTPトリガーであればパラメータ「trigger_param_id」で渡された値、EventGridなどでJSONで渡るのであればJSONの中の「trigger_param_id」の値がドキュメントIDであるようなものが検索され、その値を変数にセットした状態で関数アプリが呼び出されます。
これに対し、例えば「ドキュメントの departmentIdという属性の値がパラメータの値と等しいドキュメントを複数取得したい」というような場合は
{ "name": "inputDocument", "type": "documentDB", "direction": "in", "databaseName": "MyDatabase", "collectionName": "MyCollection", "sqlQuery": "SELECT * from c where c.departmentId = {trigger_param_id}", "connection": "CosmosDBConnection" }
と書きます。 SqlQueryで記載するSQLではテーブル名を指定する必要がありますが、この名前は特にコレクション名(上記だとMyCollection)と一致する必要は無く、慣例的に「c」が使われることが多いようです。ここはCosmos DB explorerでの検索と同じですね。
なお、ここで1点注意なのですが、SqlQueryとidの指定は排他であり、両方が指定されていると関数アプリの起動時にエラーとなります。指定しない、というのは「値がない」というのではなく、「function.jsonに現れない」ことが求められます。
ブラウザでポータルにアクセスして、「統合」の画面から設定すると、idの値を指定しなくても以下のようなfunction.jsonが生成され、このままだと実行時エラーになります。そのため、コードの編集画面から明示的にidの行を削除してください。
{ "name": "inputDocument", "type": "documentDB", "direction": "in", "databaseName": "MyDatabase", "collectionName": "MyCollection", "id":"", // ←この行が不要 "sqlQuery": "SELECT * from c where c.departmentId = {trigger_param_id}", "connection": "CosmosDBConnection" }
JSONの特定のパラメータで検索したい
上記の検索のパラメータですが、入力が複雑なJSONで、その特定のパラメータで検索したいと言うことはあるかと思います。 例えばIoT Hubに登録した「myDevice」というデバイスからのメッセージをEventGridを経由して関数アプリを起動する場合、渡されるJSONを抜粋するとこんな感じになってます。
{ "id":"....", "topic":"............", "subject":"devices/myDeviceId", "eventType":"Microsoft.Devices.DeviceTelemetry", "data":{ "properties":{ ...}, "systemProperties":{ "iothub-connection-device-id":"myDeviceId", "iothub-connection-auth-method":"{ ..... }, "iothub-connection-auth-generation-id":"XXXXXXXXXXXXXXX", "iothub-enqueuedtime":"yyyy-mm-ddThh:hi:ss.sssZ", "iothub-message-source":"Telemetry" }, "body":"xxxx"; }
メッセージを送ってきたデバイスのID(myDeviceId)は、渡されたJSONのdata.systemProperties.iothub-connection-device-id
という部分に含まれています。
これをSqlQueryで使おうとする場合
{ "name": "inputDocument", "type": "documentDB", "direction": "in", "databaseName": "MyDatabase", "collectionName": "MyCollection", "sqlQuery": "SELECT * from c where c.deviceId = {data.systemProperties.iothub-connection-device-id}", "connection": "CosmosDBConnection" }
と書けばいいかというと・・残念ながらこれは動きません。
じゃあJavaScriptっぽく{data["systemProperties"]["iothub-connection-device-id"]}
と書けるか・・というとこれもNGです。
デバイスIDはsubjectにも含まれていますが、そこから取り出す(またはCosmos DBのドキュメントのパラメータをこの形にする)のもまた手がかかります。
UDFで解決する
今回、私はこれをCosmos DBのUDF(ユーザ定義関数)で解決しました。
UDFはJavaScriptで記載でき、Cosmos DBのSQLなど色々なところで呼び出す処理を記載することが出来ます。
ポータルからは、Cosmos DBのコンテナ名の右にあるメニューから、「Create New UDF」を選択します。
今回は「GetDeviceId」というIDのUDFを以下の内容で作成します。
function getDeviceId(data){ return data['systemProperties']['iothub-connection-device-id'] }
そして、function.jsonでSqlQueryは以下のように書きます。
{ "name": "inputDocument", "type": "documentDB", "direction": "in", "databaseName": "MyDatabase", "collectionName": "MyCollection", "sqlQuery": "SELECT * from c where c.deviceId = udf.GetDeviceId({data}); "connection": "CosmosDBConnection" }
これで無事に検索結果の複数のドキュメントが、配列データとして関数に渡されます。
他にも、SqlQueryの中で{変数名}
として変数を置換すると文字列として置換されてしまうため、他の型を想定したSQLを書くとうまく動かないときなどにもUDFは利用できます。大変便利な機能なので是非ご活用ください。
ただし、JavaScriptの全ての機能が使えるかというとそういうわけでもなさそうで、少なくともbase64をデコードしようとatob()
関数を使うとエラーになりましたのでその点はご注意ください。