はじめに
筆者は18年新卒入社のまだまだビギナーなエンジニアです。
なので、記事のレベルはあまり高くないですが、
業務でコードを書いた際にハマった点をこれから来る後輩向けに残したい!!
→どうせ残すならQiitaで書きたい!!と思い投稿してみました。
今回ハマった背景
Spring+MyBatisでバックエンド開発を行っていたときの話です。
担当した機能でリクエストから送られてきたn件のオブジェクトを、
ロジックを通した後にMyBatisを用いて複数件Updateするということをやりました。
その前にMyBatisって?
今回の話をする前に軽くMyBatisの話をします。
MyBatisとは、XMLにSQLを記述する形式のORマッパーフレームワークです。
ORマッパーの中には、わざわざSQLを開発者が書かなくても、
通常のCRUD処理が担保されているものもありますが、
MyBatisはあえて、SQLを手書きすることで複雑な結合や、
UPSERT文などの特殊なSQLをJavaオブジェクトとマッピングができたり、
SQL分の中にif文やforeach等が書け、動的なSQLを生成することができます。
例) INSERT文だと以下のような書き方ができます。
public interface UserRepository{
public int insertUser(List<User> userList);
}
<insert id="insertUser" parameterType="java.util.List">
INSERT INTO users(
user_id
, user_name
, user_mail
)
VALUES
<foreach item="user" collection="list" open="" separator="," close="">
(
#{userId, jdbcType=BIGINT}
, #{userName, jdbcType=VARCHAR}
, #{userEmail, jdbcType=VARCHAR}
)
</foreach>
</insert>
上記だと、登録したい値をUserオブジェクトに詰めておき、
それらをListにしてORマッパーに渡すことで、1度のクエリ発行で複数のINSERTが可能です。
この1度の発行というのが良く、DBとのアクセス回数を絞ることでパフォーマンスを下げないで済みます。
Updateでハマる
上記のノリでUpdateもループで回そう!とやるとハマります。
事象
具体的には以下のようなSQLを書いた場合です。(Javaは割愛)
<update id ="updateUser" parameterType= "java.util.List" >
<foreach collection ="itemList" item="item" separator= ";">
update users
<set>
user_name = #{userName, jdbcType=VARCHAR}
, user_email = #{userEmail, jdbcType=VARCHAR}
</set >
WHERE user_id = #{userId, jdbcType=VARCHAR}
</foreach>
</update >
例えば、上記SQLで「あああ」さんと「いいい」さんの更新をするとします。
これによって発行されるクエリは以下のようになります。
update users
set
user_name = 'あああ',
user_email = 'aaa@aaa.com'
where user_id = '001';
update users
set
user_name = 'いいい',
user_email = 'iii@iii.com'
where user_id = '002';
一見良さげに見えますが、実際に処理を行おうとするとエラーになります。
しかし、この発行されたSQLをテーブルに直接流すと正常に処理が終了します。
原因
これはMybatisというか、Javaのメソッドという面で考えれば納得がいきます(少なくても筆者は)
Javaの挙動として、updateUserというメソッドを呼ぶことになるのですが、
その中でクエリをn件発行しDBへのアクセス行います。
この際、1回のクエリ発行による処理件数を返り値として呼び元のクラスに返すのですが、
上記Update文の場合、一度のメソッド呼びだしで複数のクエリが発行されるので、
複数の返り値を返そうとしてしまいエラーとなります。
なぜInsertはforeachできるのか
これはMyBatisの話をした箇所で書いたとおり、Insert文の場合は
1回のクエリ発行で複数の処理を行うため、
1度のメソッド呼び出しで2件登録したとしても、
返り値が「2」となるだけで、返り値の個数は1つなので正常に処理が終了します。
どうやってUpdateでループするのか
mySQLなどのUPSERT文が使用できるDBならDUPLICATE KEYの記述をし、
UPSERTにしてしまうことで解決できます。
<insert id="upsertUser" parameterType="java.util.List">
INSERT INTO users (
user_id
, user_name
, user_mail
)
VALUES
<foreach item="user" collection="list" open="" separator="," close="">
(
#{userId, jdbcType=BIGINT},
#{userName, jdbcType=VARCHAR},
#{userEmail, jdbcType=VARCHAR},
)
</foreach>
ON DUPLICATE KEY UPDATE
user_name = VALUES(userName),
user_email = VALUES(userEmail)
</insert>
上記SQLだと、更新対象のオブジェクトからでも一旦はINSERT文が発行されます。
これを実行すると、実行時に一意成約違反になるINSERT文の場合、
ON DUPLICATE KEY UPDATEに記述されたUpdate文に切り替わるという処理となります。
よってn件の更新用オブジェクトをこの処理に渡すと、
結果的に一度のクエリ発行で結果的に複数の更新ができることになり処理が正常に行われます。
おわりに
最後まで読んでいただきありがとうございます。
おかしな部分がありましたら、ご指摘お願いします。
また、これからも記事投稿してこうと思うので、執筆についてにアドバイスなども大歓迎です!