課題
MyBatisの動的SQLを用いて、データベース(Oracle)からフィールド値にNULLが含まれるデータを検索する方法を解決します。
要件
フロントエンドから複数の値が渡され、その中に空値(NULL)が含まれる場合、バックエンドはそのフィールドがNULLのデータも検索結果に含める必要があります。
リスト形式のパラメータによる問題点
フロントエンドから複数選択された値がリストとして渡される場合、その中に空文字列が含まれると、データベースの該当フィールドには空文字が格納されます。このリストをパラメータとして検索条件を構築すると、status IN ('SUCCESS', '') のように生成されますが、これはstatus IS NULLのデータを検索できません。
<!-- 問題のある実装例 -->
<select id="selectByStatusList" resultType="YourEntity">
SELECT * FROM your_table
WHERE 1=1
<!-- statusListがnullでなく、かつ要素数が0より大きい場合 -->
<if test="statusValues != null and statusValues.size() > 0">
and status in
<!-- リストをカンマ区切りのIN句に展開 -->
<foreach item="valueItem" index="idx" collection="statusValues" open="(" separator="," close=")">
#{valueItem}
</foreach>
</if>
</select>
文字列形式の解決策
この問題を解決するために、フロントエンド側で空値を特別な文字列(例: 'NULL')として処理し、バックエンドではカンマ区切りの文字列として受け取ります。その後、split()関数で分割し、contains()関数で'NULL'の存在をチェックし、OR status IS NULLの条件を追加します。この際、(status IN (...) OR status IS NULL)のように全体を括弧で囲むことが重要です。
<!-- 推奨される実装例 -->
<select id="selectByStatusString" resultType="YourEntity">
SELECT * FROM your_table
WHERE 1=1
<!-- statusParamがnullでなく、かつ空白でない場合 -->
<if test="statusParam != null and statusParam.trim() != ''">
and (
status in
<!-- 文字列をカンマで分割し、IN句を生成 -->
<foreach item="value" index="i" collection="statusParam.split(',')" open="(" separator="," close=")">
#{value}
</foreach>
<!-- 'NULL'が含まれている場合、追加のNULLチェックを追加 -->
<if test="statusParam.contains('NULL')">
or status is null
</if>
)
</if>
</select>
重要な注意点:条件式の括弧
上記の例のように、(status IN (...) OR status IS NULL)全体を括弧で囲む必要があります。括弧がないと、他の検索条件(例: beginDt = '20240410')と組み合わせた場合に、意図しないデータが検索されてしまいます。
例えば、beginDt = '20240410' AND status IN ('SUCCESS') OR status IS NULL は、beginDt = '20240410' AND status IN ('SUCCESS') と status IS NULL の両方の条件を満たすデータの和集合を返してしまいます。
なぜリスト形式 + 特別な値('NULL')は避けるべきか
リスト形式のパラメータに特別な値('NULL')を含めて渡す方法もありますが、これは正しく動作しません。
<!-- 非推奨:誤った実装例 -->
<select id="selectByStatusListWithNull" resultType="YourEntity">
SELECT * FROM your_table
WHERE 1=1
<if test="statusValues != null and statusValues.size() > 0">
and (
status in
<foreach item="val" index="idx" collection="statusValues" open="(" separator="," close=")">
#{val}
</foreach>
<if test="statusValues.contains('NULL')">
or status is null
</if>
)
</if>
</select>
この実装では、生成されるSQLが status IN ('SUCCESS', 'NULL') OR status IS NULL となり、statusが文字列の'NULL'であるデータと、statusがNULLであるデータの両方を検索してしまいます。これは要件に合致しません。
補足:MyBatisでの文字列連結
Oracleデータベースを使用する場合、文字列の連結にはCONCAT()関数と||演算子の2つの方法があります。
CONCAT("a", "b"): 2つの文字列しか連結できません。"a" || "b": 連結する文字列の数に制限がありません。
一般的に、制限のない||演算子が推奨されます。