LoginSignup
57
56

More than 3 years have passed since last update.

MyBatisのMapper実装を比較する

Posted at

お仕事で利用している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
  • テーブル

create table if not exists todo (
    todo_id varchar(36) primary key,
    todo_title varchar(30),
    finished boolean,
    created_at timestamp
)
  • エンティティ
@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の入力パラメータとしても使うことにします。このためfinishedtrue/falseと未入力を選択できるようBoolean型にしておきます。

  • MyBatisの設定
<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のカラムを柔軟にマッピングできますが、コード量が増えるのが嫌なので自動マッピングに頼ることにします。
mapUnderscoreToCamelCaseをONにすると、キャメルケースのプロパティ名とスネークケースのカラム名を自動でマッピングしてくれます。
2 XMLベースのMapperで使うため、エンティティのタイプエイリアスを指定しています。

従来のXMLベースのMapper

Mapper XML ファイル

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

Mapperインターフェイス

public interface TodoRepository {

    Optional<Todo> findById(String todoId); // 1件取得 (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 1件取得の戻り値をOptionalでラップすると、よりタイプセーフに使えます。
2 パラメータにTodoを渡して、動的SQLを実装してみます。

Mapper 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>タグ等でインターフェイスのメソッドと関連付けます。
parameterTypeは引数が1つでJava標準でない型のときに指定します。
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 アノテーション

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

Mapperインターフェイス

// (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 アノテーションのサンプル

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

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

Mapperインターフェイス

// (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の組み立てにはイニシャライザを利用したほうが簡単に書け、可読性も向上すると思います。

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

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

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

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

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

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

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

MyBatis Dynamic SQLのMapper

MyBatis Dynamic SQL Quick Start

ここからは素のMyBatisではなく、MyBatis Dynamic SQLのMapperです。
MyBatis Dynamic SQLはC#で言うところのLINQ to Entitiesのような機能で、Mapperを利用する側でメソッドチェインにSQLを構築することができます。

時代はメソッドチェインだ!SQLなんぞ書かん!とMyBatisを根底から否定するMapperです。

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

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

Mapperインターフェイス

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

// (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@InsertProviderSqlProviderAdapterを指定します。
引数には今までと大きく異なりSelectStatementProvider等になります。
3 SelectStatementProvider等でSQLを構築するためのSQLテーブルを公開します。
4 SqlTableを拡張したSQLテーブルを実装します。
コンストラクタでテーブル名を、SqlColumn型フィールドでカラム名を指定します。

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

Mapperの利用

// (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

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

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

Mapperインターフェイス

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

// (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 メソッドを定義する代わりに、CommonSelectMapperCommonInsertMapperの拡張インターフェイスにします。
3 SelectStatementProvider等でSQLを構築するためのSQLテーブルを公開します。
4 SqlTableを拡張したSQLテーブルを実装します。
コンストラクタでテーブル名を、SqlColumn型フィールドでカラム名を指定します。
5 取得した結果をエンティティとマッピングするためのファンクションを定義します。

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

Mapperの利用

// (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#selectOneOptionalに対応していません。上記例では他と実装を合わせるため、戻り値を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ベースにやや軍配が上がりそうかなーと。

57
56
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
57
56