Azure FunctionsのSQLトリガーを使ってみた

# 概要

[Functions の Azure SQL トリガー](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-azure-sql-trigger) に沿って、Azure FunctionsのSQLトリガーを設定してみました。

# 作業

## DBの設定変更

### データベースとテーブルを作成する
「ToDoDB」データベースに「ToDo」テーブルを作成しました。
テーブルはサンプルの通り下記SQLクエリを使用しています。
```sql
CREATE TABLE dbo.ToDo (
    [Id] UNIQUEIDENTIFIER PRIMARY KEY,
    [order] INT NULL,
    [title] NVARCHAR(200) NOT NULL,
    [url] NVARCHAR(200) NOT NULL,
    [completed] BIT NOT NULL
);
```

### 変更の追跡設定を行う（SQLクエリ）
SQLクエリで変更の追跡設定を変更する場合はサンプル通り下記を実行します。

```sql
ALTER DATABASE [ToDoDB]
SET CHANGE_TRACKING = ON
(CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON);

ALTER TABLE [dbo].[ToDo]
ENABLE CHANGE_TRACKING;
```

システムデータベースの場合は変更できませんのでエラーが発生します。

```
メッセージ 5090、レベル 16、状態 1、行 1
Database 'master' is a system database. Change tracking settings cannot be modified for system databases.
メッセージ 5069、レベル 16、状態 1、行 1
ALTER DATABASE statement failed.
```

### 変更の追跡設定を行う（Microsoft SQL Server Management Studio）
SSMSを使用して変更の追跡設定を変更する場合は、まずデータベースのプロパティから「変更の追跡」をクリックし、各種設定を変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7886/495429d8-06b1-50c5-bc4f-142e0a7ac1ae.png)

システムデータベースの場合は変更できませんのでエラーが発生します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7886/f6770f45-b484-ccb0-6506-b410522a9f74.png)

次にテーブルのプロパティから「変更の追跡」をクリックし、各種設定を変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7886/9310a06b-201c-e0fa-bee3-9a55446a7753.png)

テーブルの「変更の追跡」は、データベースの「変更の追跡」が`True`になっていないと変更できません。

## プロジェクトの作成

Function（タイプ）に`SQL trigger`を選択し、進みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7886/ec48b2a2-1db0-1185-be82-7127dc7f5093.png)

今回はLocalDBに接続するので、「SQL Server Express LocalDB（ローカル）」を選択しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7886/2089a02a-0cb9-1ea4-37ff-71e1e8f12e5f.png)


## プログラムの作成

### local.settings.json
環境変数から接続文字列を取得したい場合は`local.settings.json`を修正します。

```diff_json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  },
+  "ConnectionStrings": {
+    "ToDoDBConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=ToDoDB;Integrated Security=True"
+  }
}
```

### Function1.cs
`Newtonsoft.Json`を使用したソースコードが初期生成されたのですが、サンプルに合わせて下記のように変更しました。

```csharp
using System;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker.Extensions.Sql;
using Microsoft.Extensions.Logging;

namespace FunctionApp2
{
    public class Function1
    {
        private readonly ILogger _logger;

        public Function1(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger<Function1>();
        }

        // Visit https://aka.ms/sqltrigger to learn how to use this trigger binding
        [Function("Function1")]
        public void Run(
            [SqlTrigger("[dbo].[ToDo]", "ToDoDBConnectionString")] IReadOnlyList<SqlChange<ToDoItem>> changes,
                FunctionContext context)
        {
            foreach (SqlChange<ToDoItem> change in changes)
            {
                ToDoItem toDoItem = change.Item;
                _logger.LogInformation($"Change operation: {change.Operation}");
                _logger.LogInformation($"Id: {toDoItem.Id}, Title: {toDoItem.title}, Url: {toDoItem.url}, Completed: {toDoItem.completed}");
            }
        }
    }

    public class ToDoItem
    {
        public Guid Id { get; set; }
        public int? order { get; set; }
        public string title { get; set; }
        public string url { get; set; }
        public bool? completed { get; set; }
    }
}
```

## 実行結果

データをInsertしてみると下記のように実行できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7886/9db9d08d-b791-b463-37bf-d666a91c1197.png)

# エラーなど

## 変更の追跡設定ができていない場合
変更の追跡設定ができていない場合、Functions起動後、テーブルに接続できない旨のメッセージが表示されます。

```
 Failed to start SQL trigger listener for table: 'dbo.ToDo', function ID: 'cd196062e99a425e'. Exception: System.InvalidOperationException: Could not find table: 'dbo.ToDo'.
    at Microsoft.Azure.WebJobs.Extensions.Sql.SqlTriggerUtils.GetUserTableIdAsync(SqlConnection connection, SqlObject userTable, ILogger logger, CancellationToken cancellationToken)
    at Microsoft.Azure.WebJobs.Extensions.Sql.SqlTriggerListener`1.StartAsync(CancellationToken cancellationToken)
 The listener for function 'Functions.Function1' was unable to start.
```

## 互換が無い型を設定した場合
`IReadOnlyList<SqlChange<ToDoItem>>`で型のマッピングを行った上でFunctionsを実行するため、型が異なっていた場合は実行されずにエラーとなります。

```
Function 'Function1', Invocation id '8864b11f-0131-437c-b1f7-664cb7ae98ea': An exception was thrown by the invocation.
Result: Function 'Function1', Invocation id '8864b11f-0131-437c-b1f7-664cb7ae98ea': An exception was thrown by the invocation.
Exception: Microsoft.Azure.Functions.Worker.FunctionInputConverterException: Error converting 1 input parameters for Function 'Function1': Cannot convert input parameter 'changes' to type 'System.Collections.Generic.IReadOnlyList`1[[Microsoft.Azure.Functions.Worker.Extensions.Sql.SqlChange`1[[FunctionApp2.ToDoItem, FunctionApp2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], Microsoft.Azure.Functions.Worker.Extensions.Sql, Version=3.0.534.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91]]' from type 'System.String'. Error:System.MissingMethodException: 削除済みのメソッドの実装を呼び出そうとしました。これは、アプリケーションの実行中にメソッドが削除されたり、メソッドの名前やシグネチャが変更されたりした場合に発生する可能性があります。
    at FunctionApp2.ToDoItem.set_Id(String value)
    at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
    at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
    at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
    at System.Text.Json.Serialization.Converters.SmallObjectWithParameterizedConstructorConverter`5.TryRead[TArg](ReadStack& state, Utf8JsonReader& reader, JsonParameterInfo jsonParameterInfo, TArg& arg)
    at System.Text.Json.Serialization.Converters.SmallObjectWithParameterizedConstructorConverter`5.ReadAndCacheConstructorArgument(ReadStack& state, Utf8JsonReader& reader, JsonParameterInfo jsonParameterInfo)
    at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter`1.ReadConstructorArgumentsWithContinuation(ReadStack& state, Utf8JsonReader& reader, JsonSerializerOptions options)
    at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
    at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
    at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value)
    at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.ContinueDeserialize(ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack)
    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken)
    at Microsoft.Azure.Functions.Worker.Converters.JsonPocoConverter.GetConversionResultFromDeserialization(Byte[] bytes, Type type) in D:\a\_work\1\s\src\DotNetWorker.Core\Converters\JsonPocoConverter.cs:line 66
```

# 雑感

ちゃんとサンプル通りに実行していたら問題がなかったのですが、既存のデータベースを利用しようとしたり、色々変えてしまったために少し時間がかかってしまいました。

料理のさしすせそ（ネタ）通り、まずはレシピ通りやらないとダメですね。。