Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

Azure FunctionsのCosmos DB入力バインドでSqlQueryを使う時の便利技

こんにちは。無事AZ-900に合格してホッとしている木村です。

今回は、Azure Functionsの入力バインドでCosmos DBを使う際に、SqlQueryでちょっとはまったところを解決した手順を書こうと思います。

バインドとは

Azure Functionsは、設定されたイベントトリガーに基づいて起動される小さなプログラム(関数:Function)を実行するための基盤を提供する、FaaS(Function as a Service)です。 Azure Functionsには、関数アプリを起動するためのトリガー(例えばHTTPアクセスやEventGridからのイベントなど)とは別に、関数への入出力として「バインド」というものを設定できるという便利な機能があります。

docs.microsoft.com

これは、関数アプリと別のサービスを紐付ける時、サービスに接続してデータを入出力する部分を自前で関数内にコーディングせず、より抽象化して扱えるようにするための機能です。 サービスに接続して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(ユーザ定義関数)で解決しました。

docs.microsoft.com

UDFはJavaScriptで記載でき、Cosmos DBのSQLなど色々なところで呼び出す処理を記載することが出来ます。

ポータルからは、Cosmos DBのコンテナ名の右にあるメニューから、「Create New UDF」を選択します。

f:id:showm001:20200613164648p:plain
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()関数を使うとエラーになりましたのでその点はご注意ください。