Azure API Management の GraphQL API の リゾルバー で Azure SQL データソースを使ってみる

# はじめに

API Managementは、API を簡単に管理できるサービスですが、REST API だけでなく GraphQL の API も作成できることができます。
API Management では、これまで既存の REST API をデータソースとして GraphQL の API を作成することができましたが、Cosmos DB と Azure SQL Database をデータソースとして使用できるようになりました。**(※プレビュー段階)**
これらのデータソースを使用することで、別途バックエンドを用意せずにデータベースを直接 API として公開することができます。
この記事では、Azure SQL Database のデータソースを使って、API Management で GraphQL の API を作成してみたいと思います。
（タイトルが「パルスのファルシのルシがパージでコクーン」みたいになってしまった）

# 前提

- API Management のリソースが作成済みであること。
    - この記事では API Management のリソース作成手順については説明しません。
    - 価格レベルは何でも良いと思いますが、執筆時は従量課金レベルで検証しています。
- Azure SQL Database のリソースが作成済みであること
    - この記事では Azure SQL Database のリソース作成手順については説明しません。

:::note warn
[2023/08/20 時点]
ドキュメントには従量課金レベルではサポートされていないと記載されていますが、使用可能になったようです。
(最初うまく動かずに MS の Q&A で問い合わせたら、「従量課金だと使えないよ」と回答があって放置してましたが、先日、使用可能になったと連絡をいただきました)
:::

# 事前準備

基本的にはドキュメントに従って進めていきます。

https://learn.microsoft.com/ja-jp/azure/api-management/sql-data-source-policy

## API Management のマネージド ID を有効にする

今回 SQL Database にはマネージド ID を使用して接続するため、マネージド ID を有効にします。
Azure Portal で API Management のリソースを表示し、`マネージド ID` から 状態を オン に変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/8b348e39-65bc-9a4f-0949-5f51d14306d4.png)

## SQL Database の Azure AD アクセスを有効化

マネージド ID で接続するために、SQL Database で Azure AD によるアクセスを有効にする必要があります。
SQL Server のリソースを表示し、`Azure Active Directory` > `管理者の設定`で自身のアカウントを選択し、`保存`をクリック。

※Azure Active Directory の表記はいずれ Microsoft Entra ID に変更される...と思います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/fb00dc9b-1e40-a249-8d13-b3b601edc244.png)

## SQL Database のロールの割り当て

API Management の SQL Database 上でのロールを設定します。
Azure Portal のクエリーエディタ、または任意のクライアントツールでデータベースに接続し、以下のクエリを実行します。

```sql
DECLARE @identityName nvarchar(60) = '<API Managementのリソース名>';

EXECUTE(N'
CREATE USER ['+ @identityName +'] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [' + @identityName + '];
ALTER ROLE db_datawriter ADD MEMBER [' + @identityName + '];
ALTER ROLE db_ddladmin ADD MEMBER [' + @identityName + '];
');

```

以下は Portal のクエリエディタを使用した場合。(SQL Server のリソースより)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/e0d1543b-d162-06c8-beb3-8509c7c11fb0.png)

## テーブル作成

適当なテーブルを作成し、適当なデータを投入します。


# API の作成

## Schema ファイルの作成

GraphQL API を作成する際に Schema ファイルを選択する必要があるため、事前に作成します。
今回は以下のように作成しました。

```graphql
type Query {
  getUser(id: ID!): User!
  getUsers: UserList!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(input: UpdateUserInput!): User!
  deleteUser(id: ID!): User!
}

type User {
  id: ID!
  name: String!
  age: Int!
}

type UserList {
  items: [User!]!
}

input UpdateUserInput {
  id: ID!
  name: String!
  age: Int!
}

input CreateUserInput {
  id: ID!
  name: String!
  age: Int!
}
```

## GraphQL API の追加

API Management のリソースを表示し、`API` > `Add API` > `GraphQL`をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/4af8dc1e-1b11-b8a1-c5d7-290786710b20.png)

画像の様に設定して Create をクリック。
GraphQL type で Synthetic GraphQL を選択すると Schema ファイルを選択できるようになります。
Schema ファイルに先ほど作成したファイルを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/412c5ae4-21c5-33fc-9a9d-eb2d10492108.png)

## Resolver の作成

作成した API が一覧に表示されるので、クリックすると先ほど選択した Schema が見られます。
更に行番号の左にある`＋`をクリックすると、Resolver の登録画面に行くことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/d974539a-0e60-d816-8ba5-db1066d2b61a.png)

Resolver の登録画面で Data source に Azure SQL を設定し、Resolver policy(後述)を入力して Create をクリック。
※Schema から ジャンプした場合は Name、Type、Field は自動的に入力されています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/76b86366-a2af-f804-542d-806cb4a22ca0.png)

### ポリシーの設定

#### 接続設定(共通)

マネージドIDで接続するため、`connection-string`の属性に`use-managed-identity="true"`を指定します。

```xml
<sql-data-source>
    <!-- 接続設定 -->
	<connection-info>
		<connection-string use-managed-identity="true">
            Server=tcp:<サーバー名>.database.windows.net,1433;Initial Catalog=<データベース名>;
        </connection-string>
	</connection-info>
    <!-- 省略 -->
</sql-data-source>
```


#### 単一結果の取得

単一結果を取得する場合は以下の様なポリシーとなります。
(getUser)

```xml
<!-- getUser -->
<sql-data-source>
	<!-- 接続設定は省略 -->
	<request single-result="true">
		<sql-statement>
            SELECT
                u.[id],
                u.[name],
                u.[age]
            FROM
                [dbo].[users] u
            WHERE
                u.[id] = @id
        </sql-statement>
		<parameters>
			<parameter name="@id">@(context.GraphQL.Arguments["id"])</parameter>
		</parameters>
	</request>
</sql-data-source>
```

結果セットが1件であることを明示するため、`request`の属性に`single-result="true"`を指定します。
`parameters`に指定したパラメータを`sql-statement`内で使用することができます。
`parameter`には [ポリシー式](https://learn.microsoft.com/ja-jp/azure/api-management/api-management-policy-expressions) を指定できます。
`context.GraphQL.Arguments`にリクエスト時に指定した引数が格納されています。
(ちなみに`context.GraphQL.Arguments`の型はJSONオブジェクトを操作するためのクラスである [JToken](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Linq_JToken.htm) となっていました)


https://learn.microsoft.com/ja-jp/azure/api-management/api-management-policy-expressions

https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Linq_JToken.htm

#### 複数行の取得

複数行の結果を取得する場合は以下の様なポリシーとなります。
(getUsers)

```xml
<!-- getUsers -->
<sql-data-source>
	<!-- 接続設定は省略 -->
	<request>
		<sql-statement>
            SELECT
                u.[id],
                u.[name],
                u.[age]
            FROM
                [dbo].[users] u
        </sql-statement>
	</request>
</sql-data-source>
```

結果セットが複数行の場合は、`request`の属性の`single-result="true"`は不要です。

#### 更新系

更新系の場合は以下の様なポリシーとなります。
(createUser)

```xml
<!-- createUser -->
<sql-data-source>
	<!-- 接続設定は省略 -->
	<request single-result="true">
		<sql-statement>
            INSERT INTO [dbo].[users] (
                [id],
                [name],
                [age]
            )
            OUTPUT inserted.*
            VALUES (
                @id,
                @name,
                @age
            )
        </sql-statement>
		<parameters>
			<parameter name="@id">@(context.GraphQL.Arguments["input"]["id"])</parameter>
			<parameter name="@name">@(context.GraphQL.Arguments["input"]["name"])</parameter>
			<parameter name="@age">@(context.GraphQL.Arguments["input"]["age"])</parameter>
		</parameters>
	</request>
</sql-data-source>
```

更新系も結局 SQL が変わるだけなのですが、Schema 定義にあわせて挿入したレコードを返すため、`OUTPUT inserted.*`で取得しています。
更に`single-result="true"`に設定しています。
`OUTPUT ...`がないデフォルトの場合は、挿入した件数が返されました。

##### その他の更新系ポリシー

<details>
<summary>表示する</summary>

**updateUser**

```xml
<!-- updateUser -->
<sql-data-source>
	<!-- 接続設定は省略 -->
	<request single-result="true">
		<sql-statement>
            UPDATE [dbo].[users] SET 
                [name] = @name,
                [age] = @age
            OUTPUT inserted.*
            WHERE
                [id] = @id
        </sql-statement>
		<parameters>
			<parameter name="@id">@(context.GraphQL.Arguments["input"]["id"])</parameter>
			<parameter name="@name">@(context.GraphQL.Arguments["input"]["name"])</parameter>
			<parameter name="@age">@(context.GraphQL.Arguments["input"]["age"])</parameter>
		</parameters>
	</request>
	<response />
</sql-data-source>
```

**deleteUser**

```xml
<!-- deleteUser -->
<sql-data-source>
	<!-- 接続設定は省略 -->
	<request single-result="true">
		<sql-statement>
            DELETE
            FROM [dbo].[users] 
            OUTPUT deleted.*
            WHERE
                [id] = @id
        </sql-statement>
		<parameters>
			<parameter name="@id">@(context.GraphQL.Arguments["id"])</parameter>
		</parameters>
	</request>
	<response />
</sql-data-source>
```

</details>

# API のテスト

API Management の API を選択し、`Test`タブから API のテストを実行できます。
左側に Schema 定義に従って一覧が表示され、チェックボックスをチェックすることで自動的に query が組み立てられます。
`Send`または`Trace`をクリックするとリクエストを送信します。
Trace はその名の通り、トレース情報を出力してくれるので、デバッグする際は有効です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/baa04b84-b9b5-3a55-d755-73c4d442a740.png)

# おまけ

## レスポンスをカスタマイズする

以下のように`set-body`でレスポンスのフォーマットをカスタマイズすることができます。

```diff_xml
<!-- getUsers -->
<sql-data-source>
	<!-- 接続設定は省略 -->
	<request>
		<sql-statement>
            SELECT
                u.[id],
                u.[name],
                u.[age]
            FROM
                [dbo].[users] u
        </sql-statement>
	</request>
+   <response>
+       <set-body template="liquid">
+           [
+               {% JSONArrayFor user in body.items %}
+               {
+                   "id": "{{ user.id }}",
+                   "name": "{{ user.name }}",
+                   "age": {{ user.age }}
+               }
+               {% endJSONArrayFor %}
+           ]
+       </set-body>
+   </response>
</sql-data-source>
```

もともとは以下の様なフォーマットのところ
```json
{
  "items": [
    {
      "id": "user001",
      "name": "Name001",
      "age": 10,
    },
    ...
  ]
}
```

以下のようにカスタマイズしています。

```json
[
  {
    "id": "user001",
    "name": "Name001",
    "age": 10,
  },
  ...
]
```

`set-body`では Liquid というテンプレートエンジンの記法で記述することができます。
Liquid は Power Pages でも採用されているみたいですね。

https://learn.microsoft.com/ja-jp/power-pages/configure/liquid/liquid-overview


## オプショナルなパラメータを指定する

### ポリシー式を使用する

オプショナルなパラメータを定義して、指定された場合のみ条件に含めるといったことがやりたくなると思います。
そういった場合、SQL を動的に組み立てる必要がありますが、ポリシー式を使用することで実現できます。

```diff_xml
<!-- getUsers -->
<sql-data-source>
	<!-- 接続設定は省略 -->
   <request single-result="true">
+       <sql-statement>
+       @{
+           var sql = new StringBuilder(@"
+               SELECT
+                   u.[id],
+                   u.[name],
+                   u.[age]
+               FROM
+                   users u
+           ");
+
+           var conditions = new List<string>();
+           if (context.GraphQL.Arguments["name"].Value<string>() != null)
+           {
+               conditions.Add("u.[name] LIKE '%' + @name + '%'");
+           }
+           if (context.GraphQL.Arguments["age"].Value<int?>() != null)
+           {
+               conditions.Add("u.[age] >= @age");
+           }
+
+           if (conditions.Count > 0)
+           {
+               sql.Append($" WHERE {string.Join(" AND ", conditions)}");
+           }
+
+           return sql.ToString();
+       }
+       </sql-statement>
+       <parameters>
+           <parameter name="@name">@(context.GraphQL.Arguments["name"].Value<string>() ?? "empty")</parameter>
+           <parameter name="@age">@(context.GraphQL.Arguments["age"].Value<int?>() ?? 0)</parameter>
+       </parameters>
  </request>
</sql-data-source>
```

`@{}`で囲むと普通に C# のコードの様に複数行で記述することができます。
`return`された文字列が SQL クエリとなります。

パラメータを以下のように書いてしまうと、指定しない場合に「必須だよ！」と怒られてしまうので

```xml
<parameter name="@name">@(context.GraphQL.Arguments["name"])</parameter>
<parameter name="@age">@(context.GraphQL.Arguments["age"])</parameter>
```

以下のように未指定の場合はダミーの値を設定するようにします。
他にやり方ありそうな気がしますが、わかりませんでした。
もっとスマートな方法知っている方は是非教えてください。

```xml
<parameter name="@name">@(context.GraphQL.Arguments["name"].Value<string>() ?? "empty")</parameter>
<parameter name="@age">@(context.GraphQL.Arguments["age"].Value<int?>() ?? 0)</parameter>
```

### Liquid テンプレートを使用する

もう1つ動的に SQL を組み立てる方法として、`set-body`で Liquid テンプレートを使用する方法があります。

```diff_xml
<!-- getUsers -->
<sql-data-source>
	<!-- 接続設定は省略 -->
    <request single-result="true">
+       <set-body template="liquid"><![CDATA[
+           SELECT
+               u.[id],
+               u.[name],
+               u.[age]
+           FROM
+               users u
+           WHERE 
+               1 = 1  --(横着しました)
+       {% if body.arguments.name != null %}
+           AND u.[name] LIKE '%' + @name + '%'
+       {% endif %}
+       {% if body.arguments.age != null %}
+           AND u.[age] >= @age 
+       {% endif %}
+       ]]></set-body>
+       <sql-statement>
+           @(Regex
+               .Match(context.Request.Body.As<string>(), @"<!\[CDATA\[(?<sql>.*?)\]\]>", RegexOptions.Singleline)
+               .Groups["sql"]?.Value)
+       </sql-statement>
        <parameters>
            <parameter name="@name">@(context.GraphQL.Arguments["name"].Value<string>() ?? "empty")</parameter>
            <parameter name="@age">@(context.GraphQL.Arguments["age"].Value<int?>() ?? 0)</parameter>
        </parameters>
    </request>
</sql-data-source>
```

レスポンスのカスタマイズのところで`set-body`を使用していましたが、リクエストの方でも使うことができます。
SQL で不等号を使うと保存したときに勝手にエスケープされてしまうので、`<![CDATA[...]]>`で囲んでいます。
そして、`set-body`で設定した body を`sql-statement`で参照しています。
そのままだと`<![CDATA[` `]]>`の部分も含まれてしまうので、囲まれた部分だけ抽出するようにしています。

# おわりに

API Management と データベースだけで GraphQL の API を作成することができました。
ゴリゴリ SQL 書かないといけないので、あまりお手軽ではないかもしれませんが、逆に言うと柔軟に対応できるということでもありますね。
ストアドプロシージャも呼び出すことができるみたいなので、複雑な処理もできそうです。
デバッグはトレース情報見るくらいしかないので、もう少しやりやすくなると良いなーと思いました。
(自分が知らないだけかもしれない)

次の機会に Cosmos DB のデータソースも試してみたいと思います。