[Azure] Functions(Python)からSQL DBにアクセスする

前回Functionsのデバッグ環境を整えたわけですが、今回はAzure SQLにFunctionsからアクセスしていきたいと思います。

:::note info
前回記事
[IoT Hubと接続したローカルAzure Functionsアプリをステップ実行デバッグする #Azure - Qiita](https://qiita.com/fsd_maclala/items/e18bbae6e9b63f1b9abd)
:::

## おおまかな流れ
1. パッケージのインストール
1. コードの確認
1. AzureFunctionsにpublish、そして確認
1. 余談、パッケージのバージョン指定


## パッケージのインストール
今回、Functionsの中でAzureSQLに接続する中で、最初はODBCを使うという選択をしていました。

私が参考にしたコードの中でpyodbcが使われていたんですね。

しかし、これはあんまり上手くない方法だと分かりました。

理由は、ODBCを使うにはシステムにODBCを使うためのインストール作業を行わなければならなかったからです。

aptにMicrosoftのリポジトリを追加して、システムにODBCをインストールし、そのインタフェースとしてPythonインタプリター側にpyodbcパッケージをインストールし、コード内で使っていく、という流れになります。

しかしこうなると、このODBC自体はFunctionsの環境に入っているのか？という疑問が当然起こります。

requirements.txtにパッケージ情報を書くことで、Pythonインタプリターにインターフェースとしてのパッケージをインストールすることはできますが、そのインタフェースの先にODBCコアがなければコードは正常に動きません。

それで調べてみたんですが、どうやらFunctionsの中にはODBC環境はないようでした。

ということで、ODBCを使わずにAzureSQLに接続する方法を探したところ、sqlalchemyと
pymssqlを使えばイケるということが分かりました。

ついては、これらをインストールしていきます。

```bash
pip install sqlalchemy
pip install pymssql
```

ついでにパスワード等、DB接続情報の安全運用を行うため

```bash
pip install python-dotenv
```
もやっておきます。

続いて、コードベースで説明していきます。

### コードの確認

こちらがFunctionsのコードの全文となります。

```python:__init__.py
import os
import logging
import json
from datetime import datetime
from sqlalchemy import create_engine, text

from azure.functions import EventHubEvent
from typing import List
from dotenv import load_dotenv

load_dotenv()


def main(events: List[EventHubEvent]):
    server = os.getenv('DB_SERVER')
    database = os.getenv('DB_NAME')
    username = os.getenv('DB_USER')
    password = os.getenv('DB_PASSWORD')

    # SQLAlchemy 接続文字列の作成
    connection_string = f"mssql+pymssql://{username}:{password}@{server}/{database}"
    # SQLAlchemy エンジンの作成
    engine = create_engine(connection_string)

    for event in events:
        # イベントデータの解析
        event_data = json.loads(event.get_body().decode('utf-8'))
        logging.info(
            f"Python EventHub trigger processed an event: {event_data}")

        temperature = event_data['temperature']
        humidity = event_data['humidity']
        create_time = datetime.now()

        try:
            # データベースへのデータ挿入
            with engine.connect() as connection:
                insert_statement = text(
                    "INSERT INTO test_table (create_time, temperature, humidity) VALUES (:create_time, :temperature, :humidity)"
                )
                # パラメータを辞書形式に変換
                params = {'create_time': create_time,
                          'temperature': temperature, 'humidity': humidity}
                connection.execute(insert_statement, params)
                connection.commit()
                logging.info("データベースにデータを挿入しました。")

        except Exception as e:
            logging.error(f"データベースへのデータ挿入中にエラーが発生しました: {e}")

```

### データベースの接続文字列の作り方

冒頭でSQLAlchemy向けのデータベース接続文字列を作っています。

```python
    server = os.getenv('DB_SERVER')
    database = os.getenv('DB')
    username = os.getenv('DB_USER')
    password = os.getenv('DB_PASSWORD')

    # SQLAlchemy 接続文字列の作成
    connection_string = f"mssql+pymssql://{username}:{password}@{server}/{database}"
```

この内容についてはデータベースの接続文字列の画面から取得することができます。

![スクリーンショット 2024-03-15 104658.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3249097/45e39905-9e53-b844-e5b2-76f226eb7851.png)

```
Server=tcp:<DB_SERVER>,1433;Initial Catalog=<DB_NAME>;Persist Security Info=False;User ID=<DB_USER>;Password=<DB_PASSWORD>;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
```

というようなフォーマットでDB接続に必要な各情報が収められていますので、ここから取得して環境変数として保持します。

### python-dotenvについて

環境変数に格納するにあたって、.envに書いておき、python-dotenvを使ってロードするとコードの中に重要情報を書き込むことなく、デバッグにもAzureにアップロードした後にも使えていい感じでした。

os.getenv()はローカルデバッグ時にはプロジェクト直下.envに書かれた情報を参照してくれます。

また、Azure環境上にアップロードされた後には、Functionsの「構成」で設定した環境変数を参照してくれます。

したがって、本番コードもデバッグコードも同じコードを使用することができます。

![スクリーンショット 2024-03-15 105512.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3249097/a778cc3a-3158-63a9-f61e-7e8fb36271dd.png)


### IoT Hubへの接続文字列の追加
構成には、忘れずIoT Hubへの接続文字列も追加しておきましょう。

これは前回の記事で紹介した function.js の ``` "connection": "IoTHubConnectionString" ```に当たる文字列のことです。ローカルデバッグのときにはlocal.settings.jsonのIoTHubConnectionStringの値を読みに行って接続していましたが、local.settings.jsonはFunction上にアップロードされないため、IoTHubConnectionStringの値はFunctionsの構成に設定しておく必要があります。

## AzureFunctionsにpublish、そして確認

ローカルデバッグで動作を確認したら、Functionsにデプロイします。

```bash
func azure functionapp publish <function-app-name> --python  
```

下記の表示が出たら成功です。

![スクリーンショット 2024-03-15 105925.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3249097/83dae788-b10f-862e-5880-578567098ad8.png)

アップロードできたら、その動作を確認します。

Functionsリソースの概要から、先ほどアップした\<function-app-name\>の書かれている「名前」欄の項目をクリックします。

![スクリーンショット 2024-03-15 110120.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3249097/8fab5b9e-e99c-60ca-26aa-c3f8b20ca9a4.png)

更に左側のペインからモニターの画面に行き、ログのタブをクリックしてしばらくすると```Application Insights に接続しています...```というメッセージが表示され```接続されました。```となります。

この状況でIoT Hubに対してメッセージを送信すると、リアルタイムでログが表示されます。

![スクリーンショット 2024-03-15 110829.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3249097/5da711fe-2c47-aab1-e6f1-13c8ed41e166.png)

データベースでクエリを発行すると、無事データはDBの中に収められていました。

ただ、create_timeに関してはAzureの中ではUTCが採用されるようなので、このあたり+9時間で入力するかどうかは考えものですね。
デバッグ時にはシステムの時間（日本時間）がcreate_timeに入ってくるので、常に日本時間を入力しようとすると、本番とデバッグ時でコードを変更する必要があり、これは中々面倒な問題です。

## 余談、パッケージのバージョン指定

最後に余談ですが、AzureFunctions上のランタイムにインストールされるリストとして、requirements.txtを用意する必要がありますが、こちらの内容について、私の場合はバージョンを指定しました。

```
greenlet==3.0.3
pymssql==2.2.11
python-dotenv==1.0.1
sqlalchemy==2.0.28
typing-extensions==4.10.0
```

バージョンを指定しない場合、常に最新のバージョンがインストールされることになると思います。
そうなると、バージョン互換性の問題が発生する可能性があります。

バージョンを指定していても、そのバージョンがパッケージリポジトリから削除されたりした場合、逆に問題の原因になりますが、まあバージョンは指定しておいたほうがいいのではないかなと思います。

これらバージョンについてはpoetryを使って抽出することができます。パッケージが依存しているパッケージのバージョンも固めることができ、より運用安全性が増すと思われます。

``` 
poetry export --without-hashes -f requirements.txt > requirements.txt 
```