🐤

MyBatisでforeachの中でbindを使う時の挙動

2023/07/01に公開

問題

以下のようなテーブルがあるときに

CREATE TABLE hogeschema.hoge_piyo (  
	hoge TEXT,  
	piyo TEXT,  
	PRIMARY KEY (hoge, piyo)  
);

MyBatisを使ってSQLを実行する

<insert id="テストinsert1">  
	<foreach collection="hogeリスト" item="hoge">  
		<bind name="piyo" value="piyoマップ.get(hoge)"/>  
		INSERT INTO "hogeschema"."hoge_piyo" (  
			"hoge",  
			"piyo"  
		) VALUES  
		(  
			#{hoge.value},  
			#{piyo.value}  
		)  
		;  
	</foreach>  
</insert>

JavaからMyBatisに渡したパラメータはこちら

List<Hoge> list = new ArrayList<>();  
list.add(testHoge1);  
list.add(testHoge2);  
list.add(testHoge3);  
Hogeリスト型 hogeリスト = new Hogeリスト型(list); // ファーストクラスコレクション  
  
HashMap<Hoge, Piyo> map = new HashMap<>();  
map.put(testHoge1, new Piyo("testPiyo1"));  
map.put(testHoge2, new Piyo("testPiyo2"));  
map.put(testHoge3, new Piyo("testPiyo3"));  
Piyoマップ型 piyoマップ = new Piyoマップ型(map); // ファーストクラスコレクション

実行後の想定結果はテーブルのレコードが以下になる想定だったが

hoge piyo
testHoge1 testPiyo1
testHoge2 testPiyo2
testHoge3 testPiyo3

実際はhogeとpiyoが対応せず、piyoの最後の要素が全てのhogeに対応してしまった

hoge piyo
testHoge1 testPiyo3
testHoge2 testPiyo3
testHoge3 testPiyo3

結論

foreachの中でbindしたくなった時は、bindを利用せずMyBatisに渡すパラメータをまとめてしまうのが良い

<foreach collection="hogepiyoリスト" item="hogepiyo">  
	INSERT INTO "hogeschema"."hoge_piyo" (  
		"hoge",  
		"piyo"  
	) VALUES  
	(  
		#{hogepiyo.hoge.value},  
		#{hogepiyo.piyo.value}  
	)  
	;  
</foreach>

パラメータをまとめるだけのHogePiyo型を作り

public record HogePiyo(Hoge型 hoge, Piyo型 piyo) {  
}

HogePiyo型のリストにまとめた型をMyBatisに渡す

List<Hoge> list = new ArrayList<>();  
list.add(testHoge1);  
list.add(testHoge2);  
list.add(testHoge3);  
Hogeリスト型 hogeリスト = new Hogeリスト型(list); // ファーストクラスコレクション  
  
HashMap<Hoge, Piyo> map = new HashMap<>();  
map.put(testHoge1, new Piyo("testPiyo1"));  
map.put(testHoge2, new Piyo("testPiyo2"));  
map.put(testHoge3, new Piyo("testPiyo3"));  
Piyoマップ型 piyoマップ = new Piyoマップ型(map); // ファーストクラスコレクション

List<HogePiyo> list = new ArrayList<>();  
	for (Hoge型 hoge : hogeリスト()) {  
	Piyo型 piyo = piyoマップ().get(hoge);  
	list.add(new HogePiyo(hoge, piyo));  
}
HogePiyoリスト型 HogePiyoリスト = new HogePiyoリスト型(list)

やったこと

'#{}'をつかったバインド変数ではなく'${}'を利用してみた

<insert id="テストinsert2">  
	<foreach collection="hogeリスト" item="hoge">  
		<bind name="piyo" value="piyoマップ.get(hoge)"/>  
		INSERT INTO "hogeschema"."hoge_piyo" (  
			"hoge",  
			"piyo"  
		) VALUES  
		(  
			'${hoge.value}',  -- ここを#から$に変えた。hogeは変えなくても良さそう。
			'${piyo.value}'   -- ここを#から$に変えた
		)  
		;  
	</foreach>  
</insert>

これによってテーブルは以下の期待値通りになったが、置換変数はユーザーからの入力の場合SQLインジェクションを許してしまう。

hoge piyo
testHoge1 testPiyo1
testHoge2 testPiyo2
testHoge3 testPiyo3

bindのvalueで呼び出す処理にエスケープ処理を実装すれば良いが、自前で書くのは危険と考えJavaでのエスケープ処理を探した。
PreparedStatementを見つけたが、これはコネクションを使わなければならないため、エスケープをしたいだけだがコネクションを使うことをしたくなかった。
他に良い方法を探し中…

動作確認用のリポジトリ

https://github.com/KakiageSeiro/VerificationOfMybatisBind

Discussion