MyBatisのMapper実装を比較する

お仕事で利用しているMyBatis、自分でSQLを書いてO/Rマッピングするためテーブル設計ガタガタでもとりあえず小回り利くのが魅力です。

オブジェクトとSQLをマッピングするためにMapperを実装しますが、近年はこのMapperにもいろいろな実装方法があるようなので、まとめてみました。

> 私はオールドユーザなのでお仕事では従来のMapper XMLしか使いませんが。

今回は以下のMapperを比較してみます。

* 従来のXMLベースのMapper
* アノテーションベースのMapper
* SQLプロバイダベースのMapper
* MyBatis Dynamic SQLのMapper
* MyBatis Dynamic SQLのCommon Mapper

> MyBatis Generatorのような自動生成APIは使いません。

## 構成情報

* ライブラリ
  * mybatis 3.5.6
  * mybatis-spring 2.0.6
  * mybatis-dynamic-sql 1.2.1
  * lombok 1.18.16

* テーブル

```sql
create table if not exists todo (
    todo_id varchar(36) primary key,
    todo_title varchar(30),
    finished boolean,
    created_at timestamp
)
```

* エンティティ

```java
@Data
public class Todo implements Serializable {
    private static final long serialVersionUID = 1L;

    private String todoId;
    private String todoTitle;
    private Boolean finished;
    private Date createdAt;
}
```
`Todo`クラスは取得したデータをマッピングするだけでなく、SQLの入力パラメータとしても使うことにします。このため`finished`は`true/false`と未入力を選択できるよう`Boolean`型にしておきます。

* MyBatisの設定

```xml
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true" /> <!--(1)-->
    </settings>
    <typeAliases>
        <typeAlias alias="Todo" type="domain.Todo"/> <!--(2)-->
    </typeAliases>    
</configuration>
```

| No. | Description |
|----|----|
| 1 | MyBatisではParameterMap・ResultMapを定義することでオブジェクトのプロパティとSQLのカラムを柔軟にマッピングできますが、コード量が増えるのが嫌なので自動マッピングに頼ることにします。<br>`mapUnderscoreToCamelCase`をONにすると、キャメルケースのプロパティ名とスネークケースのカラム名を自動でマッピングしてくれます。 |
| 2 | XMLベースのMapperで使うため、エンティティのタイプエイリアスを指定しています。 |

## 従来のXMLベースのMapper

[Mapper XML ファイル](https://mybatis.org/mybatis-3/ja/sqlmap-xml.html)

まずはみんな大好きXMLベースのMapperです。
XMLをSQL Mapsと呼ぶようなオールドユーザにはこれこそがMyBatisです。

### Mapperインターフェイス

```java
public interface TodoRepository {

    Optional<Todo> findById(String todoId); // １件取得 (1)
    Collection<Todo> findAll(); // 全件取得
    void create(Todo todo); // 挿入
    boolean update(Todo todo); // 更新
    void delete(Todo todo); // 削除
    long count(Todo todo); // 条件に合致するレコードをカウント (2)
}
```

Mapperインターフェイスではメソッドシグネチャのみ定義します。

| No. | Description |
|----|----|
| 1 | １件取得の戻り値を`Optional`でラップすると、よりタイプセーフに使えます。 |
| 2 | パラメータに`Todo`を渡して、動的SQLを実装してみます。 |

### Mapper XML

```xml
<?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="repository.TodoRepository">

    <!--(2)-->
    <select id="findById" resultType="Todo">
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at
        FROM
            todo
        WHERE
            todo_id = #{todoId} <!--(3)-->
    </select>

    <select id="findAll" resultType="Todo">
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at
        FROM
            todo
    </select>

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

    <update id="update" parameterType="Todo">
        UPDATE todo
        SET
            todo_title = #{todoTitle},
            finished = #{finished},
            created_at = #{createdAt}
        WHERE
            todo_id = #{todoId}
    </update>

    <delete id="delete" parameterType="Todo">
        DELETE FROM
            todo
        WHERE
            todo_id = #{todoId}
    </delete>

    <select id="count" parameterType="Todo" resultType="_long">
        SELECT
            COUNT(*)
        FROM
            todo
        <!--(4)-->
        <where>
            <if test="todoId != null">
                AND todo_id = #{todoId}
            </if>
            <if test="todoTitle != null">
                AND todo_title = #{todoTitle}
            </if>
            <if test="finished != null">
                AND finished = #{finished}
            </if>
            <if test="createdAt != null">
                AND createdAt = #{createdAt}
            </if>
        </where>
    </select>

</mapper>
```

Mapper XMLではインターフェイスに対応するSQLを定義します。
基本的に、動作確認済みのSQLをコピペしてMyBatisの文法をはめ込んであげると良いです。

| No. | Description |
|----|----|
| 1 | `namespace`でインターフェイスと関連付けます。 |
| 2 | `<select>`タグ等でインターフェイスのメソッドと関連付けます。<br>`parameterType`は引数が１つでJava標準でない型のときに指定します。<br>`resultType`は戻り値があるときに指定します。 |
| 3 | SQLパラメータに`#{引数またはそのプロパティ名}`を指定します。 |
| 4 | `where`タグや`if`タグでSQLを動的に生成することができます。 |

> テーブル結合したSQLで取得したデータをネストしたオブジェクトに自動マッピングするときは、`customer_name AS "customer.customer_name"`のように、キャメルケースに直すとEL式でプロパティを指定するようなカラム名に調整すればOKです。

### XMLベースのメリット

* **IDEのプラグイン（EclipseならMybatipse）によりコード補完が効く**ので、意外なほど実装しやすいです。ただし、SQLとパラメータは補完が効かないので、できるだけXMLではSQLを記述しない＝**動作確認済みのSQLをコピペすることをオススメ**します。

* XMLに記述した**SQLの可読性が高い**です。ただし、`<sql>`＋`<include>`タグを利用してSQLの断片化を行うと可読性を確保するのは難しくなってきます。

### XMLベースのデメリット

* Javaの**リファクタリングに弱い**です。インターフェイスのパッケージ移動、メソッドシグネチャやプロパティ名の変更など、いたる所でJavaとXMLの壁を感じます。

* **自動フォーマットに弱い**です。SQLはすべて単純文字列として扱われるため、XMLファイルをIDEの機能などで自動フォーマットするとインデントが崩れます。SQL全体を`<![[CDATA]]>`タグで囲むよう指導する人もいますが、逆に**`<if>`タグが効かないトラブルに陥りやすい**のでオススメしません。

## アノテーションベースのMapper

[Mapper アノテーション](https://mybatis.org/mybatis-3/ja/java-api.html#Mapper_.E3.82.A2.E3.83.8E.E3.83.86.E3.83.BC.E3.82.B7.E3.83.A7.E3.83.B3)

次に比較的新しいアノテーションベースのMapperです。
何でもアノテーションがやってくれると思ってるSpring Bootユーザのような方向けです。

### Mapperインターフェイス

```java
// (1)
@Mapper
public interface TodoRepository {

    // (2)
    @Select("SELECT"
            + "  todo_id,"
            + "  todo_title,"
            + "  finished,"
            + "  created_at"
            + " FROM todo"
            + " WHERE"
            + "  todo_id = #{todoId}")
    Optional<Todo> findById(String todoId);

    @Select("SELECT"
            + "  todo_id,"
            + "  todo_title,"
            + "  finished,"
            + "  created_at"
            + " FROM todo")
    Collection<Todo> findAll();

    @Insert("INSERT INTO todo ("
            + "  todo_id,"
            + "  todo_title,"
            + "  finished,"
            + "  created_at"
            + " ) VALUES ("
            + "  #{todoId},"
            + "  #{todoTitle},"
            + "  #{finished},"
            + "  #{createdAt}"
            + " )")
    void create(Todo todo);

    @Update("UPDATE todo"
            + " SET"
            + "  todo_title = #{todoTitle},"
            + "  finished = #{finished},"
            + "  created_at = #{createdAt}"
            + " WHERE"
            + "  todo_id = #{todoId}")
    boolean update(Todo todo);

    @Delete("DELETE FROM todo"
            + " WHERE"
            + "  todo_id = #{todoId}")
    void delete(Todo todo);

    // (3)
    @Select("<script>"
            + " SELECT"
            + "  COUNT(*)"
            + " FROM todo"
            + "<where>"
            + "<if test='todoId != null'>"
            + "  AND todo_id = #{todoId}"
            + "</if>"
            + "<if test='todoTitle != null'>"
            + "  AND todo_title = #{todoTitle}"
            + "</if>"
            + "<if test='finished != null'>"
            + "  AND finished = #{finished}"
            + "</if>"
            + "<if test='createdAt != null'>"
            + "  AND created_at = #{createdAt}"
            + "</if>"
            + "</where>"
            + "</script>")
    long count(Todo todo);
}
```

Mapper XMLの代わりに、インターフェイスでSQLを定義します。

| No. | Description |
|----|----|
| 1 | インターフェイスに`@Mapper`をつけます。 |
| 2 | メソッドの`@Select`や`@Insert`にSQLを記述します。 |
| 3 | 動的SQLを定義するときは、SQLを`<script>`タグで囲みます。 |

> デフォルトではXMLタグを利用して動的SQLを定義します。XMLベースのMapperに比べるとタグは単なる文字列として書くのでコード補完が効きません。

### アノテーションベースのメリット

* **XMLファイルを作らなくて良い**のが最大かつ唯一のメリットです。XMLベースで起こったJavaとXMLの壁によるバグが起こりにくいです。

簡単なSQLを定義するだけならこちらのほうが楽で良いですね。
~~もっと簡単に実装したいならJPAとかSpring JDBC使えば良いんじゃない？~~

### アノテーションベースのデメリット

* XMLベースのMapperと違い**動的SQLの実装でコード補完が効かない**ので、実装しにくいです。

* アノテーションにSQLを記述するので、当然ながら**SQLが非常に横長**になります。
コード例では可読性を担保するためXMLと同じように見えるよう改行して文字列連結してますが、こうすべきと言っている人はほとんど見たことがありませんw

横長にしろ文字列連結にしろ、とにかくSQLがメンテしづらいです。
個人的にはデメリットしか感じないので、大きい開発では使いたくないですね。

公式にも以下のようにあります。

> 残念ながら、アノテーションの表現力と柔軟性には制限があります。調査や試行錯誤に多くの時間を費やしたにも関わらず、複雑なマッピングをアノテーションで実現することはできません。（例えば）C# の Attributes にはこのような制限が無いので、MyBatis.NET では XML の代わりに Attributes を活用することができます。とは言っても、Java のアノテーションにも利点が無いわけではありません。

## SQLプロバイダベースのMapper

[Mapper アノテーションのサンプル](https://mybatis.org/mybatis-3/ja/java-api.html#Mapper_.E3.82.A2.E3.83.8E.E3.83.86.E3.83.BC.E3.82.B7.E3.83.A7.E3.83.B3.E3.81.AE.E3.82.B5.E3.83.B3.E3.83.97.E3.83.AB)

次にアノテーションベースの派生でSQLプロバイダベースのMapperです。
何でもアノテーションがやってくれる幻想を殺されたユーザが縋る救世主（メシア）です。

SQLプロバイダは動的SQLをサポートするための仕組みで、メソッドベースでSQLを構築することができます。今回は動的に限らずすべてのSQLをこれで実装します。

### Mapperインターフェイス

```java
// (1)
@Mapper
public interface TodoRepository {

    // (2)
    @SelectProvider(TodoSqlProvider.class)
    Optional<Todo> findById(String todoId);

    @SelectProvider(TodoSqlProvider.class)
    Collection<Todo> findAll();

    @InsertProvider(TodoSqlProvider.class)
    void create(Todo todo);

    @UpdateProvider(TodoSqlProvider.class)
    boolean update(Todo todo);

    @DeleteProvider(TodoSqlProvider.class)
    void delete(Todo todo);

    @SelectProvider(TodoSqlProvider.class)
    long count(Todo todo);

    // (3)
    class TodoSqlProvider implements ProviderMethodResolver {

        // (4)
        public String findById(String todoId) {
            return new SQL() {{
                SELECT("todo_id", "todo_title", "finished", "created_at");
                FROM("todo");
                WHERE("todo_id = #{todoId}");
            }}.toString();
        }

        public String findAll() {
            return new SQL() {{
                SELECT("todo_id", "todo_title", "finished", "created_at");
                FROM("todo");
            }}.toString();
        }

        public String create(Todo todo) {
            return new SQL() {{
                INSERT_INTO("todo");
                VALUES("todo_id", "#{todoId}");
                VALUES("todo_title", "#{todoTitle}");
                VALUES("finished", "#{finished}");
                VALUES("created_at", "#{createdAt}");
            }}.toString();
        }

        public String update(Todo todo) {
            return new SQL() {{
                UPDATE("todo");
                SET("todo_title = #{todoTitle}");
                SET("finished = #{finished}");
                SET("created_at = #{createdAt}");
                WHERE("todo_id = #{todoId}");
            }}.toString();
        }

        public String delete(Todo todo) {
            return new SQL() {{
                DELETE_FROM("todo");
                WHERE("todo_id = #{todoId}");
            }}.toString();
        }

        // (5)
        public String count(Todo todo) {
            return new SQL() {{
                SELECT("COUNT(*)");
                FROM("todo");
                if (todo.getTodoId() != null) {
                    WHERE("todo_id = #{todoId}");
                }
                if (todo.getTodoTitle() != null) {
                    WHERE("todo_title = #{todoTitle}");
                }
                if (todo.getFinished() != null) {
                    WHERE("finished = #{finished}");
                }
                if (todo.getCreatedAt() != null) {
                    WHERE("created_at = #{createdAt}");
                }
            }}.toString();
        }
    }
}
```

メソッドベースでSQLを構築するSQLプロバイダを実装します。

> SQLプロバイダは通常別クラスとして実装しますが、今回はインターフェイスと不可分なのでインナークラスとして実装しています。

| No. | Description |
|----|----|
| 1 | インターフェイスに`@Mapper`をつけます。 |
| 2 | メソッドの`@SelectProvider`や`@InsertProvider`にSQLプロバイダを指定します。 |
| 3 | `ProviderMethodResolver`を実装したSQLプロバイダを定義します。 |
| 4 | インターフェイスに対応したメソッドを定義し、`SQL`クラスを利用してSQLを構築します。 |
| 5 | 動的SQLの実装は、Javaなので感覚的に書くことができます。 |

> SQLプロバイダはクラス＋メソッド名を指定する必要がありますが、MyBatis 3.5.1からSQLプロバイダが`ProviderMethodResolver`を実装することで、メソッド名を自動解決してくれるようになりました。

> `SQL`クラスは上記例のようなイニシャライザではなく、メソッドチェインでSQLを構築することもできます。動的SQLの組み立てにはイニシャライザを利用したほうが簡単に書け、可読性も向上すると思います。

> `WHERE`や`SET`メソッドのパラメータ値には引数をセットすることもできますが、これまでと同様に**必ず`#{}`を利用**してください。`PreparedStatement`が利用され、SQLインジェクションを防止することができます。

### SQLプロバイダベースのメリット

* アノテーションベースより大きく**可読性がアップ**しました。
* **SQL構文がタイプセーフ**になり、Javaでメンテしやすくなりました。

あーこれこれ、こういうのだよ！って感じがしますね。

### SQLプロバイダベースのデメリット

* 実装するクラスが増えましたw アノテーションだから簡単だって言ったじゃない！

* SQL構文はタイプセーフですが、いたる所に**文字列でSQL断片を記載**しているのが分かると思います。`SQL`クラスの構文と従来のSQLを組み合わせて考える必要があり、少し慣れが必要そうです。

## MyBatis Dynamic SQLのMapper

[MyBatis Dynamic SQL Quick Start](https://mybatis.org/mybatis-dynamic-sql/docs/quickStart.html)

ここからは素のMyBatisではなく、MyBatis Dynamic SQLのMapperです。
MyBatis Dynamic SQLはC#で言うところのLINQ to Entitiesのような機能で、**Mapperを利用する側でメソッドチェインにSQLを構築する**ことができます。
 
時代はメソッドチェインだ！SQLなんぞ書かん！とMyBatisを根底から否定するMapperです。

```xml
<dependency>
    <groupId>org.mybatis.dynamic-sql</groupId>
    <artifactId>mybatis-dynamic-sql</artifactId>
    <version>1.2.1</version>
</dependency>
```

利用するにはライブラリを追加する必要があります。

### Mapperインターフェイス

> 公式チュートリアルでは実装量が多いと感じたので、できる限り省ける実装は省いています。正式な実装は公式を参考にしてください。

```java
// (1)
@Mapper
public interface TodoRepository {

    // (2)
    @SelectProvider(type = SqlProviderAdapter.class, method = "select")
    Optional<Todo> findOne(SelectStatementProvider statement);

    @SelectProvider(type = SqlProviderAdapter.class, method = "select")
    Collection<Todo> findAll(SelectStatementProvider statement);

    @InsertProvider(type = SqlProviderAdapter.class, method = "insert")
    void create(InsertStatementProvider<Todo> statement);

    @UpdateProvider(type = SqlProviderAdapter.class, method = "update")
    boolean update(UpdateStatementProvider statement);

    @DeleteProvider(type = SqlProviderAdapter.class, method = "delete")
    void delete(DeleteStatementProvider statement);

    @SelectProvider(type = SqlProviderAdapter.class, method = "select")
    long count(SelectStatementProvider statement);

    // (3)
    static final TodoTable TODO = new TodoTable();

    // (4)
    static final class TodoTable extends SqlTable {
        public final SqlColumn<String> todoId = column("todo_id");
        public final SqlColumn<String> todoTitle = column("todo_title");
        public final SqlColumn<Boolean> finished = column("finished");
        public final SqlColumn<Date> createdAt = column("created_at");

        public TodoTable() {
            super("todo");
        }
    }
}
```

SQLプロバイダを実装しない代わりに、SQLを構築するために利用するSQLテーブルを実装します。

> SQLテーブルは通常別クラスとして実装しますが、今回はインターフェイスと不可分なのでインナークラスとして実装しています。

| No. | Description |
|----|----|
| 1 | インターフェイスに`@Mapper`をつけます。 |
| 2 | メソッドの`@SelectProvider`や`@InsertProvider`に`SqlProviderAdapter`を指定します。<br>引数には今までと大きく異なり`SelectStatementProvider`等になります。 |
| 3 | `SelectStatementProvider`等でSQLを構築するためのSQLテーブルを公開します。 |
| 4 | `SqlTable`を拡張したSQLテーブルを実装します。<br>コンストラクタでテーブル名を、`SqlColumn`型フィールドでカラム名を指定します。 |

> 構築したいSQLの種類に応じて、`SqlProviderAdapter`には`select`や`insert`などのメソッドがあるので、発行したいSQLの種類に応じて指定してください。コード補完できないので`SqlProviderAdapter`のメソッドシグネチャを確認してください。

### Mapperの利用

```java
// (1)
import static com.example.todo.domain.repository.todo.TodoRepository.TODO;
import static org.mybatis.dynamic.sql.SqlBuilder.countFrom;
import static org.mybatis.dynamic.sql.SqlBuilder.deleteFrom;
import static org.mybatis.dynamic.sql.SqlBuilder.insert;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import static org.mybatis.dynamic.sql.SqlBuilder.select;
import static org.mybatis.dynamic.sql.SqlBuilder.update;
import static org.mybatis.dynamic.sql.render.RenderingStrategies.MYBATIS3;

public class TestService {

    @Autowired
    TodoRepository todoRepository;

    public Todo findOne(String todoId) {
        // (2)
        SelectStatementProvider select = select(TODO.todoId, TODO.todoTitle, TODO.finished, TODO.createdAt)
                .from(TODO)
                .where(TODO.todoId, isEqualTo(todoId))
                .build()
                .render(MYBATIS3);
        return todoRepository.findOne(select).orElseThrow(() -> ...);
    }

    public Collection<Todo> findAll() {
        SelectStatementProvider select = select(TODO.todoId, TODO.todoTitle, TODO.finished, TODO.createdAt)
                .from(TODO)
                .build()
                .render(MYBATIS3);
        return todoRepository.findAll(select);
    }

    public void create(Todo todo) {
        // (3)
        InsertStatementProvider<Todo> insert = insert(todo)
                .into(TODO)
                .map(TODO.todoId).toProperty("todoId")
                .map(TODO.todoTitle).toProperty("todoTitle")
                .map(TODO.finished).toProperty("finished")
                .map(TODO.createdAt).toProperty("createdAt")
                .build()
                .render(MYBATIS3);
        todoRepository.create(insert);
    }

    public void update(Todo todo) {
        UpdateStatementProvider update = update(TODO)
                .set(TODO.todoTitle).equalTo(todo.getTodoTitle())
                .set(TODO.finished).equalTo(true)
                .set(TODO.createdAt).equalTo(todo.getCreatedAt())
                .where(TODO.todoId, isEqualTo(todo.getTodoId()))
                .build()
                .render(MYBATIS3);
        todoRepository.update(update);
    }

    public void delete(String todoId) {
        DeleteStatementProvider delete = deleteFrom(TODO)
                .where(TODO.todoId, isEqualTo(todoId))
                .build()
                .render(MYBATIS3);
        todoRepository.delete(delete);
    }

    public long count(Todo todo) {
        SelectStatementProvider count = countFrom(TODO)
                .where(TODO.finished, isEqualTo(true))
                .build()
                .render(MYBATIS3);
        return todoRepository.count(count);
    }
}
```

Mapperを利用する側でSQLを構築します。

| No. | Description |
|----|----|
| 1 | SQL構文を構築する`SqlBuilder.*`、テーブルやカラムを指定するSQLテーブル`TodoRepository.TODO`、SQLを実行するプラットフォームを指定する`RenderingStrategies.MYBATIS3`をstaticインポートします。 |
| 2 | `SqlBuilder.*`を利用してSQLを構築し、Mapperの引数にセットして実行します。 |
| 3 | INSERT文の構築ではSQLテーブルのカラムとエンティティクラスのプロパティをマッピングする必要があります。 |

> `RenderingStrategies.SPRING_NAMED_PARAMETER`を指定することで、Spring JDBCの`NamedParameterJdbcTemplate`向けのSQLを構築することもできます。もはやMyBatisとは関係ないSQL構築テンプレートですね。

### MyBatis Dynamic SQLのMapperのメリット

* SQLプロバイダベースより**さらにタイプセーフ**になりました。
* 利用する側で自由にSQLを構築できるため、**Mapperの自由度が向上**しました。

### MyBatis Dynamic SQLのMapperのデメリット

* **利用する側の実装が煩雑**になりました。利用する側のバグが多くなりそうなので、大きな開発ではここからさらにSQLの共通化やレイヤリングを検討する必要がありそうですね。

* SELECT文やDELETE文は良いですが、**INSERT文やUPDATE文は自分でエンティティとマッピングする必要**があり、毎回利用する側でこれを書くのは面倒だなーと思いました。

## MyBatis Dynamic SQLのCommon Mapper

[MyBatis Dynamic SQL MyBatis3 Support](https://mybatis.org/mybatis-dynamic-sql/docs/mybatis3.html)

勘の良い人なら思うでしょう。Mapperインターフェイスはワンオフ（ガンダム）ではなく量産機（ジム）で十分だろうと。

MyBatis Dynamic SQLではSpring Data JPAの`CrudRepository`のような標準CRUDのインターフェイスが用意されています。
このCommon Mapperを利用して、Mapperインターフェイスをより簡単に実装することができます。 

### Mapperインターフェイス

> 公式チュートリアルでは実装量が多いと感じたので、できる限り省ける実装は省いています。正式な実装は公式を参考にしてください。

```java
// (1)
@Mapper
public interface TodoRepository extends CommonSelectMapper, CommonInsertMapper<Todo>, CommonUpdateMapper, CommonDeleteMapper, CommonCountMapper {
    // (2)

    // (3)
    static final TodoTable TODO = new TodoTable();

    // (4)
    static final class TodoTable extends SqlTable {
        public final SqlColumn<String> todoId = column("todo_id");
        public final SqlColumn<String> todoTitle = column("todo_title");
        public final SqlColumn<Boolean> finished = column("finished");
        public final SqlColumn<Date> createdAt = column("created_at");

        public TodoTable() {
            super("todo");
        }

        // (5)
        public final Function<Map<String, Object>, Todo> mapper = map -> {
            if (map == null) {
                return null;
            }
            Todo t = new Todo();
            Optional.ofNullable(map.get(todoId.aliasOrName().toUpperCase())).ifPresent(v -> t.setTodoId((String) v));
            Optional.ofNullable(map.get(todoTitle.aliasOrName().toUpperCase())).ifPresent(v -> t.setTodoTitle((String) v));
            Optional.ofNullable(map.get(finished.aliasOrName().toUpperCase())).ifPresent(v -> t.setFinished((Boolean) v));
            Optional.ofNullable(map.get(createdAt.aliasOrName().toUpperCase())).ifPresent(v -> t.setCreatedAt((Date) v));
            return t;
        };
    }
}
```

MyBatis Dynamic SQLのMapperと大きく違うのは、メソッドの定義が不要な点と、取得した結果とエンティティをマッピングする必要がある点です。

| No. | Description |
|----|----|
| 1 | インターフェイスに`@Mapper`をつけます。 |
| 2 | メソッドを定義する代わりに、`CommonSelectMapper`や`CommonInsertMapper`の拡張インターフェイスにします。 |
| 3 | `SelectStatementProvider`等でSQLを構築するためのSQLテーブルを公開します。 |
| 4 | `SqlTable`を拡張したSQLテーブルを実装します。<br>コンストラクタでテーブル名を、`SqlColumn`型フィールドでカラム名を指定します。 |
| 5 | 取得した結果をエンティティとマッピングするためのファンクションを定義します。 |

> エンティティとマッピングするファンクションは本来、利用する側で実装するものです。今回はテーブルとエンティティの対応が明確なので、SQLテーブルに実装しています。

### Mapperの利用

```java
// (1)
import static com.example.todo.domain.repository.todo.TodoRepository.TODO;
import static org.mybatis.dynamic.sql.SqlBuilder.countFrom;
import static org.mybatis.dynamic.sql.SqlBuilder.deleteFrom;
import static org.mybatis.dynamic.sql.SqlBuilder.insert;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import static org.mybatis.dynamic.sql.SqlBuilder.select;
import static org.mybatis.dynamic.sql.SqlBuilder.update;
import static org.mybatis.dynamic.sql.render.RenderingStrategies.MYBATIS3;

public class TestService {

    @Autowired
    TodoRepository todoRepository;

    public Todo findOne(String todoId) {
        // (2)
        SelectStatementProvider select = select(TODO.todoId, TODO.todoTitle, TODO.finished, TODO.createdAt)
                .from(TODO)
                .where(TODO.todoId, isEqualTo(todoId))
                .build()
                .render(MYBATIS3);
        // (3)
        return Optional.ofNullable(todoRepository.selectOne(select, TODO.mapper)).orElseThrow(() -> ...);
    }

    public Collection<Todo> findAll() {
        SelectStatementProvider select = select(TODO.todoId, TODO.todoTitle, TODO.finished, TODO.createdAt)
                .from(TODO)
                .build()
                .render(MYBATIS3);
        return todoRepository.selectMany(select, TODO.mapper);
    }

    public void create(Todo todo) {
        // (4)
        InsertStatementProvider<Todo> insert = insert(todo)
                .into(TODO)
                .map(TODO.todoId).toProperty("todoId")
                .map(TODO.todoTitle).toProperty("todoTitle")
                .map(TODO.finished).toProperty("finished")
                .map(TODO.createdAt).toProperty("createdAt")
                .build()
                .render(MYBATIS3);
        todoRepository.insert(insert);
    }

    public void update(Todo todo) {
        UpdateStatementProvider update = update(TODO)
                .set(TODO.todoTitle).equalTo(todo.getTodoTitle())
                .set(TODO.finished).equalTo(true)
                .set(TODO.createdAt).equalTo(todo.getCreatedAt())
                .where(TODO.todoId, isEqualTo(todo.getTodoId()))
                .build()
                .render(MYBATIS3);
        todoRepository.update(update);
    }

    public void delete(String todoId) {
        DeleteStatementProvider delete = deleteFrom(TODO)
                .where(TODO.todoId, isEqualTo(todoId))
                .build()
                .render(MYBATIS3);
        todoRepository.delete(delete);
    }

    public long count(Todo todo) {
        SelectStatementProvider count = countFrom(TODO)
                .where(TODO.finished, isEqualTo(true))
                .build()
                .render(MYBATIS3);
        return todoRepository.count(count);
    }
}
```

MyBatis Dynamic SQLのMapperと大きく違うのは、メソッドがCommon Mapperで定義された標準的なものになっている点と、取得した結果とエンティティをマッピングするファンクションを利用している点です。

| No. | Description |
|----|----|
| 1 | SQL構文を構築する`SqlBuilder.*`、テーブルやカラムを指定するSQLテーブル`TodoRepository.TODO`、SQLを実行するプラットフォームを指定する`RenderingStrategies.MYBATIS3`をstaticインポートします。 |
| 2 | `SqlBuilder.*`を利用してSQLを構築し、Mapperの引数にセットして実行します。 |
| 3 | 取得した結果をエンティティにマッピングするため、SQLテーブルに定義したファンクションを呼び出します。 |
| 4 | INSERT文の構築ではSQLテーブルのカラムとエンティティクラスのプロパティをマッピングする必要がある点は変わりません。 |

> `CommonSelectMapper#selectOne`は`Optional`に対応していません。上記例では他と実装を合わせるため、戻り値を`Optional`でラップしています。

### MyBatis Dynamic SQLのCommon Mapperのメリット

* **Mapperインターフェイスを標準化**することができます。

SQLの構築は利用する側でやるので、インターフェイスはCommon Mapperを利用して標準化するのが良さそうですね。

### MyBatis Dynamic SQLのCommon Mapperのデメリット

* **SELECT文で取得した結果をエンティティにマッピングする実装が必要**です。
* **`Optional`に対応していません。**

`CommonSelectMapper`だけ継承せずにSELECTだけ自分で定義しても良いかもしれませんね。

## まとめ

個人的にはDynamic SQLが好きですが、エンティティとのマッピングがやや煩雑なのと、ロジックとSQL構築の分離がしづらくなるため実開発での使用にはやや慎重になりそうです。
~~JPAとかSpring JDBC使えば良いんじゃない？勢もいるし~~

XMLベースとSQLプロバイダはどちらも実装しやすいので好みの問題かと思いますが、MyBatisを利用するモチベーションが複雑なSQLを扱う必要があることだと考えると、実行可能なSQLをそのまま貼り付けられるXMLベースにやや軍配が上がりそうかなーと。