はじめに
本記事で紹介する機能はプレビュー段階であるため、Microsoft Azure プレビューの追加使用条件 に同意した上で使用するようにしてください。
最近 (2023/03) Static Web Apps のデータベース接続という機能がパブリックプレビューになりました。
データベース接続機能を使用すると、バックエンド側のコードを一切書かずに、RESTやGraphQLのエンドポイントを通じて、データベースのテーブルやエンティティに対してCRUD操作や組み込みの認証を行うことができます。
Azure Cosmos DB, Azure SQL, Azure Database for MySQL, Azure Database for PostgreSQLなどのさまざまなデータベースタイプをサポートしています。
Data API Builder というエンジン (こちらもプレビュー版) が使われています。
前回は GraphQL を使用して Cosmos DB への接続を試してみたので、今回は REST を使用して Azure SQL Database への接続を試してみたいと思います。
まずはローカルで動かしてみる
公式のチュートリアルを参考に動かしてみます。
設定ファイル作成
Static Web Apps CLI を使用して DB 接続用の設定ファイルの雛形を作成できます。
- データベースの種類
- SQL Database
- 接続文字列
@env('MSSQL_CONNECTION_STRING')
- ※
@env()
は環境変数を参照
npx swa db init \
--database-type mssql \
--connection-string "@env('MSSQL_CONNECTION_STRING')"
実行すると以下のフォルダーとファイルが出来上がります。
基本的には Cosmos DB の時と同じですが、GraphQL のスキーマ定義ファイルは作られませんでした。
swa-db-connections/
└── staticwebapp.database.config.json
staticwebapp.database.config.json
データベース接続の設定ファイルです。
staticwebapp.database.config.json (全体)
{
"$schema": "https://github.com/Azure/data-api-builder/releases/download/v0.6.14/dab.draft.schema.json",
"data-source": {
"database-type": "mssql",
"options": {
"set-session-context": false
},
"connection-string": "@env('MSSQL_CONNECTION_STRING')"
},
"runtime": {
"rest": {
"enabled": true,
"path": "/rest"
},
"graphql": {
"allow-introspection": true,
"enabled": true,
"path": "/graphql"
},
"host": {
"mode": "production",
"cors": {
"origins": [],
"allow-credentials": false
},
"authentication": {
"provider": "StaticWebApps"
}
}
},
"entities": {}
}
ブロックごとに見ていきます。
まずはデータソースの設定ですね。
コマンドで指定した値が設定されています。
"data-source": {
"database-type": "mssql",
"options": {
"set-session-context": false
},
"connection-string": "@env('MSSQL_CONNECTION_STRING')"
},
ランタイム (REST、GraphQL) 毎の設定です。
Cosmos DB (NoSQL) では GraphQL のみ対応でしたが、こちらは REST も有効となっています。
"runtime": {
"rest": {
"enabled": true,
"path": "/rest"
},
"graphql": {
"allow-introspection": true,
"enabled": true,
"path": "/graphql"
},
ホストの設定です。
Cosmos DB の時と同じです。
CORS や 認証プロバイダの設定があります。
認証プロバイダは Static Web Apps では StaticWebApps
固定になります。
"host": {
"mode": "production",
"cors": {
"origins": [],
"allow-credentials": false
},
"authentication": {
"provider": "StaticWebApps"
}
}
データベースのエンティティ (テーブル) の設定です。
最初は空です。
"entities": {}
設定ファイル編集
staticwebapp.database.config.json
先ほど空だった entities
を設定します。
設定内容は Cosmos DB の時と同様です。
"entities": {
"User": {
"source": "users",
"permissions": [
{
"role": "anonymous",
"actions": ["*"]
}
]
}
}
SQL Database の準備
SQL Server、SQL Database、テーブルを作成します。
- SQL Server 名
- <任意の名前>
- データベース名
- sqldb-swadb-demo
- テーブル名
- users
また、Static Web Apps からアクセスできるようにするため、SQL Server リソースのネットワーク設定で下記を設定します。
- 選択したネットワーク (パブリックアクセス)
- クライアントの IP アドレスを追加する
- Azure サービスおよびリソースにこのサーバーへのアクセスを許可する
ローカル開発の場合は接続文字列が必要になるので、SQL データベースリソースの「接続文字列」メニューで「ADO.NET (SQL 認証)」の値を控えておきます。(Data API Builder は .NET で作られている)
az コマンドで作成する場合
# リソースグループは作成されている前提
resourceGroup=<リソースグループ名>
sqlServerName=<SQL Server名>
sqldbName=sqldb-swadb-demo
adminUser=<管理者ユーザー名>
adminPassword=<管理者パスワード>
sqlServerFirewallName=sqlfw-swadb-demo
# クライアントのグローバルIPを取得
clientIp=$(curl inet-ip.info)
# SQL Server 作成
az sql server create \
--name $sqlServerName \
--resource-group $resourceGroup \
--admin-user $adminUser \
--admin-password $adminPassword
# ネットワーク設定
# ローカルクライアントからのアクセスを許可
az sql server firewall-rule create \
--name $sqlServerFirewallName \
--resource-group $resourceGroup \
--server $sqlServerName \
--start-ip-address $clientIp \
--end-ip-address $clientIp
# Azureサービスからアクセスを許可
az sql server firewall-rule create \
--name $sqlServerFirewallName \
--resource-group $resourceGroup \
--server $sqlServerName \
--start-ip-address 0.0.0.0 \
--end-ip-address 0.0.0.0
# SQL Database 作成
az sql db create \
--name $sqldbName \
--resource-group $resourceGroup \
--server $sqlServerName \
--compute-model Provisioned \
--edition Basic \
--capacity 5
# SQL Database の接続文字列取得
sqlDatabaseConnectionString=$(
az sql db show-connection-string \
--client ado.net \
--name $sqldbName \
--server $sqlServerName \
--outpu tsv
)
echo
echo SQL Database Connection String:
echo " $sqlDatabaseConnectionString"
最後に出力される SQL Database Connection String の値を控えておきます。
※テーブルはコマンドで作成できないので、お好みのクライアントアプリ、またはポータル上から作成してください
環境変数の設定
上記で取得した Cosmos DB の接続文字列を環境変数に設定します。
(プロジェクトルートに配置した .env ファイルに記載しておいても読み込んでくれました)
export MSSQL_CONNECTION_STRING="Server=tcp:<SQL Server名>.database.windows.net,1433;Initial Catalog=sqldb-swadb-demo;Persist Security Info=False;User ID=<adminユーザー名>;Password=<adminパスワード>;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;""
アプリの起動
--data-api-location
オプションを指定してコマンドを実行します。
npx swa start --data-api-location swa-db-connections
動作確認
CRUD のリクエストを投げてレスポンスを表示するだけのアプリを作って確認します。
公開される REST エンドポイントには基本的な CRUD 操作が含まれています。
/data-api/rest/<エンティティ名>[/<プライマリキー名>][/プライマリキー値]
No. | メソッド | エンドポイント | 概要 |
---|---|---|---|
1 | GET | /data-api/rest/User | 一覧取得 |
2 | GET | /data-api/rest/User/id/{id} | プライマリキーを指定して取得 |
3 | POST | /data-api/rest/User | 作成 |
4 | PUT | /data-api/rest/User/id/{id} | 更新 |
5 | DELETE | /data-api/rest/User/id/{id} | 削除 |
DB の初期状態
一覧取得
GET /data-api/rest/User
にリクエストします。
/** 一覧 */
const listUsers: SendRequest<ListParams<User>> = async () => {
return sendRequest('GET', '/data-api/rest/User')
}
また、フィルターでの絞り込みや、ソートの指定も可能となっています。
更にページングにも対応しています。
パラメータに $first=n
を指定すると、最初の n 件を取得し、残りのデータがある場合はレスポンスに nextLink
プロパティが含まれます。
nextLink
が次のページデータを取得するための REST エンドポイントとなっています。
[2023/05/03 現在]
この nextLink
ですが、現在 バグ があり、URLに /data-api
が含まれていないため、Static Web Apps ではそのままリクエストすると 404 となってしまいます。
誤:"nextLink": "http://localhost:4280/rest/User?xxxxxxx..."
正:"nextLink": "http://localhost:4280/data-api/rest/User?xxxxxxx..."
なので、現状は自分でURLを加工してリクエストする必要があります。
const nextLink = new URL(response.nextLink)
// nextLink.origin ではなく location.origin にしているのは
// Azure にデプロイしたときも何故か nextLink のプロトコルが http のままだから
const nextLinkSwa = `${location.origin}/data-api${nextLink.pathname}${nextLink.search}`
指定できるパラメータ
指定できるパラメータは以下ドキュメントを参照。
プライマリキーを指定して取得
GET /data-api/rest/User/id/{id}
にリクエストします。
/** キーを指定して取得 */
const getUser: SendRequest<Pick<User, 'id'>> = async ({ id }) => {
return sendRequest('GET', `/data-api/rest/User/id/${id}`)
}
登録
POST /data-api/rest/User
にリクエストします。
/** 登録 */
const createUser: SendRequest<User> = async (user) => {
return sendRequest('POST', '/data-api/rest/User', user)
}
更新
PUT /data-api/rest/User/id/{id}
にリクエストします。
/** 更新 */
const updateUser: SendRequest<User> = async ({ id, name, age }) => {
return sendRequest('PUT', `/data-api/rest/User/id/${id}`, { name, age })
}
削除
DELETE /data-api/rest/User/id/{id}
にリクエストします。
/** 削除 */
const deleteUser: SendRequest<Pick<User, 'id'>> = async ({ id }) => {
return sendRequest('DELETE', `/data-api/rest/User/id/${id}`)
}
アクセス制御の確認
Cosmos DB (GraphQL) と同様にテーブルレベルでロールベースのアクセス制御を設定することができます。
更に Cosmos DB 以外の DB では、フィールドレベルでの制御も設定することができます。
ここではフィールドレベルでの制御を試してみたいと思います。
設定ファイル編集
staticwebapp.database.config.json を編集します。
"entities": {
"User": {
"source": "dbo.users",
"permissions": [
{
"role": "anonymous",
- "actions": ["*"]
+ "actions": [
+ "create",
+ "update",
+ "delete",
+ {
+ "action": "read",
+ "fields": {
+ "exclude": ["age"]
+ }
+ }
+ ]
}
]
}
}
create
、update
、delete
は全てのフィールドにアクセス可能
read
は age
フィールドのみアクセス不可
としました。
制限されたフィールドにアクセスしない場合
データ取得時に $select
パラメータを指定すると取得するフィールドを指定できるので、age
以外を取得してみます。
アクセスできてますね。
制限されたフィールドにアクセスしようとした場合
今度は age
も含めて取得しようとしてみます。
エラーになりました!ちゃんと制御できてますね🐤
リレーションの設定
Cosmos DB 以外のリレーショナルデータベースでは、テーブル間のリレーションも設定できます。
ただし、GraphQL 限定の機能となっているので、ここだけ GraphQL で検証します。
(もしかして Cosmos DB でもできる...?今度検証してみます → できませんでした・・・!)
テーブル構成変更
組織テーブルを追加して、1(組織):n(ユーザー) のリレーションを設定することにします。
※organizationId
は FK と書きましたが、実際には外部キー制約は設定していません
スキーマ定義作成
Cosmos DB の時と同様、staticwebapp.database.schema.gql を作成します。
type User @model {
id: ID!
name: String!
age: Int!
organizationId: String
}
type Organization @model {
id: ID!
name: String!
users: UserConnection!
}
設定ファイル編集
staticwebapp.database.config.json の entities
にリレーションの設定を追加します。
"entities": {
"User": {
・・・
},
+ "Organization": {
+ "source": "dbo.organizations",
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": ["*"]
+ }
+ ],
+ "relationships": {
+ "users": {
+ "source.fields": ["id"],
+ "target.entity": "User",
+ "target.fields": ["organizationId"],
+ "cardinality": "many"
+ }
+ }
}
}
source.fields
は親テーブル (organizations) のキーとなるフィールド名
target.entity
は子テーブル (users) のエンティティ名 (この設定ファイル上の名前)
target.fields
は子テーブルが持つ外部キーのフィールド名
cardinality
は子テーブルの関係。(1:1、1:n、n:n)
動作確認
データの状態
リクエスト
こんな感じのクエリをリクエストします。
query Organizations {
organizations {
items {
id
name
users {
items {
id
name
age
}
}
}
}
}
レスポンス
組織とユーザーが紐づいたレスポンスが返ってきます!
{
"data": {
"organizations": {
"items": [
{
"id": "org1",
"name": "Organization001",
"users": {
"items": [
{
"id": "user1",
"name": "Name001",
"age": 20
},
{
"id": "user3",
"name": "Name333",
"age": 5
}
]
}
},
{
"id": "org2",
"name": "Organization002",
"users": {
"items": [
{
"id": "user2",
"name": "Name002",
"age": 30
}
]
}
}
]
}
}
}
Azure へのデプロイ
デプロイ
ローカルで動かすことができたので、Azure 環境にデプロイしてみます。
Cosmos DB の時と同様、Static Web Apps CLI でデプロイします。
デプロイ方法は以下の記事を参考にしてください。
SQL Database のリンク
デプロイできたら Cosmos DB を Static Web Apps にリンクさせます。
Static Web Apps のリソース画面で データベース接続(プレビュー)
> 既存のデータベースのリンク
を選択。
先ほど作成した SQL Database の情報を入力してリンクします。
認証の種類は接続文字列の他にマネージドIDが選べます。
ここでは接続文字列を選択しました。(というか Free プランだと接続文字列しか選択できない)
マネージドIDを選ぶには、Static Web Apps のホスティングプランを「Standard」に設定して、IDを有効にする必要があります
az コマンドで設定する場合
# Static Web Apps のリソースが作成済みの前提
resourceGroup=<リソースグループ名>
swaName=<Static Web Appsリソース名>
sqlServerName=<SQL Server名>
sqldbName=sqldb-swadb-demo
adminUser=<adminユーザー名>
adminPassword=<adminパスワード>
# SQL Server の ID 取得
sqlServerId=$(
az sql server show \
--name $sqlServerName \
--resource-group $resourceGroup \
--query id \
--output tsv
)
# データベース接続設定
echo
echo Creating Database Connection to $sqlServerId ...
az staticwebapp dbconnection create \
--db-resource-id "$sqlServerId" \
--db-name $sqldbName \
--name $swaName \
--resource-group $resourceGroup \
--username $adminUser \
--password $adminPassword
まとめ
バックエンドのコードを一切書かずに REST による DB 操作ができました。
データベース接続機能を使用すれば、背後にある DB が変わったとしてもソースコードはそのままで、設定ファイルの修正のみで済むため、アーキテクチャが固まっていない状態でも柔軟に対応できそうです。
実際のアプリケーションでは、もっと複雑な検索が必要だったり、複数のデータを一括更新したりする場面も当然あると思います。そういった複雑な部分は バックエンドを実装し、単純な CRUD はデータベース接続を使う というように使い分けていけば、実装を最小限に抑えられるかもしれませんね😊