MyBatis/iBatisのsqlmapファイルを解析してタグ情報＆パラメータを取得する

## 1． はじめに
かなり久しぶりのQiita投稿になります。とあるシステム開発プロジェクトのFW刷新においてDBアクセスに[iBatis](https://qiita.com/tags/ibatis)を利用していたものを[MyBatis](https://qiita.com/tags/mybatis)に変更する作業を行いました。設定ファイル（sqlmapファイル→mapperファイル）の変換処理は[ibatis2mybatis](https://github.com/mybatis/ibatis2mybatis)を利用して何とか終わりましたが、動的SQLの評価箇所を集中的に試験・確認したい状況になりました。そこで動的SQLの情報やパラメータ情報を取得したい必要性が発生して今回の記事となります。ソースコードについては量があるので[別の記事](https://qiita.com/5zm/items/65aeaf290315dd42dab1)としました。

## ２． 概要
MyBatisのmapperファイルやiBatisのSqlMapファイルを読み込み、XMLのタグ情報、属性情報、テキストノードの情報を取得、タグの入れ子構造を考慮して解析結果をJSON形式で標準出力に出力するツールです。標準出力なのでパイプやリダイレクトで他のコマンド（`grep`,`jq`等）やファイル出力に連携できます。
ポイントはテキストノード内のパラメータ（`#{xxx}`,`${xxx}`）やタグでパラメータを指定している属性を取得して纏めて分かるようにしているところです。なお、外部ライブラリの導入がいろいろメンドウな状況なのでJava17の標準機能のみで実装します。

![MyBatis.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195748/e7f2ad36-41f7-a39f-5337-58fb3c8e2470.jpeg)

## ３． 実行例

### ３．１． コマンド
```shell
java app.SqlMapAnalyze ./demo/TodoDemo.sqlmap.xml 
```

### ３．２． 入力のmapperファイル
```xml:TodoDemo.sqlmap.xml（TERASOLUNA 5.xのガイドラインから引用）
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- (1) -->
<mapper namespace="todo.domain.repository.todo.TodoRepository">

    <!-- (2) -->
    <resultMap id="todoResultMap" type="Todo">
        <id property="todoId" column="todo_id" />
        <result property="todoTitle" column="todo_title" />
        <result property="finished" column="finished" />
        <result property="createdAt" column="created_at" />
    </resultMap>

    <!-- (3) -->
    <select id="findOne" parameterType="String" resultMap="todoResultMap">
    <![CDATA[
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at
        FROM
            todo
        WHERE
            todo_id = #{todoId}
    ]]>
    </select>

    <!-- (4) -->
    <select id="findAll" resultMap="todoResultMap">
    <![CDATA[
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at
        FROM
            todo
    ]]>
    </select>

    <!-- (5) -->
    <insert id="create" parameterType="Todo">
    <![CDATA[
        INSERT INTO todo
        (
            todo_id,
            todo_title,
            finished,
            created_at
        )
        VALUES
        (
            #{todoId},
            #{todoTitle},
            #{finished},
            #{createdAt}
        )
    ]]>
    </insert>

    <!-- (6) -->
    <update id="update" parameterType="Todo">
    <![CDATA[
        UPDATE todo
        SET
            todo_title = #{todoTitle},
            finished = #{finished},
            created_at = #{createdAt}
        WHERE
            todo_id = #{todoId}
    ]]>
    </update>

    <!-- (7) -->
    <delete id="delete" parameterType="Todo">
    <![CDATA[
        DELETE FROM
            todo
        WHERE
            todo_id = #{todoId}
    ]]>
    </delete>

    <!-- (8) -->
    <select id="countByFinished" parameterType="Boolean"
        resultType="Long">
    <![CDATA[
        SELECT
            COUNT(*)
        FROM
            todo
        WHERE
            finished = #{finished}
    ]]>
    </select>

</mapper>
```

### ３．３． 解析結果のJSON

```json:解析結果のJSON（別途Pretty化したもの）
{
  "param": {
    "dirPath": "./demo/TodoDemo.sqlmap.xml",
    "fileSuffix": ".xml",
    "type": "-mybatis",
    "analyzeTime": "2024-02-26T22:48:54.681389"
  },
  "result": [
    {
      "filePath": "./demo/TodoDemo.sqlmap.xml",
      "sqlMap": {
        "nestLevel": "1",
        "tag": "mapper",
        "text": "",
        "params": null,
        "attributes": {
          "namespace": "todo.domain.repository.todo.TodoRepository"
        },
        "tags": [
          {
            "nestLevel": "2",
            "tag": "select",
            "text": " SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #{todoId} ",
            "params": [
              "todoId"
            ],
            "attributes": {
              "parameterType": "String",
              "resultMap": "todoResultMap",
              "id": "findOne"
            },
            "tags": []
          },
          {
            "nestLevel": "2",
            "tag": "select",
            "text": " SELECT todo_id, todo_title, finished, created_at FROM todo ",
            "params": [],
            "attributes": {
              "resultMap": "todoResultMap",
              "id": "findAll"
            },
            "tags": []
          },
          {
            "nestLevel": "2",
            "tag": "insert",
            "text": " INSERT INTO todo ( todo_id, todo_title, finished, created_at ) VALUES ( #{todoId}, #{todoTitle}, #{finished}, #{createdAt} ) ",
            "params": [
              "todoId",
              "todoTitle",
              "finished",
              "createdAt"
            ],
            "attributes": {
              "parameterType": "Todo",
              "id": "create"
            },
            "tags": []
          },
          {
            "nestLevel": "2",
            "tag": "update",
            "text": " UPDATE todo SET todo_title = #{todoTitle}, finished = #{finished}, created_at = #{createdAt} WHERE todo_id = #{todoId} ",
            "params": [
              "todoTitle",
              "finished",
              "createdAt",
              "todoId"
            ],
            "attributes": {
              "parameterType": "Todo",
              "id": "update"
            },
            "tags": []
          },
          {
            "nestLevel": "2",
            "tag": "delete",
            "text": " DELETE FROM todo WHERE todo_id = #{todoId} ",
            "params": [
              "todoId"
            ],
            "attributes": {
              "parameterType": "Todo",
              "id": "delete"
            },
            "tags": []
          },
          {
            "nestLevel": "2",
            "tag": "select",
            "text": " SELECT COUNT(*) FROM todo WHERE finished = #{finished} ",
            "params": [
              "finished"
            ],
            "attributes": {
              "parameterType": "Boolean",
              "id": "countByFinished",
              "resultType": "Long"
            },
            "tags": []
          }
        ]
      }
    }
  ]
}
```

## 4． さいごに
試しに解析してみたTodoDemo.sqlmap.xmlはタグの入れ子も浅く、また利用している動的SQLも少ないですが、プロジェクトのファイルはもっと複雑（入れ子も深く、動的SQLの数も多い）でした。最深の入れ子の動的SQLを有効にするには、その入れ子までの動的SQLを全て有効（TRUE）にしないと到達しません。そのような場合、XMLのタグの入れ子構造とパラメータにフォーカスした今回の方法は有効になるのではないでしょうか。次回はツールのソースコードについて説明したいと思います。