✌️

Azure Functions V4 で Azure SQL Database バインドを試してみた

2023/10/03に公開

Azure Functions V4 ドキュメント少ない問題

実は、Functions V4はついこの前出場したハッカソンで友達が使っていて認知したのですが、その時は公式ドキュメントにすらV4のサンプルコードなどが用意されておらず、圧倒的ドキュメントの少なさで苦戦していて、何とかgithubからサンプルコードを拾ってきたりしていて中々苦しそうでした。

しかし、不幸なことにハッカソンが終わった次の日にドキュメントが更新されて、いい感じにV4でバインドするコードが載っていて、なんとも言えないタイミングですね...w

まだnode版はGAされたばっかで、一応日本語対応(ja-jp)にすると、V4のサンプルコードが表示されませんが、en-usではV4のサンプルコードがしっかりと書かれていますね。(2023/9/27時点)
https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-azure-sql-input?tabs=in-process%2Cnodejs-v4&pivots=programming-language-typescript

使用技術

  • React(typescript)
  • Azure Static Web App
  • Azure functions v4 (swa内包)
  • Azure SQL Database
  • bun (Javascriptランタイム・パッケージマネージャー)
  • WSL2
  • Azure Data Studio

爆速の肉まん(bun)使ってみました🍽

事前準備

とりあえず、めんどくさい事前準備です。

Azure SQL Databaseのリソース作成

とりま、Azure SQL Databaseのリソースを作成していきましょう。
portalに移動して、リソースグループを作成します。
次にAzure SQL Databaseと検索して、リソースを作成していきます。

リソースの作成を押して、適当なDB名などを入力していきます。

DBサーバの作成

DBサーバについては、新規作成を押して、適当に作成していきます。
本記事では、以下のように認証方法をAzure Active Directory (Azure AD) 認証のみを使用するで選択しました。使用しているサブスクによっては、Azure ADが使えない方もいると思うので、その方は、SQL認証を選択してください。

追記:2023/10/7

学生アカウントでAzure AD認証を選択することは出来ますが、私のAzure学生アカウントに割り当てられた権限では、デプロイ先でDB接続時の認証が通せませんでした。学生アカウントの方は注意してください。
https://zenn.dev/smaru1111/articles/5423e60840deaf

冗長性

冗長性については、ローカル冗長で十分だと思うので、それを選択します。

後は全部すっ飛ばして、確認および作成からリソースを作成します。

DBのテーブルを定義する

では、さきほど作成したAzure SQL Databaseにテーブルを定義していきます。
事前準備の章で載せていますが、Azure Database Studioをインストールしていない方は、ここからインストールしてください。

ファイアウォールの設定

作業する前に繋いでるネットのIPアドレスが変わっていれば、ファイアウォール規則ではじかれるので、portalのSQLサーバのリソースに移動して設定しなおしましょう。

Azure Database Studioを開いて、下の画像のNew Connectionのボタンから、DBを接続していきます。

認証方法の設定

Azure AD認証を設定しているひとは、画像のAzure Active Directoryを選択します。
設定していない人は、前の手順で設定したSQL認証の情報を適宜入力して下さい。

Server

少し見にくいですが、ServerはportalのDBのリソースの概要サーバ名にありますので、そちらを入れてください。

クエリの作成

コネクションが完了したら、SQLを書いてTableを作成していきます。
左のタブにサーバ名が書かれた項目が出てくるので、そこを右クリックしてNew Queryを選択しましょう。

テーブルの概要

今回、テーブルはこのように作成します。

todoId todoName todoStatus
int(主キー) nvarchar(50) bit

テーブルの定義

SQL文は以下のようになります。コピペして実行します。

create-todo-table.sql
CREATE TABLE dbo.TodoTable (
    todoId int identity(1,1) primary key,
    todoName nvarchar(50) not null,
    todoStatus bit not null
)

SQL文の補足説明

todoId

todoId int identity(1,1) primary key,

identity(1,1)は、データを追加したときに、自動でインクリメントした値を入れてくれます。

todoName

todoName nvarchar(50) not null,

nvarchar(50)というのは、Unicode文字を含む可変長の文字列を定義しています。
これを設定しないと、日本語文字列をデータとして挿入したときに文字化けを起こします。最初気づかず引っかかるポイント。

todoStatusについては、特に説明することがないので省きます。

テーブルが出来てるか確認

クエリを実行出来たら、テーブルが出来たか確認していきます。
サーバ>Databases/{DB名}/Tables/{Table名}を右クリックして、Edit Dataを選択します。

こんな感じでテーブルができていれば成功です。

functionsからDBにデータを追加してみる

functionからDBにデータをバインドする処理を書いていきます。
functionの作成方法については、前のスクラップで書いています。

接続文字列の設定

portalのSQLDBのリソースを開いて、以下の画像の場所から接続文字列を取得します。

プロジェクトのフォルダをVSCodeで開いて、api/local.settings.jsonを編集します。
{DB接続文字列}に、先ほどコピーしてきた接続文字列を入れてください。

api/local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
  },
  "ConnectionStrings": {
    "SqlConnectionString": "{DB接続文字列}"
  }
}

functionの処理

新しくaddData関数とgetData関数、delData関数を作って、以下コピペします。

Output

INSERT文に当たるものです。
ちなみに、DBにすでにあるidでレコードを追加しようとすると、そのレコードが上書きされるUPDATE文になります。
例えば、DBに{ todoId:1, todoName:hoge, todoStatus:false }のデータがあり、こちらのOutputの処理でもう一度{ todoId:1, todoName:piyo, todoStatus:true }のデータを保存しようとすると、最終的にDBに保存されているデータは、{ todoId:1, todoName:piyo, todoStatus:true }になります。

/api/src/functions/addData.ts
import { app, HttpRequest, HttpResponseInit, InvocationContext, output } from "@azure/functions";

// バインドのタイプを定義
const sqlOutput = output.generic({
    type: 'sql',
    // 普通ここにSQL文をべた書きする、table名だけだとinsert文になる。
    commandtext: 'dbo.TodoTable',
    connectionStringSetting: 'SqlConnectionString'
})

export async function addData(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    // リクエストbodyの内容を取り出している
    const todo = await request.json()
    context.log("😇: ", todo)
    // sqlOutputの情報と挿入するリクエストボディのデータを入れてバインド
    context.extraOutputs.set(sqlOutput, todo);
    return {
        status: 201,
        body: JSON.stringify(todo)
    };
}

app.http('addData', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    // 出力タイプか入力かを定義
    extraOutputs: [sqlOutput],
    // 上で定義したバインド処理の関数をハンドラとして持たせる。
    handler: addData
});

Input

SELECT文に当たるものです。

/api/src/functions/getData.ts
import { app, HttpRequest, HttpResponseInit, InvocationContext, input } from "@azure/functions";

const sqlInput = input.generic({
    // azure sql databaseの時これ
    type: 'sql',
    commandtype: 'text',
    // SQL文ここに書く
    commandtext: 'select * from dbo.TodoTable',

    // 接続文字列の設定
    connectionStringSetting: 'SqlConnectionString'
})

export async function getData(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    const data = context.extraInputs.get(sqlInput);
    context.log("😇: ", data)
    return {
        status: 200,
        body: JSON.stringify(data)
    };
}

app.http('getData', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    extraInputs: [sqlInput],
    handler: getData
});

Input(DELETE文?)

最後にDELETEだけ特殊です。
AzureのSQLバインドにDELETE文にあたるものは実装されていないので、ストアドプロシージャを作成して、それをInput処理としてバインド時に指定してあげて、DELETE文を実装する形です。

とりあえず、クエリを作成した要領で、以下のSQL文を実行してストアドプロシージャを作成します。

CREATE PROCEDURE [dbo].[DeleteToDo]
    @Id INT
AS
    DECLARE @UID INT = TRY_CAST(@ID AS INT)
    IF @UId IS NOT NULL AND @Id != ''
    BEGIN
        DELETE FROM dbo.TodoTable WHERE todoId = @UID
    END
    ELSE
    BEGIN
        DELETE FROM dbo.TodoTable WHERE @ID = ''
    END

    SELECT [todoId], [todoName], [todoStatus] FROM dbo.TodoTable
GO

そして、そのストアドプロシージャを実行するFunctionを作成しましょう。
以下のFunctionでは、クエリパラメータでtodoIdを渡してあげて、それを基にDELETE文を実行します。

/api/src/functions/delData.ts
import { app, HttpRequest, HttpResponseInit, InvocationContext, input } from "@azure/functions";

const sqlInput = input.generic({
    // azure sql databaseの時これ
    type: 'sql',
    commandtype: 'StoredProcedure',
    // SQL文ここに書く
    commandtext: 'DeleteToDo',
    parameters: "@Id = {Query.id}",
    // 接続文字列の設定
    connectionStringSetting: 'SqlConnectionString'
})

export async function delData(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`Http function processed request for url "${request.url}"`);
    const data = context.extraInputs.get(sqlInput);
    context.log("😇: ", data)
    return {
        status: 200,
        body: JSON.stringify(data)
    };
}

app.http('delData', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    extraInputs: [sqlInput],
    handler: delData
});

Functions V4のバインド処理のサンプルここで見れます。
https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-azure-sql-output?tabs=in-process%2Cnodejs-v4&pivots=programming-language-typescript

テスト

ファイアウォールの設定

テストの前に、繋いでいるネットのIPアドレスが変わっていれば、ファイアウォール規則ではじかれるので、portalのSQLサーバのリソースに移動して設定しなおしましょう。

addDataの実行

apiに移動してfunctionをローカルで立ち上げます。

bash1
cd api
func start

そして、別のコンソールを立ち上げて、curlでfuncitonを叩きます。

bash2
curl --request POST http://localhost:7071/api/addData --data '{"todoName":"Azure Rocks","todoStatus":true}'

実際に追加されているか確認

成功していたら、先ほど確認したAzure Database Studioのテーブルに追加されているはずです。
(私のはデータ削除をしているので、todoIdは11からになっています)

getDataの実行

getDataを実行します。

bash2
curl --request GET http://localhost:7071/api/getData

ログ見ていい感じに取れてきてたら成功です。

bash2
[2023-10-02T09:53:05.536Z] 😇:  [ { todoId: 11, todoName: 'Azure Rocks', todoStatus: true } ]
[2023-10-02T09:53:05.537Z] Executed 'Functions.getData' (Succeeded, Id=hogehogehoghegoheogheohgeo, Duration=16111ms)

delDataの実行

delDataを実行します。

bash2
curl --request GET http://localhost:7071/api/delData?id=1

成功していたら、先ほど確認したAzure Database Studioのテーブルから、idが1のデータが消えているはずです。

まとめ

functions V4になってから、ディレクトリ構成が変わり、かなり簡潔になりました。
Nodejs版はまだGAされたばかりなので、ドキュメントの数が少ないですが、バインドがさらに簡潔に書けるようになったり、開発体験がとても良いです。
これからも積極的に使っていきたいと思います。

参考文献

Functions v4 node版のGA公式発表

https://azure.microsoft.com/en-us/updates/generally-available-azure-functions-v4-programming-model-for-nodejs/

バインドする際に参考にした資料たち

DBリソース作成方法について

https://learn.microsoft.com/ja-jp/azure/azure-sql/database/single-database-create-quickstart?view=azuresql&tabs=azure-portal

Functions v4のバインドサンプルコード

https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-azure-sql-output?tabs=in-process%2Cnodejs-v4&pivots=programming-language-typescript

https://github.com/Azure/azure-functions-sql-extension/tree/main/samples/samples-js-v4

元スクラップ

https://zenn.dev/smaru1111/scraps/4198d9156a860b

Discussion