8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Azure FunctionsからMySQLに接続する~Java編~

Last updated at Posted at 2022-10-17

対象者

  • Azure FunctionsからAzure Database for MySQLに接続してみたい方
  • Javaで開発したい方
  • コマンドベースで操作したい方

概要

この記事では以下の内容について扱っています。
基本的にコマンドベースで進めていきます。

  • Azure Database for MySQLフレキシブル サーバー のリソース作成
  • Azure FunctionsからMySQLに接続
  • 【おまけ】Azure FunctionsでAPIキーを使用する

前提

前回投稿した記事「Azure FunctionsでAPIを作る~Java編~」の続きとなります。
Azure Functionsの開発環境構築、デプロイ方法については記事をご参照ください。

<前回のあらすじ>
  1. Azure Functionsのローカル開発環境構築(Java)
  2. 適当なダミーJSONデータを返すAPIを作成
    【今回】このデータをMySQLから取得するように変更(ついでに登録・更新・削除も追加)
  3. Azure へのデプロイ

動作環境

作業時の環境は以下の通りです。

  • OS
    • Windows 11 Pro (21H2)
    • Ubuntu 20.04 (WLS)
  • JDK
  • Azure CLI 2.40.0

データベースの準備

Azure Database for MySQL フレキシブル サーバー の作成

Azure Database for MySQLには「シングルサーバー」と「フレキシブルサーバー」の2種類がありますが、シングルサーバーは提供終了予定のため、フレキシブルサーバーが推奨されているようです。

MySQLサーバーの作成には az mysql flexible-server create コマンドを使用します。

Bash
az mysql flexible-server create \
    --name <MySQLサーバー名> \
    --resource-group <リソースグループ名> \
    --location eastus \
    --admin-user <Adminユーザー名> \
    --admin-password <Adminパスワード> \
    --sku-name Standard_B1s \
    --public-access 0.0.0.0 \
    --version 8.0.21
PowerShell
PowerShell
az mysql flexible-server create `
    --name <MySQLサーバー名> `
    --resource-group <リソースグループ名> `
    --location eastus `
    --admin-user <Adminユーザー名> `
    --admin-password <Adminパスワード> `
    --sku-name Standard_B1s `
    --public-access 0.0.0.0 `
    --version 8.0.21

オプションの補足

上記オプション指定では、MySQLサーバーはパブリックアクセスモードで作成されます。
パブリックといっても、ファイアウォールによって許可されたIPアドレスのみが接続可能です。
ファイアウォールを設定しない場合、どのIPからの接続も受け付けません。
--public-access0.0.0.0 に設定することで、Azure内リソースからMySQLサーバーにパブリックアクセス可能となるようファイアウォールが構成されます。

今回は触れませんが、VNet統合という仕組みを使うと、Azure FunctionsとMySQL間でプライベート通信を行うことが可能になるようです。

image.png

データベース作成

MySQLサーバーを作成すると、flexibleserverdb というデータベースが自動で作成されますが、新たに追加してみたいと思います。

データベースの作成には az mysql flexible-server db create コマンドを使用します。

Bash
az mysql flexible-server db create \
    --resource-group <リソースグループ名> \
    --server-name <MySQLサーバー名> \
    --charset utf8mb4 \
    --collation utf8mb4_bin \
    --database-name <データベース名> 
PowerShell
PowerShell
az mysql flexible-server db create `
    --resource-group <リソースグループ名> `
    --server-name <MySQLサーバー名> `
    --charset utf8mb4 `
    --collation utf8mb4_bin `
    --database-name <データベース名> 

文字コード(charset)、照合順序(collation)等はお好みで。

image.png

ファイアウォールルールの追加

次は作成したデータベースにテーブルを作成・・・と行きたいところなのですが、このままではクライアント端末からMySQLサーバーにアクセスすることができません。
上記で書いたように、アクセスするにはファイアウォールで接続元のIP(グローバルIP)を許可する必要があります。

ファイアウォールルールの追加には az mysql flexible-server firewall-rule create コマンドを使用します。

Bash
az mysql flexible-server firewall-rule create \
    --resource-group <リソースグループ名> \
    --name <MySQLサーバー名> \
    --rule-name <ファイアウォールルール名> \
    --start-ip-address <クライアントのIPアドレス> 
PowerShell
PowerShell
az mysql flexible-server firewall-rule create `
    --resource-group <リソースグループ名> `
    --name <MySQLサーバー名> `
    --rule-name <ファイアウォールルール名> `
    --start-ip-address <許可するIPアドレス> 

オプションの補足

start-ip-addressend-ip-address で範囲指定も可能です。
start-ip-address のみ指定した場合は指定した、IPのみが対象となります。

自分のIPアドレスを確認するには、「IPアドレス 確認」とかでググってください。
例えばここなどで確認できます。
(ここはコマンド使わんのかい!)

image.png

テーブルの作成

ファイアウォールルールの追加によりMySQLサーバーに接続できるようになっているはずなので、テーブルを作成します。

テーブル作成は az mysql flexible-server execute コマンドを使用して、クエリを実行します。

Bash
az mysql flexible-server execute \
    --name <MySQLサーバー名> \
    --admin-user <Adminユーザー名> \
    --admin-password <Adminパスワード> \
    --database-name <データベース名> \
    --querytext \
    "
        create table todo (
            id int(8) auto_increment,
            content varchar(60),
            done bit(1) not null default b'0',
            created_at datetime not null default current_timestamp,
            primary key (id)
        );
    "
PowerShell
PowerShell
az mysql flexible-server execute `
    --name <MySQLサーバー名> `
    --admin-user <Adminユーザー名> `
    --admin-password <Adminパスワード> `
    --database-name <データベース名> `
    --querytext `
    "
        create table todo (
            id int(8) auto_increment,
            content varchar(60),
            done bit(1) not null default b'0',
            created_at datetime not null default current_timestamp,
            primary key (id)
        );
    "

Azure Functions側の実装

細かい実装の話が続くので、面倒くさい方は環境変数の設定までスキップしてください。
ここで特筆したいのは接続情報を環境変数から取ってくるという部分です。

※ソースは抜粋しているので、全容はGithubをご覧ください

ORマッパーの導入

せっかくなのでORマッパーを導入したいと思います。
今回は MyBatis を使用します。(SQLは自分で書きたいし、コードと分離したい派なので...)
本筋から外れるので、MyBatisの説明は割愛します。
興味のある方は調べてみてください。

どのORマッパーを選んだらよいか迷ったときは、こちらのスライドがとても参考になります。(クリックで展開)

依存関係の追加

MyBatisとMySQLコネクタを使用するため、pom.xmlに依存関係を追加します。

pom.xml (一部抜粋)
・・・
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>
・・・

設定ファイルの追加

MyBatisの設定ファイルを追加します。
DBの接続情報をソース管理されるソースに含めるのはセキュリティ上よろしくないので、変数化(${xxx})しています。
この変数には後述の環境変数の値が入ります。

mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <typeAliases>
    <package name="com.azure_functions.models"/>
  </typeAliases>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <!-- ↓ポイント↓ -->
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
        <!-- ↑ポイント↑ -->
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <package name="com.azure_functions.mappers"/>
  </mappers>
</configuration>

SQL定義の追加

実際に発行されるSQL文を定義します。

TodoItemMapper.xml (一部抜粋)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.azure_functions.mappers.TodoItemMapper">
  <!-- TODO一覧取得 -->
  <select id="selectTodo" resultType="TodoItem">
    select
        id
      , content
      , done
      , created_at
    from
      todo
  </select>
  ・・・
</mapper>

SQLとJavaをつなぐインターフェースを追加

JavaからSQLを呼び出すためにインターフェースを追加します。
メソッド名とSQL定義id が紐づきます。

TodoItemMapper.java (一部抜粋)
/**
 * TodoItemのマッパー
 */
public interface TodoItemMapper {
  /**
   * TODO一覧を全件取得
   *
   * @return TODO一覧
   */
  List<TodoItem> selectTodo();
  ・・・
}

DB接続周りのコードを追加

接続情報を環境変数から取得して、設定ファイルで変数化していた部分にセットしています。

SqlSessionManager.java
@RequiredArgsConstructor
public class SqlSessionManager {

  /** SQLセッションファクトリー */
  private static SqlSessionFactory sqlSessionFactory = sqlSessionFactory();

  /** 実行コンテキスト */
  private final ExecutionContext context;

  private static SqlSessionFactory sqlSessionFactory() {
    // ↓ポイント↓
    // DB接続情報を環境変数から取得
    Properties mybatisProps = new Properties();
    mybatisProps.put("url", System.getenv("MYSQL_URL"));
    mybatisProps.put("username", System.getenv("MYSQL_USER"));
    mybatisProps.put("password", System.getenv("MYSQL_PASSWORD"));
    // ↑ポイント↑

    try (Reader config = Resources.getResourceAsReader("mybatis-config.xml")) {
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config, mybatisProps);
      return sqlSessionFactory;

    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
  ・・・
}

モデルの修正

作成したテーブルのカラムにあわせて、モデルを変更します。

TodoItem.java
@Data
@Builder
@JsonDeserialize(builder = TodoItem.TodoItemBuilder.class)
public class TodoItem {

  // ↓追加↓
  /** ID */
  private int id;
  // ↑追加↑

  /** 内容 */
  private String content;

  /** 完了フラグ */
  private boolean done;

  /** 作成日時 */
  private LocalDateTime createdAt;

}

エンドポイントの追加

前回はTODO一覧を取得するためのエンドポイントのみでしたが、登録・更新・削除用のエンドポイントを追加します。

HTTPトリガーの追加

@HttpTrigger を付与したメソッドを作成します。
また、前回対応しなかったAPIキーの使用についても対応しています。
(authLevelAuthorizationLevel.FUNCTION に変更するだけです!)

Function.java (一部抜粋)
public class Function {

  /**
   * TODO一覧を取得
   *
   * @param request リクエスト
   * @param context 実行コンテキスト
   * @return レスポンス
   */
  @FunctionName("fetchTodoItems")
  public HttpResponseMessage fetchTodoList(
    @HttpTrigger(
      name = "req",
      methods = { HttpMethod.GET },
      // 承認レベルをFUNCTIONに変更(APIキーが必要となる)
      authLevel = AuthorizationLevel.FUNCTION,
      // エンドポイントを「/api/todos」に設定
      route = "todos"
    ) HttpRequestMessage<Optional<String>> request,
    final ExecutionContext context
  ) {
    ・・・
  }

  /**
   * TODOを登録
   *
   * @param request リクエスト
   * @param context 実行コンテキスト
   * @return レスポンス
   */
  @FunctionName("createTodoItem")
  public HttpResponseMessage createTodoItem(
    @HttpTrigger(
      name = "req",
      methods = { HttpMethod.POST },
      authLevel = AuthorizationLevel.FUNCTION,
      // エンドポイントを「/api/todos」に設定
      route = "todos"
    ) HttpRequestMessage<Optional<String>> request,
    final ExecutionContext context
  ) {
    ・・・
  }

  /**
   * TODOを更新
   *
   * @param request リクエスト
   * @param context 実行コンテキスト
   * @return レスポンス
   */
  @FunctionName("updateTodoItem")
  public HttpResponseMessage updateTodoItem(
    @HttpTrigger(
      name = "req",
      methods = { HttpMethod.PATCH },
      authLevel = AuthorizationLevel.FUNCTION,
      // エンドポイントを「/api/todos/<id>」に設定
      route = "todos/{id:int}"
    ) HttpRequestMessage<Optional<String>> request,
    final ExecutionContext context,
    // パスのidをバインド
    @BindingName("id") int id
  ) {
    ・・・
  }

  /**
   * TODOを削除
   *
   * @param request リクエスト
   * @param context 実行コンテキスト
   * @return レスポンス
   */
  @FunctionName("deleteTodoItem")
  public HttpResponseMessage deleteTodoItem(
    @HttpTrigger(
      name = "req",
      methods = { HttpMethod.DELETE },
      authLevel = AuthorizationLevel.FUNCTION,
      // エンドポイントを「/api/todos/<id>」に設定
      route = "todos/{id:int}"
    ) HttpRequestMessage<Optional<String>> request,
    final ExecutionContext context,
    // パスのidをバインド
    @BindingName("id") int id
  ) {
    ・・・
  }
  ・・・
}

HTTPトリガーの設定内容(サマリ)

関数名 HTTPメソッド エンドポイント 承認レベル 処理
fetchTodoItems GET /api/todos FUNCTION 一覧取得
createTodoItem POST /api/todos FUNCTION 登録
updateTodoItem PATCH /api/todos/{id} FUNCTION 更新
deleteTodoItem DELETE /api/todos/{id} FUNCTION 削除

リクエストボディ(JSON)をオブジェクトに変換するためのデシリアライズ処理を追加

前回はオブジェクト→JSON変換のためにシリアライズ処理を実装しましたが、今回はその逆で、JSON→オブジェクト変換のためのデシリアライズ処理を実装します。

JsonUtil.java
/**
 * JSON関連のユーティリティ
 */
@RequiredArgsConstructor
public class JsonUtil {

  /** JSONシリアライズ用のマッパー */
  private static final ObjectMapper MAPPER = objectMapper();

  /** 実行コンテキスト */
  private final ExecutionContext context;

  private static ObjectMapper objectMapper() {
    ・・・
    // ↓追加↓
    // LocalDateTime用のデシリアライザを追加
    javaTimeModule.addDeserializer(
      LocalDateTime.class,
      new LocalDateTimeDeserializer(
        DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")
      )
    );
    // ↑追加↑
    ・・・

    return mapper;
  }

  ・・・
  // ↓追加↓
  /**
   * JSONからオブジェクトに変換
   *
   * @param <T>  オブジェクトの型
   * @param json JSON文字列
   * @param type オブジェクトの型
   * @return オブジェクト
   */
  public <T> T deserialize(String json, Class<T> type) {
    try {
      return MAPPER.readValue(json, type);
    } catch (JsonProcessingException e) {
      context
        .getLogger()
        .severe(
          "JSON→オブジェクトの変換に失敗しました。エラーメッセージ:" +
          e.getMessage()
        );
      throw new IllegalArgumentException(e);
    }
  }
  // ↑追加↑
}

データ操作ロジックの変更

前回ダミーデータを生成していた部分を、DBから取得するように変更します。
また、今回追加した登録・更新・削除についてもロジックを追加します。

TodoService.java (一部抜粋)
/**
 * TODOサービス
 */
@RequiredArgsConstructor
public class TodoService {

  /** 実行コンテキスト */
  private final ExecutionContext context;

  /** SQLセッション管理 */
  private final SqlSessionManager sqlSessionManager;

  /**
   * TODO一覧を取得
   *
   * @return TODO一覧
   */
  public List<TodoItem> fetchTodoItems() {
    context.getLogger().info("fetchTodoItemsが呼び出されました。");

    // ↓DBからデータ取得するよう変更↓
    // DBからTODO一覧取得
    List<TodoItem> todoItems = sqlSessionManager.transaction(
      sqlSession -> {
        TodoItemMapper todoItemMapper = sqlSession.getMapper(
          TodoItemMapper.class
        );
        return todoItemMapper.selectTodo();
      }
    );
    // ↑DBからデータ取得するよう変更↑

    return todoItems;
  }

  /**
   * TODOを登録
   * @param todo TODOインスタンス
   * @return 登録件数
   */
  public int insertTodo(TodoItem todo) {
    ・・・
  }

  /**
   * TODOを更新
   * @param todo TODOインスタンス
   * @return 更新件数
   */
  public int updateTodo(int id, TodoItem todo) {
    ・・・
  }

  /**
   * TODOを削除
   * @param id TODOのID
   * @return 削除件数
   */
  public int deleteTodo(int id) {
    ・・・
  }
}

環境変数の設定

ローカル実行とAzure上で環境変数の設定方法が異なります。

ローカル実行時の環境変数

Mavenプロジェクト作成時に作成されている local.settings.json 内の Values に設定を追加します。
こうすることで、実行時に設定が読み込まれ、環境変数にセットされます。

local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java",
    // ↓追加↓
    "MYSQL_URL": "jdbc:mysql://<MySQLサーバー名>.mysql.database.azure.com:3306/<データベース名>?useSSL=true",
    "MYSQL_USER": "<Adminユーザー>",
    "MYSQL_PASSWORD": "<Adminパスワード>"
    // ↑追加↑
  }
}

◎個人的躓きポイント

local.settings.json は、テスト時は読み込まれません
そのため、テスト実行する場合は事前にターミナル等で環境変数を設定しておく必要があります。

mvn clean package

でビルドした場合、デフォルトではテストが実行されてしまうので、スキップしたい場合は以下のようにオプションを指定する

mvn clean package -DskipTests=true

または、pom.xml に設定を追加してデフォルトの挙動を変更します。

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    ・・・
    <properties>
        ・・・
        <!-- ↓追加↓ -->
        <skipTests>true</skipTests>
        <!-- ↑追加↑ -->
    </properties>

Azure上にデプロイした関数アプリの環境変数(アプリケーション設定)

Azure上にデプロイした関数アプリの環境変数(アプリケーション設定)を追加するには
az functionapp config appsettings set コマンドを使用します。

Bash
az functionapp config appsettings set \
    --resource-group <リソースグループ> \
    --name <関数アプリ名> \
    --settings \
    MYSQL_URL=jdbc:mysql://<MySQLサーバー名>.mysql.database.azure.com:3306/<データベース名>?useSSL=true \
    MYSQL_USER=<Adminユーザー> \
    MYSQL_PASSWORD=<Adminパスワード>
PowerShell
PowerShell
az functionapp config appsettings set `
    --resource-group <リソースグループ> `
    --name <関数アプリ名> `
    --settings `
    MYSQL_URL=jdbc:mysql://<MySQLサーバー名>.mysql.database.azure.com:3306/<データベース名>?useSSL=true `
    MYSQL_USER=<Adminユーザー> `
    MYSQL_PASSWORD=<Adminパスワード>

ここではアプリケーション設定に設定値をべた書きしましたが、Key Vault(Keyコンテナー) というサービスを使用することで、シークレットを一元管理して複数のアプリで共有するといったことも可能です。
↓の記事が参考になりました。
[Azure] Azure FunctionsからAzure Key Vaultの値を参照する - Qiita

image.png

最終的なフォルダ構成

azure-functions
├── README.md
├── host.json
├── local.settings.json -------------------------------- 変更
├── pom.xml -------------------------------------------- 変更
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── azure_functions
    │   │           ├── Function.java ------------------ 変更
    │   │           ├── db
    │   │           │   └── SqlSessionManager.java ----- 追加
    │   │           ├── mappers
    │   │           │   └── TodoItemMapper.java -------- 追加
    │   │           ├── models
    │   │           │   └── TodoItem.java -------------- 変更
    │   │           ├── services
    │   │           │   └── TodoService.java ----------- 変更
    │   │           └── utils
    │   │               └── JsonUtil.java -------------- 変更
    │   └── resources
    │       ├── com
    │       │   └── azure_functions
    │       │       └── mappers
    │       │           └── TodoItemMapper.xml --------- 追加
    │       └── mybatis-config.xml --------------------- 追加
    └── test

デプロイ

アプリをAzureにデプロイします。
デプロイ手順は前回と同様なので割愛します。

【デプロイ後】
image.png

動作確認

APIキーの確認

今回は関数にAPIキーを設定しているため、パラメータとしてAPIキーを指定する必要があります。
事前にコマンド叩いて確認します。

Bash
# 関数アプリのホストを取得
azFunctionHost=$(az functionapp show \
        --resource-group <リソースグループ名> \
        --name <関数アプリ名> \
        --query "defaultHostName" | sed 's/"//g') # 「"」を除去

# APIキーを取得
azFunctionKey=$(az functionapp keys list \
        --resource-group <リソースグループ名> \
        --name <関数アプリ名> \
        --query "functionKeys.default" | sed 's/"//g') # 「"」を除去
PowerShell
PowerShell
# 関数アプリのホストを取得
$azFunctionHost = (az functionapp show `
        --resource-group <リソースグループ名> `
        --name <関数アプリ名> `
        --query "defaultHostName").Trim('"') # 「"」を除去

# APIキーを取得
$azFunctionKey = (az functionapp keys list `
        --resource-group <リソースグループ名> `
        --name <関数アプリ名> `
        --query "functionKeys.default").Trim('"') # 「"」を除去

上記コマンドの出力結果はJSONですが、--query オプションで絞り込み・ソート・整形ができます。
--query オプションには JMESPath というJSON向けのクエリを指定することができます。

image.png

API呼び出し

code パラメータにAPIキーを指定します。

一覧

curl -i -X GET https://${azFunctionHost}/api/todos?code=${azFunctionKey}

登録

Bash
curl -i -X POST -d '{\"content\":\"Azureの資格取得\"}' https://${azFunctionHost}/api/todos?code=${azFunctionKey}
PowerShell
PowerShell
# 日本語が文字化けするので一旦データをファイルに吐いて、ファイル指定にする
$tmpFile = New-TemporaryFile
Write-Output '{"content":"Azureの資格取得"}' | Set-Content $tmpFile
curl -i -X POST -d @$tmpFile https://${azFunctionHost}/api/todos?code=${azFunctionKey}
Remove-Item $tmpFile

更新

Bash
curl -i -X PATCH -d '{\"content\":\"Azureの資格取得(AZ-900)\", \"done\":true}' https://${azFunctionHost}/api/todos/1?code=${azFunctionKey}
PowerShell
PowerShell
# 日本語が文字化けするので一旦データをファイルに吐いて、ファイル指定にする
$tmpFile = New-TemporaryFile
Write-Output '{"content":"Azureの資格取得(AZ-900)", "done":true}' | Set-Content $tmpFile
curl -i -X PATCH -d @$tmpFile https://${azFunctionHost}/api/todos/1?code=${azFunctionKey}
Remove-Item $tmpFile

削除

curl -i -X DELETE  https://${azFunctionHost}/api/todos/1?code=${azFunctionKey}

まとめ

  • MySQLサーバーにローカルから接続するときはファイアウォールでIPを許可する
  • DBの接続情報は環境変数から取ってくる

これで簡単なバックエンドの処理ができたので、次はフロント部分からAPIを呼び出せるようにして、簡易的なSPAアプリ的な感じにしてみたいと思います。

2022/10/24 続きの記事を投稿しました

参考文献

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?