Azure Functions V4 で Azure SQL Database バインドを試してみた
Azure Functions V4 ドキュメント少ない問題
実は、Functions V4はついこの前出場したハッカソンで友達が使っていて認知したのですが、その時は公式ドキュメントにすらV4のサンプルコードなどが用意されておらず、圧倒的ドキュメントの少なさで苦戦していて、何とかgithubからサンプルコードを拾ってきたりしていて中々苦しそうでした。
しかし、不幸なことにハッカソンが終わった次の日にドキュメントが更新されて、いい感じにV4でバインドするコードが載っていて、なんとも言えないタイミングですね...w
まだnode版はGAされたばっかで、一応日本語対応(ja-jp)にすると、V4のサンプルコードが表示されませんが、en-usではV4のサンプルコードがしっかりと書かれていますね。(2023/9/27時点)
使用技術
- React(typescript)
- Azure Static Web App
- Azure functions v4 (swa内包)
- Azure SQL Database
- bun (Javascriptランタイム・パッケージマネージャー)
- WSL2
- Azure Data Studio
爆速の肉まん(bun)使ってみました🍽
事前準備
とりあえず、めんどくさい事前準備です。
- Node18
- Azureのサブスク(できればAD認証使えると嬉しい)
- VSCodeのAzure拡張機能
- Azure Static Web Apps CLI(フロントも作る方は)
- Azure Functions Core Tools(必須)
- Azure Data Studio(必須)
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接続時の認証が通せませんでした。学生アカウントの方は注意してください。
冗長性
冗長性については、ローカル冗長で十分だと思うので、それを選択します。
後は全部すっ飛ばして、確認および作成
からリソースを作成します。
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 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接続文字列}
に、先ほどコピーしてきた接続文字列を入れてください。
{
"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 }
になります。
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文に当たるものです。
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文を実行します。
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のバインド処理のサンプルここで見れます。
テスト
ファイアウォールの設定
テストの前に、繋いでいるネットのIPアドレスが変わっていれば、ファイアウォール規則ではじかれるので、portalのSQLサーバのリソースに移動して設定しなおしましょう。
addDataの実行
apiに移動してfunctionをローカルで立ち上げます。
cd api
func start
そして、別のコンソールを立ち上げて、curlでfuncitonを叩きます。
curl --request POST http://localhost:7071/api/addData --data '{"todoName":"Azure Rocks","todoStatus":true}'
実際に追加されているか確認
成功していたら、先ほど確認したAzure Database Studioのテーブルに追加されているはずです。
(私のはデータ削除をしているので、todoIdは11からになっています)
getDataの実行
getDataを実行します。
curl --request GET http://localhost:7071/api/getData
ログ見ていい感じに取れてきてたら成功です。
[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を実行します。
curl --request GET http://localhost:7071/api/delData?id=1
成功していたら、先ほど確認したAzure Database Studioのテーブルから、idが1のデータが消えているはずです。
まとめ
functions V4になってから、ディレクトリ構成が変わり、かなり簡潔になりました。
Nodejs版はまだGAされたばかりなので、ドキュメントの数が少ないですが、バインドがさらに簡潔に書けるようになったり、開発体験がとても良いです。
これからも積極的に使っていきたいと思います。
参考文献
Functions v4 node版のGA公式発表
バインドする際に参考にした資料たち
DBリソース作成方法について
Functions v4のバインドサンプルコード
元スクラップ
Discussion