Alternative Architecture DOJO

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

Azure Functions の Azure SQL バインディングを試した(入力編)

MLBお兄さんこと松村です。弊社オフィスのある福岡県も徐々に寒くなってきました。秋が終わっちゃいましたね。

11月中頃に Azure Functions のバインディング可能なリソースに Azure SQL Databaase が追加されました。これを試してみます。
なお、記事執筆時点ではプレビューで C# のみ提供されています。

azure.microsoft.com

バインドとは

Azure Functions の「バインド(バインディング)」とは、関数に他のリソースを宣言的に接続するための方法です。
バインドしたリソースはアクセス可能な状態で使用することができ、入力方向(取得)と出力方向(保存)の操作を行うことができます。

docs.microsoft.com

Azure SQL バインドの場合は以下の挙動になります。今回は入力バインドについて試してみます。

  • 入力バインド:データベースからデータを読み取る
  • 出力バインド:データベースにデータを保存する

セットアップ

拡張機能のインストール

Azure Functions のプロジェクトを作成したら、次の NuGet パッケージをインストールします。

www.nuget.org

接続文字列の追加

開発環境では local.settings.json に Azure SQL の接続文字列を追加します。
設定名は後述のプログラム側の定義と一致させます。

{
  ...,
  "ConnectionStrings": {
    "SqlConnectionString": "{Azure SQLの接続文字列}"
  }
}

バインド定義

Azure SQL バインドを使用するには Microsoft.Azure.WebJobs.SqlAttribute の属性付きの引数を用意します。

public IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
    [Sql("select * from users where name = @Name",
        CommandType = System.Data.CommandType.Text,
        Parameters = "@Name={Query.name}",
        ConnectionStringSetting = "SqlConnectionString")] IEnumerable<User> users)

Sql 属性の設定項目は以下の通りです。

  • コンストラクタ引数 : バインド時に実行したい SQL コマンド
    • パラメーターは @ を先頭につける
  • CommandType : コマンドの種別(純粋な SQL 文やストアドプロシージャ)
  • Parameter : SQL コマンドで指定しているパラメーターを指定する
    • HTTP トリガーのクエリパラメーターを使用する場合は {Query.クエリパラメーター名} とする
    • HTTP トリガーのパスパラメーターを使用する場合は {パスパラメーター} とする
  • ConnectionStringSetting : local.settings.json や環境変数に定義している Azure SQL への接続文字列

それではいくつかサンプルを書いたので載せていきます。
なおデータベースは Azure SQL 作成時に指定できるサンプルデータベースを使用しています。

docs.microsoft.com

純粋な SELECT 文

[FunctionName("GetAllProducts")]
public IActionResult GetAll(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
    [Sql("select * from SalesLT.Product order by ProductId asc",
        CommandType = System.Data.CommandType.Text,
        Parameters = "",
        ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Product> products)
{
    return new OkObjectResult(products);
}
curl -X GET "http://localhost:7071/api/GetAllProducts" -H  "accept: application/json"

[
  {
    "productID": "680",
    "name": "HL Road Frame - Black, 58",
    "productNumber": "FR-R92B-58",
    "color": "Black",
    ...
  },
  {
    "productID": "706",
    "name": "HL Road Frame - Red, 58",
    "productNumber": "FR-R92R-58",
    "color": "Red",
    ...
  }
]

条件指定

[FunctionName("GetProducts")]
public IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
    [Sql("select * from SalesLT.Product where Color = @Color order by ProductId asc",
        CommandType = System.Data.CommandType.Text,
        Parameters = "@Color={Query.color}",
        ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Product> products)
{
    return new OkObjectResult(products);
}
curl -X GET "http://localhost:7071/api/GetProducts?color=Red" -H  "accept: application/json"

[
  {
    "productID": "706",
    "name": "HL Road Frame - Red, 58",
    "productNumber": "FR-R92R-58",
    "color": "Red",
    ...
  },
  ...
]

カウント

[FunctionName("GetProductsCount")]
public IActionResult GetCount(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
    [Sql("select count(ProductId) cnt from SalesLT.Product where Color = @Color",
        CommandType = System.Data.CommandType.Text,
        Parameters = "@Color={Query.color}",
        ConnectionStringSetting = "SqlConnectionString")] IEnumerable<ProductCount> products)
{
    return new OkObjectResult(products.FirstOrDefault());
}
curl -X GET "http://localhost:7071/api/GetProductsCount?color=Red" -H  "accept: application/json"

{
  "cnt": 38
}

View テーブルへの SELECT

[FunctionName("GetCategoryView")]
public IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
    [Sql("select * from SalesLT.vGetAllCategories where ParentProductCategoryName = @Name",
        CommandType = System.Data.CommandType.Text,
        Parameters = "@Name={Query.name}",
        ConnectionStringSetting = "SqlConnectionString")] IEnumerable<CategoryView> categories)
{
    return new OkObjectResult(categories);
}
curl -X GET "http://localhost:7071/api/GetCategoryView?name=Accessories" -H  "accept: application/json"

[
  {
    "parentProductCategoryName": "Accessories",
    "productCategoryName": "Bike Racks",
    "productCategoryID": 30
  },
  {
    "parentProductCategoryName": "Accessories",
    "productCategoryName": "Bike Stands",
    "productCategoryID": 31
  },
  ...
]

JOIN 句

クエリが長い場合は定数化するのがオススメです。
また、この例では条件指定にパスパラメーターを使用しています。

private const string COMMAND_TEXT =
@"SELECT H.SalesOrderID, D.SalesOrderDetailId, D.OrderQty
  FROM SalesLT.SalesOrderHeader H
  INNER JOIN SalesLT.SalesOrderDetail D
        ON (H.SalesOrderID = D.SalesOrderID)
    WHERE H.SalesOrderID = @Id";

[FunctionName("GetSalesOrder")]
public IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "GetSalesOrder/{id}")] HttpRequest req,
    [Sql(COMMAND_TEXT,
        CommandType = System.Data.CommandType.Text,
        Parameters = "@Id={id}",
        ConnectionStringSetting = "SqlConnectionString")] IEnumerable<SalesOrder> salesOrders)
{
    return new OkObjectResult(salesOrders);
}
curl -X GET "http://localhost:7071/api/GetSalesOrder/71774" -H  "accept: application/json"

[
  {
    "salesOrderID": 71774,
    "salesOrderDetailId": 110562,
    "orderQty": 1
  },
  {
    "salesOrderID": 71774,
    "salesOrderDetailId": 110563,
    "orderQty": 1
  }
]

長くなってしまいましたが、一般的な SELECT 文であれば問題なく実行することができそうです。
上記のサンプルコードはこのリポジトリに置いています。参考にしてみてください。

github.com

これまでは EntityFramework Core のような O/R マッパーを用いてクエリを発行していましたが、 Azure SQL バインドを使うことでコードがよりシンプルになります。
次回は出力バインドを試してみたいと思います。