Posted at

絶対分かるMyBatis!MyBatisで覚えるべきチェックルール25(中編)


チェックルール(つづき)

絶対分かるMyBatis!MyBatisで覚えるべきチェックルール25(前半) のつづきになります。本来は残りのチェックルール(13~25)について説明するつもりでしたが、予想以上に長くなってしまったので3部構成に変更しました。今回はチェックルール(13~20)まで説明したいと思います。


13. Mapperファイル内で引数のJavaのデータを参照する場合、バインド変数#{...}を利用すること

MyBatisのデフォルトではJavaのPreparedStatementを利用してSQLを実行します。実はバインド変数#{...}PreparedStatementのパラメータとして設定されます。つまりパラメータが利用できる箇所でのみ、利用することができます。


  • SELECT文のWHERE句における値

  • INSERT文のVALUES句における値

  • UPDATE文のSET句における値、WHERE句における値

  • DELETE文のWHERE句における値

バインド変数#{...}の変数名は引数のJavaクラスのフィールド名、Mapの場合はキー名になります。

引数がプリミティブ型、ラッパークラス、Stringの場合は引数名にします。


CustomerRepostiory.java

public interface CustomerRepository {

void insert(Customer customer);
Customer findOne(String customerCode);
}


CustomerRepository.xml

<mapper namespace="com.example.demo.mybatis3.domain.repostiory.CustomerRepostiory">

<!-- 引数のデータ型はCustomer -->
<insert id="insert" parameterType="Customer">
INSERT INTO customer (
customer_code,
customer_name,
customer_pass,
customer_birth,
customer_mail
) VALUES (
#{customerCode},
#{customerName},
#{customerPass},
#{customerBirth},
#{customerMail}
)
</insert>

<!-- 引数のデータ型はString-->
<!-- 戻り値のデータ型はCustomer -->
<select id="findOne" parameterType="string" resultType="Customer">
SELECT
customer_code,
customer_name,
customer_pass,
customer_birth,
customer_mail
FROM
customer
WHERE
customer_code = #{customerCode}
</select>
</mapper>



14. テーブル名やカラム名などPreparedStatementでは設定できない箇所にJavaのデータを設定する等、バインド変数#{...}では対応できない場合に限り、置換変数${...}を利用すること

置換変数${...}はSQL文を作る際に文字列内の変数を置換するものです。

簡単に言うとSQL文の文字列のどこでも利用することができます。

(注意)置換変数${...}を利用すると不正なSQL文を作ることも可能であり、SQLインジェクションが起こらないように変数の妥当性をチェックする必要があります。


AnythingTableRepository.java

public interface AnythingTableRepository {

Long count(String tableName);
Long deleteAll(String tableName);
}


AnythingTableRepository.xml

<mapper namespace="com.example.demo.mybatis3.domain.repostiory.AnythingTableRepository">

<!-- 引数のデータ型はString -->
<!-- 戻り値のデータ型はLong -->
<select id="count" parameterType="string" resultType="long">
SELECT
COUNT(*)
FROM
${tableName}
</select>

<!-- 引数のデータ型はString -->
<!-- 戻り値のデータ型はLong -->
<delete id="deleteAll" parameterType="string" resultType="long">
DELETE FROM
${tableName}
</delete>
</mapper>



15. メソッドの引数が複数(2つ以上)の場合、@Paramアノテーションを引数に付与すること

メソッドの引数が複数(2つ以上)の場合、引数に@Param(org.apache.ibatis.annotations.Param)アノテーションを付与してMyBatisの変数に名前を付与してください。

@Paramを付与しない場合、第一引数はparam1、第二引数はparam2のように「param+1からのインデックス」という変数になります。

可読性や保守性を考慮し、引数が複数の場合は必ず@Paramアノテーションを付与するようにしてください。

(注意)mybatis-springを利用する場合、誤ってSpring frameworkの@Param(org.springframework.data.repository.query.Param)アノテーションを付与してしまう方が多いので注意してください。


16. メソッドの引数が一つの場合、@Paramアノテーションを付与しないこと

メソッドの引数が一つの場合、@Paramアノテーションを付与しないでください。

付与した場合は変数名はネストした形で記述する必要があり、Mapperファイルの記述ミスを招く可能性があります。

そもそも@Paramアノテーションを付与しない方がシンプルなので、必要がない限り付与しないようにすべきです。


(NG)引数が一つの場合に@Paramを付与するのはよろしくない

public interface ItemRepository {

void insert(@Param("item") Item item);
}


(NG)変数はネストで記述しなければならない

<insert id="insert" parameterType="Item">

INSERT INTO item (
item_code,
item_name,
status,
created_at
) VALUES (
<!-- @Param("item") を付与した場合、ネストで記述する必要がある -->
#{item.itemCode},
#{item.itemName},
#{item.status},
#{item.createdAt}
<!-- @Param("item") を付与しない場合
#{itemCode},
#{itemName},
#{status},
#{createdAt}
-->

)
</insert>

実は引数が一つだけの基本データ型(プリミティブ、ラッパークラス、日付型等)の場合、MyBatisの変数名はどんな名前でも参照可能です。ですので、わざわざ@Paramで変数名を指定する必要はありません。

ですが、可読性を考慮し、インターフェースの引数名と同じ変数名を利用してください。


引数が一つのインターフェースの例

public interface ItemRepository {

Item fineOne(String itemCode);
}


引数が一つの場合はバインド変数名は何でもOK

<select id="findOne" parameterType="string" resultType="Item">

SELECT
item_code,
item_name,
created_at
FROM
item
WHERE
item_code = #{itemCode}
<!-- 自動的に付与される変数名でもOK -->
<!-- item_code = #{param1} -->
<!-- 適当な変数名でもOK -->
<!-- item_code = #{hoge} -->
</select>


17. マッピングするクラスには空のコンストラクタが存在すること

マッピングするクラス(resultType属性、resultMapのtype属性で指定するクラス)に空のコンストラクタが存在することを確認してください。

MyBatisは空のコンストラクタを利用してインスタンスを生成します。そのため、空のコンストラクタが存在しない場合、マッピングの処理でエラーとなります。

引数のあるコンストラクタを定義した場合、空のコンストラクタも忘れずに定義してください。


コンストラクタが無い旨のエラーが発生する

org.apache.ibatis.exceptions.PersistenceException: 

### Error querying database. Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.example.demo.mybatis3.domain.model.Item matching [java.lang.String, java.lang.String, java.lang.Integer]

(補足)

正しくは「空のコンストラクタ」もしくは「ResultSetの結果のカラムに対応するコンストラクタ」を利用してインスタンスを生成します。

例えばselect item_code, item_name from itemならitem_code, item_nameの二つの引数、select item_code, item_name, status from itemならitem_code, item_name, statusの三つの引数をそれぞれ取るコンストラクタがあれば正常に処理されます。

処理結果に応じたコンストラクタを用意するのは効率的ではないので、常に明示的に空のコンストラクタを定義する方が統一しやすいかと思います。


18. SQL文の内容を条件に応じて変更したい場合はMyBatisの動的SQLを利用すること

MyBatisでは引数の内容に応じてSQL文を動的に変更することができます。

この機能を利用すると、例えばWHEREの条件が少しづつ違うSQL文を複数用意する必要がなくなり、1つのSQL文で対応できるようになります。

動的SQLとはMapperファイルで利用できるMyBatisが提供する専用のXMLタグで、以下に示すような処理を行います。詳細についてはMyBatisの公式サイトの動的SQLのページを参照ください。


  • 条件や選択肢(if、choose、when、otherwise)

  • 繰り返し(foreach)

  • 余分な出力の抑止(trim、where、set)


ItemRepository.java

public interface ItemRepository {

List<Item> searchItemByStatus(List<Integer> statusList);
List<Item> searchItemByCriteria(ItemCriteria criteria);
}


SELECT文のWHERE句の条件でifを利用

<select id="searchItemByCriteria" parameterType="ItemCriteria" resultType="Item">

SELECT
item_code,
item_name,
status,
created_at
FROM
item
WHERE
item_name like #{itemName}
<if test="afterCreated != null">
<![CDATA[
AND created_at >
#{afterCreated}
]]>
</if>
</select>


SELECT文のIN句でforeachを利用

<select id="searchItemByStatus" parameterType="list" resultType="Item">

SELECT
item_code,
item_name,
status,
created_at
FROM
item
WHERE
<foreach item="statusCode" collection="list"
open="status IN (" separator="," close=")">
#{statusCode}
</foreach>
</select>


19. ><はエスケープ処理をすること

SQLを記述していると忘れがちですがMapperファイルはXMLファイルです。そのため><はエスケープ処理が必要となります。エスケープ処理の方法はXMLファイルを記述する際の方法と同じで、実体参照(&gt;&lt;)か<![CDATA[...]]>を利用します。

<![CDATA[...]]>の中では動的SQLが有効になりません。<![CDATA[...]]>は必要最低限の箇所に利用するようにしてください。

なお、以上以下については「PostgreSQLでは以上、以下(=>、=<)が使えない!?」も参照ください。


誤った記述方法(動的SQLが有効にならない)

<select id="searchItemByCriteria" parameterType="ItemCriteria" resultType="Item">

<![CDATA[
SELECT
item_code,
item_name,
created_at
WHERE
item_name like #{itemName}
<if test="afterCreated != null">

AND created_at > #{afterCreated}
</if>
]]>
</select>


正しい記述方法(動的SQLが有効になる)

<select id="searchItemByCriteria" parameterType="ItemCriteria" resultType="Item">

SELECT
item_code,
item_name,
created_at
WHERE
item_name like #{itemName}
<if test="afterCreated != null">
<![CDATA[
AND created_at >
#{afterCreated}
]]>
</if>
</select>


20. 正しくコメントアウトされているか

コメントアウトには二種類の方法があります。XMLファイルのコメントアウトかSQLのコメントアウトです。

XMLファイルのコメントアウトはMyBatisにコメントの内容が渡されません。

SQLのコメントはMyBatisに渡された後、SQLに含まれた形で実行されます。SQLのコメントアウトはSQLが記述できる場所(<select>、<insert>、<update>、<delete>、<sql>タグの範囲内)でのみ有効です。

なおSQLのコメントアウトは利用するRDBMSに依存するので注意してください。(PostgreSQLのコメントアウトは--


XMLファイルのコメントアウト

<select id="findOne" parameterType="string" resultType="Item">

SELECT
item_code,
item_name,
created_at
WHERE
item_code = #{itemCode} <!-- これはXMLファイルのコメントアウト -->
</select>


SQLのコメントアウト

<select id="findOne" parameterType="string" resultType="Item">

SELECT
item_code,
item_name,
created_at
WHERE
item_code = #{itemCode} -- これはSQLのコメントアウト
</select>


次回予告

今回はMyBatisを利用する際に覚えておくべきチェックルール(13~20)について説明しました。次回は残りのチェックルール(21~25)について説明したいと思います。なお、内容は以下を予定しています。


  • 21. ページネーション

  • 22. SQLの共通化(<sql><include>

  • 23. 採番(キー生成)

  • 24. N+1問題はJOIN

  • 25. 複数データを登録する場合、一括登録の適用を検討すること