Javaの動的検索コードを新人エンジニア向けに解説|AND・OR条件とSQLインジェクション対策の基本
こんにちは。ゆうせいです。
今回は、Javaで「検索条件を自由に組み合わせてcarsテーブルを検索するコード」を、新人エンジニア向けに解説します。
今回のコードは、車情報を管理するcarsテーブルに対して、price、name、car_id、deleted_atなどの条件を指定し、ANDやORで組み合わせて検索できるようにしています。
たとえば、次のような検索ができます。
価格が200万円より高い かつ 名前に「車」という文字が含まれる
SQLで書くと、だいたい次のようなイメージです。
SELECT * FROM cars
WHERE price > ?
AND name LIKE ?このコードのポイントは、検索条件を固定で書くのではなく、ConditionというオブジェクトのリストからSQLを動的に作っているところです。
ただし、SQLを動的に作る場合は、SQLインジェクションに注意しなければいけません。
SQLインジェクションとは、ユーザーが入力した文字列を悪用して、想定外のSQLを実行させる攻撃です。
家の玄関でたとえるなら、本来は「名前を確認して入る」だけの場所なのに、悪意のある人が合鍵のような文字列を差し込んで、勝手に中へ入ろうとするイメージです。
今回のコードでは、フィールド名、比較演算子、結合演算子をチェックし、値はPreparedStatementのプレースホルダにバインドすることで、安全性を高めています。
このコードがやりたいこと
まず、このコード全体の目的を整理しましょう。
| 目的 | 内容 |
|---|---|
| 複数条件で検索する | price、nameなどを組み合わせてcarsテーブルを検索する |
| ANDやORを使う | 条件同士をANDまたはORでつなげる |
| SQLを動的に作る | 条件リストに応じてWHERE句を作る |
| SQLインジェクションを防ぐ | 使えるフィールド名や演算子を制限し、値はPreparedStatementで渡す |
| 検索結果をCarDtoに詰める | ResultSetから値を取り出してCarDtoのリストとして返す |
つまり、このコードは「自由度の高い検索機能」を作るためのコードです。
検索画面で、価格、名前、削除日時などを組み合わせて検索したいときに使う考え方です。
前提になるcarsテーブル
今回のコードでは、carsテーブルに次のようなカラムがある前提です。
| カラム名 | 意味 |
|---|---|
| car_id | 車のID |
| name | 車の名前 |
| price | 価格 |
| deleted_at | 削除日時 |
deleted_atは、論理削除でよく使われるカラムです。
論理削除とは、データを実際にDELETEするのではなく、deleted_atに日時を入れて「削除済み扱い」にする方法です。
ゴミ箱に書類を入れるようなものです。
書類そのものはまだ存在していますが、普段の一覧では見せないようにします。
Conditionとは何か
このコードでは、Conditionというクラスが使われています。
conditions.add(new Condition("price", ">", 2000000, "AND"));
conditions.add(new Condition("name", "LIKE", "%車%", "AND"));Conditionは、検索条件を1つ表すための入れ物だと考えてください。
| 項目 | 例 | 意味 |
|---|---|---|
| field | price | どのカラムを検索するか |
| comparator | > | どの比較演算子を使うか |
| value | 2000000 | 比較する値 |
| operator | AND | 次の条件とどうつなぐか |
1つ目のConditionは、次の条件を表しています。
price > 2000000
2つ目のConditionは、次の条件を表しています。
name LIKE '%車%'
この2つをANDでつなぐと、次のような検索になります。
price > 2000000 AND name LIKE '%車%'
Conditionは、検索条件を部品化したものです。
レゴブロックでたとえるなら、1つ1つのConditionが小さなブロックです。
それらをANDやORでつなげて、検索SQLという大きな形を作っています。
isValidFieldメソッドの役割
最初に、フィールド名をチェックするメソッドを見てみましょう。
private boolean isValidField(String field) {
List<String> validFields = Arrays.asList("car_id", "name", "price", "deleted_at");
return validFields.contains(field);
}このメソッドは、指定されたフィールド名が使ってよいカラム名かどうかを確認しています。
使ってよいフィールドは、次の4つだけです。
| 許可されているフィールド |
|---|
| car_id |
| name |
| price |
| deleted_at |
たとえば、fieldにpriceが渡された場合、validFieldsの中にpriceがあるのでtrueを返します。
一方で、fieldに次のような怪しい文字列が渡された場合はfalseになります。
price; DROP TABLE cars;
このような文字列をそのままSQLに結合すると危険です。
だから、使ってよいフィールド名をあらかじめ決めておき、それ以外は拒否しています。
このような考え方をホワイトリスト方式といいます。
ホワイトリスト方式とは、「許可したものだけ通す」考え方です。
学校の入場でたとえるなら、名簿に名前がある人だけ校内に入れる仕組みです。
名簿にない人は、たとえそれっぽい名前を名乗っても入れません。
isValidComparatorメソッドの役割
次に、比較演算子をチェックするメソッドです。
private boolean isValidComparator(String comparator) {
List<String> validComparators = Arrays.asList("=", "!=", ">", "<", ">=", "<=", "LIKE");
return validComparators.contains(comparator.toUpperCase());
}比較演算子とは、値を比べるための記号です。
| 比較演算子 | 意味 | 例 |
|---|---|---|
| = | 等しい | price = 2000000 |
| != | 等しくない | name != 'プリウス' |
| > | より大きい | price > 2000000 |
| < | より小さい | price < 3000000 |
| >= | 以上 | price >= 2000000 |
| <= | 以下 | price <= 3000000 |
| LIKE | 部分一致検索 | name LIKE '%車%' |
このメソッドでは、比較演算子も許可リストに入っているものだけ使えるようにしています。
comparator.toUpperCase()としているので、likeと入力されてもLIKEとして判定できます。
LIKEは、文字列の一部が一致するかどうかを調べる演算子です。
たとえば、次の条件は、nameに「車」という文字が含まれるデータを探します。
name LIKE '%車%'
%はワイルドカードです。
ワイルドカードとは、「何文字でもよい」という意味の記号です。
トランプのジョーカーのようなものです。
ジョーカーは何のカードにもなれるように、%は任意の文字列に対応します。
isValidOperatorメソッドの役割
次に、結合演算子をチェックするメソッドです。
private boolean isValidOperator(String operator) {
List<String> validOperators = Arrays.asList("AND", "OR");
return validOperators.contains(operator.toUpperCase());
}結合演算子とは、複数の条件をつなぐための言葉です。
| 結合演算子 | 意味 | 例 |
|---|---|---|
| AND | 両方の条件を満たす | price > 2000000 AND name LIKE '%車%' |
| OR | どちらかの条件を満たす | price > 2000000 OR name LIKE '%車%' |
ANDは「かつ」です。
ORは「または」です。
たとえば、テストで考えてみましょう。
「数学が80点以上 AND 英語が80点以上」なら、数学も英語も両方80点以上である必要があります。
「数学が80点以上 OR 英語が80点以上」なら、どちらか一方が80点以上なら条件を満たします。
このコードでは、ANDとOR以外の文字列を拒否しています。
なぜなら、結合演算子もSQLに直接文字列結合されるからです。
フィールド名や演算子は、PreparedStatementの?で置き換えられません。
そのため、必ずホワイトリストでチェックする必要があります。
searchCarListByConditionsメソッドの全体像
次に、メインとなるsearchCarListByConditionsメソッドを見ていきます。
public List<CarDto> searchCarListByConditions(List<Condition> conditions) {このメソッドは、Conditionのリストを受け取り、検索結果としてCarDtoのリストを返します。
| 入力 | 出力 |
|---|---|
| List<Condition> conditions | List<CarDto> carList |
List<Condition>は、検索条件の一覧です。
List<CarDto>は、検索結果の車一覧です。
DTOとは、Data Transfer Objectの略です。
DTOは、データを運ぶための入れ物です。
宅配便の箱でたとえると、DBから取り出したcar_id、name、price、deleted_atをCarDtoという箱に詰めて、画面や呼び出し元へ渡します。
検索結果を入れるリストを作る
メソッドの最初では、検索結果を入れるためのリストを作っています。
List<CarDto> carList = new ArrayList<>();このcarListに、検索で見つかった車情報を1件ずつ追加していきます。
買い物カゴでたとえるなら、検索結果として見つかった商品をカゴに入れていくイメージです。
条件がない場合は全件返す
次の部分では、条件がない場合の処理をしています。
if (conditions == null || conditions.isEmpty()) {
return getCarList(); // 条件なしの場合は全件返す
}conditionsがnullの場合、または空の場合は、条件なしと判断しています。
| 状態 | 意味 | 処理 |
|---|---|---|
| conditions == null | 条件リスト自体がない | 全件取得する |
| conditions.isEmpty() | 条件リストはあるが中身が空 | 全件取得する |
この場合はgetCarList()を呼び出して、carsテーブルの全件を返しています。
検索画面で、何も条件を入れずに検索ボタンを押したら全件表示するような動きです。
ただし、実務では全件取得に注意が必要です。
データが100件なら問題ありません。
でも、100万件あるテーブルで全件取得すると、処理が重くなります。
その場合はLIMITを付けたり、ページングを入れたりする必要があります。
フィールド名・比較演算子・結合演算子をチェックする
次に、すべての条件をチェックしています。
for (Condition cond : conditions) {
if (!isValidField(cond.getField())) {
throw new IllegalArgumentException(
"無効なフィールド名です: " + cond.getField());
}
if (!isValidComparator(cond.getComparator())) {
throw new IllegalArgumentException(
"無効な比較演算子です: " + cond.getComparator());
}
if (!isValidOperator(cond.getOperator())) {
throw new IllegalArgumentException(
"無効な結合演算子です: " + cond.getOperator());
}
}ここでは、各Conditionについて次の3つを確認しています。
| チェック対象 | チェック内容 |
|---|---|
| field | 許可されたカラム名か |
| comparator | 許可された比較演算子か |
| operator | 許可された結合演算子か |
もし不正な値があれば、IllegalArgumentExceptionを投げています。
IllegalArgumentExceptionは、「引数が不正です」という意味の例外です。
たとえば、priceやname以外の怪しいフィールド名が渡されたら、そこで処理を止めます。
このチェックはとても大切です。
なぜなら、このあとSQLを文字列として組み立てるからです。
危険な文字列をSQLに混ぜないように、入口で止めているわけです。
遊園地の入口で、危険物を持ち込んでいないかチェックするようなものですね。
SQLを動的に作る
次に、SQLを作っている部分です。
String sql = "SELECT * FROM cars WHERE ";
for (int i = 0; i < conditions.size(); i++) {
Condition cond = conditions.get(i);
sql += cond.getField() + " " + cond.getComparator() + " ?";
if (i < conditions.size() - 1) {
sql += " " + cond.getOperator() + " ";
}
}まず、SQLの先頭を作っています。
SELECT * FROM cars WHERE その後、conditionsの数だけループして、WHERE句の条件を追加しています。
たとえば、mainメソッドでは次の条件を作っています。
conditions.add(new Condition("price", ">", 2000000, "AND"));
conditions.add(new Condition("name", "LIKE", "%車%", "AND"));この条件から作られるSQLは、次のようになります。
SELECT * FROM cars WHERE price > ? AND name LIKE ?ここで大切なのは、値そのものはSQL文字列に直接結合していない点です。
2000000や%車%は、SQL文字列に直接入れていません。
代わりに?を入れています。
?はプレースホルダと呼ばれます。
プレースホルダとは、あとから安全に値を入れるための場所です。
空欄付きの申込書でたとえると、SQLが申込書で、?が空欄です。
あとからPreparedStatementを使って、空欄に安全に値を入れます。
なぜフィールド名と演算子は文字列結合しているのか
ここで、新人エンジニアが疑問に思いやすい点があります。
「SQLインジェクション対策なら、フィールド名や演算子も?にすればよいのでは?」
実は、PreparedStatementの?で置き換えられるのは、基本的に値です。
カラム名や比較演算子は、?で置き換える対象ではありません。
たとえば、次のような書き方は意図通りには使えません。
SELECT * FROM cars WHERE ? > ?1つ目の?にpriceを入れても、DBはそれをカラム名としてではなく、値として扱います。
そのため、カラム名や演算子を動的に変えたい場合は、文字列としてSQLを組み立てる必要があります。
だからこそ、isValidField、isValidComparator、isValidOperatorで厳しくチェックしているのです。
まとめると、こうです。
| SQLの部品 | ?で渡せるか | 安全対策 |
|---|---|---|
| 値 | 渡せる | PreparedStatementでバインドする |
| カラム名 | 基本的に渡せない | ホワイトリストでチェックする |
| 比較演算子 | 基本的に渡せない | ホワイトリストでチェックする |
| ANDやOR | 基本的に渡せない | ホワイトリストでチェックする |
この考え方は、動的SQLを書くうえでとても重要です。
PreparedStatementを作る
次に、DB接続とPreparedStatementを作っています。
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql.toString())) {Connectionは、JavaアプリとDBをつなぐ接続です。
電話でたとえるなら、JavaからDBへ電話をかけている状態です。
PreparedStatementは、SQLを安全に実行するためのオブジェクトです。
PreparedStatementを使うと、?に値を安全にセットできます。
また、このコードではtry-with-resourcesを使っています。
try-with-resourcesとは、処理が終わったら自動でcloseしてくれるJavaの仕組みです。
DB接続やPreparedStatementを閉じ忘れると、リソース不足の原因になります。
蛇口を開けっぱなしにすると水が無駄になるように、DB接続を閉じ忘れるとDBやアプリに負担がかかります。
try-with-resourcesを使うと、蛇口を自動で閉めてくれるような安心感があります。
なお、sqlはStringなので、sql.toString()は書いても動きますが、なくても問題ありません。
PreparedStatement ps = con.prepareStatement(sql)
このように書いても十分です。
条件の値をプレースホルダにバインドする
次に、?に値をセットしています。
for (int i = 0; i < conditions.size(); i++) {
Object value = conditions.get(i).getValue();
if (value instanceof Integer) {
ps.setInt(i + 1, (Integer) value);
} else {
ps.setString(i + 1, value.toString());
}
}PreparedStatementでは、?の番号は1から始まります。
JavaのListのindexは0から始まります。
そのため、i + 1を使っています。
| i | PreparedStatementの番号 | 値 |
|---|---|---|
| 0 | 1 | 2000000 |
| 1 | 2 | %車% |
mainメソッドの条件なら、次のように値がセットされます。
ps.setInt(1, 2000000);
ps.setString(2, "%車%");完成イメージは次のSQLです。
SELECT * FROM cars WHERE price > 2000000 AND name LIKE '%車%'
ただし、実際には値が安全にバインドされるため、文字列結合より安全です。
ここがSQLインジェクション対策の大事な部分です。
value instanceof Integerの意味
次の部分を見てください。
if (value instanceof Integer) {
ps.setInt(i + 1, (Integer) value);
} else {
ps.setString(i + 1, value.toString());
}instanceofは、「このオブジェクトが指定した型かどうか」を調べる演算子です。
ここでは、valueがInteger型ならsetIntを使っています。
Integer以外なら、文字列としてsetStringを使っています。
| valueの型 | 使うメソッド | 例 |
|---|---|---|
| Integer | setInt | 2000000 |
| それ以外 | setString | %車% |
priceのような数値項目ではIntegerを使います。
nameのような文字列項目ではStringを使います。
ただし、この実装ではIntegerとString以外の型に対する細かい対応はありません。
たとえば、deleted_atを日付として扱うなら、LocalDateTimeやTimestampへの対応を検討したほうがよいです。
また、valueがnullの場合、value.toString()でNullPointerExceptionになる可能性があります。
実務ではnullチェックも考えましょう。
SQLを実行してResultSetを受け取る
次に、SQLを実行しています。
try (ResultSet rs = ps.executeQuery()) {executeQueryは、SELECT文を実行するときに使うメソッドです。
実行結果はResultSetとして返ってきます。
ResultSetは、DBから返ってきた表のようなものです。
| car_id | name | price | deleted_at |
|---|---|---|---|
| 1 | 普通車 | 2500000 | null |
| 2 | 軽自動車 | 2100000 | null |
ResultSetは、この表を1行ずつ読み取るためのものです。
ResultSetからCarDtoを作る
検索結果を1行ずつ読み取り、CarDtoに詰めています。
while (rs.next()) {
CarDto car = new CarDto();
car.setCarId(rs.getInt("car_id"));
car.setName(rs.getString("name"));
car.setPrice(rs.getInt("price"));
car.setDeletedAt(rs.getString("deleted_at"));
carList.add(car);
}rs.next()は、次の行があるかどうかを確認し、あればその行へ進みます。
1行ごとにCarDtoを作り、DBの値をセットしています。
| DBのカラム | CarDtoのフィールド |
|---|---|
| car_id | carId |
| name | name |
| price | price |
| deleted_at | deletedAt |
最後に、carList.add(car)でリストに追加しています。
これにより、検索結果がList<CarDto>として呼び出し元に返せるようになります。
SQLExceptionの処理
DB処理では、SQLExceptionが発生する可能性があります。
} catch (SQLException e) {
e.printStackTrace();
}SQLExceptionは、SQL実行やDB接続で問題が起きたときの例外です。
たとえば、次のような場合に発生します。
| 原因 | 例 |
|---|---|
| SQL文が間違っている | カラム名のミス、構文エラー |
| DBに接続できない | URL、ユーザー名、パスワードの誤り |
| 型が合わない | 文字列を数値カラムと比較している |
| テーブルが存在しない | carsテーブルが作成されていない |
このコードではe.printStackTrace()でエラー内容をコンソールに表示しています。
学習段階ではわかりやすいです。
ただし、実務ではログ出力や例外の再スローを検討します。
エラーを握りつぶすと、呼び出し元が失敗に気づけない場合があるからです。
mainメソッドの動き
最後に、mainメソッドを見ていきます。
public static void main(String[] args) {
CarsDao carsDao = new CarsDao();
List<Condition> conditions = new ArrayList<>();
conditions.add(new Condition("price", ">", 2000000, "AND"));
conditions.add(new Condition("name", "LIKE", "%車%", "AND"));
List<CarDto> result = carsDao.searchCarListByConditions(conditions);
for (CarDto carDto : result) {
System.out.println(carDto);
}
}まず、CarsDaoを作っています。
CarsDao carsDao = new CarsDao();
DAOとは、Data Access Objectの略です。
DBアクセスを担当するクラスです。
アプリとDBの間に立つ窓口のような存在です。
次に、検索条件リストを作ります。
List<Condition> conditions = new ArrayList<>();
そして、条件を2つ追加しています。
conditions.add(new Condition("price", ">", 2000000, "AND"));
conditions.add(new Condition("name", "LIKE", "%車%", "AND"));
この2つの条件は、次の意味です。
| 条件 | 意味 |
|---|---|
| price > 2000000 | 価格が200万円より高い |
| name LIKE '%車%' | 名前に「車」が含まれる |
この条件を使って検索しています。
List<CarDto> result = carsDao.searchCarListByConditions(conditions);
最後に、検索結果を1件ずつ表示しています。
for (CarDto carDto : result) {
System.out.println(carDto);
}
このmainメソッドは、検索メソッドが正しく動くか確認するためのテスト実行のような役割です。
このコードの良いところ
このコードには、学習として良いポイントがたくさんあります。
| 良いところ | 理由 |
|---|---|
| 条件をリストで管理している | 検索条件を柔軟に増やせる |
| フィールド名をホワイトリストで制限している | 危険なカラム名の混入を防げる |
| 比較演算子をチェックしている | SQLに不正な演算子が入るのを防げる |
| ANDとORをチェックしている | 条件結合部分の安全性を高めている |
| 値をPreparedStatementで渡している | SQLインジェクション対策になる |
| DTOに詰めて返している | DBの結果をJavaのオブジェクトとして扱いやすい |
特に大事なのは、フィールド名や演算子をバリデーションしている点です。
動的SQLでは、ここをサボると危険です。
「値はPreparedStatementを使っているから全部安全」と考えるのは危険です。
フィールド名や演算子は文字列結合しているため、別途チェックが必要になります。
このコードで注意したいところ
次に、改善できる点も見ておきましょう。
| 注意点 | 理由 | 改善案 |
|---|---|---|
| SELECT *を使っている | 不要なカラムまで取得する可能性がある | SELECT car_id, name, price, deleted_atと明示する |
| Stringの+=でSQLを作っている | 条件が増えると読みにくくなる | StringBuilderを使う |
| 最後のConditionにもoperatorが必要 | 最後のoperatorは実際には使われない | 最後だけoperator不要にする設計も考える |
| null値に弱い | value.toString()でNullPointerExceptionになる可能性がある | nullチェックやIS NULL対応を追加する |
| 型チェックが簡易的 | IntegerとString以外の型に対応していない | 日付やLongなども考慮する |
| ANDとORの優先順位に注意 | 括弧を使えないため複雑な条件に弱い | 括弧付き条件を扱う設計を検討する |
| 論理削除条件が固定で入っていない | 削除済みデータも検索される可能性がある | 通常はdeleted_at IS NULLを追加する |
特に、実務ではSELECT *を避けることが多いです。
String sql = "SELECT car_id, name, price, deleted_at FROM cars WHERE ";
このように、必要なカラムを明示したほうが安全です。
また、論理削除を使っている場合は、通常の検索では削除済みデータを除外することが多いです。
WHERE deleted_at IS NULL
この条件を固定で入れるか、検索条件として扱うかは、システムの仕様によって決めます。
StringBuilderを使ったほうがよい理由
このコードでは、SQLを+=で連結しています。
sql += cond.getField() + " " + cond.getComparator() + " ?";
学習用としてはわかりやすいです。
ただし、文字列を何度も連結する場合は、StringBuilderを使うことが多いです。
StringBuilderとは、文字列を効率よく組み立てるためのクラスです。
ブロックを1つずつ積み上げていく専用の作業台のようなものです。
StringBuilder sql = new StringBuilder();
sql.append("SELECT car_id, name, price, deleted_at FROM cars WHERE ");
for (int i = 0; i < conditions.size(); i++) {
Condition cond = conditions.get(i);
sql.append(cond.getField())
.append(" ")
.append(cond.getComparator())
.append(" ?");
if (i < conditions.size() - 1) {
sql.append(" ")
.append(cond.getOperator())
.append(" ");
}
}このように書くと、SQLを組み立てている意図が少し明確になります。
ただし、新人エンジニアの学習段階では、まず+=で流れを理解してからStringBuilderに進むとよいです。
LIKE検索で注意すること
mainメソッドでは、LIKE検索の値として%車%を渡しています。
conditions.add(new Condition("name", "LIKE", "%車%", "AND"));
LIKE検索では、%を付ける位置によって検索の意味が変わります。
| 値 | 意味 |
|---|---|
| 車% | 「車」で始まる |
| %車 | 「車」で終わる |
| %車% | 「車」を含む |
今回の%車%は、「名前のどこかに車が含まれる」という意味です。
LIKE検索は便利ですが、大量データでは重くなる場合があります。
特に、先頭に%を付ける検索はインデックスが効きにくいことがあります。
小さいデータなら問題になりませんが、実務では性能にも注意しましょう。
ANDとORの優先順位に注意する
今回のコードでは、条件を順番につなげています。
price > ? AND name LIKE ?
単純な条件なら問題ありません。
ただし、ANDとORが混ざると注意が必要です。
price > ? OR name LIKE ? AND car_id = ?
SQLでは、ANDのほうがORより優先されます。
つまり、上のSQLは次のように解釈されます。
price > ? OR (name LIKE ? AND car_id = ?)
もし次のようにしたいなら、括弧が必要です。
(price > ? OR name LIKE ?) AND car_id = ?
今回のコードでは、括弧付き条件には対応していません。
そのため、複雑な条件を扱いたい場合は、Conditionの設計をさらに工夫する必要があります。
数学でも、掛け算が足し算より先に計算されますよね。
SQLにも似たような優先順位があります。
このコードを図ではなく流れで整理する
処理の流れを文章で整理すると、次のようになります。
| 順番 | 処理 |
|---|---|
| 1 | Conditionのリストを受け取る |
| 2 | 条件がなければgetCarListで全件返す |
| 3 | 各Conditionのfield、comparator、operatorをチェックする |
| 4 | チェック済みの条件からWHERE句を作る |
| 5 | PreparedStatementを作る |
| 6 | ?に値をセットする |
| 7 | SQLを実行する |
| 8 | ResultSetからCarDtoを作る |
| 9 | CarDtoのリストを返す |
この流れを理解できると、動的検索の基本がかなり見えてきます。
新人エンジニアがこのコードから学ぶべきこと
このコードから学べることは、単にJavaの文法だけではありません。
DB検索機能を作るうえで大切な考え方が入っています。
| 学ぶべきこと | 内容 |
|---|---|
| 動的SQL | 条件に応じてSQLを組み立てる考え方 |
| バリデーション | 入力値が安全か確認する考え方 |
| ホワイトリスト | 許可した値だけ通す安全対策 |
| PreparedStatement | 値を安全にSQLへ渡す仕組み |
| DTO | DBの検索結果をJavaオブジェクトに詰める考え方 |
| DAO | DBアクセス処理を担当するクラスの役割 |
| SQLインジェクション対策 | 文字列結合の危険性と安全な実装の基本 |
特に重要なのは、動的SQLとSQLインジェクション対策をセットで学ぶことです。
動的SQLは便利です。
でも、便利な分だけ危険もあります。
包丁と同じです。
料理には欠かせませんが、扱い方を間違えると危ないですよね。
動的SQLも、バリデーションとPreparedStatementをセットで使うことが大切です。
まとめ
今回のコードは、Conditionのリストをもとに、carsテーブルを動的に検索するためのJavaコードです。
フィールド名、比較演算子、結合演算子をチェックし、検索値はPreparedStatementで安全にバインドしています。
| ポイント | 内容 |
|---|---|
| isValidField | 使ってよいカラム名か確認する |
| isValidComparator | 使ってよい比較演算子か確認する |
| isValidOperator | ANDまたはORか確認する |
| searchCarListByConditions | 条件リストからSQLを作って検索する |
| PreparedStatement | ?に値を安全にセットする |
| ResultSet | DBの検索結果を1行ずつ読み取る |
| CarDto | 検索結果をJavaのオブジェクトとして持つ |
一言でまとめるなら、このコードは「安全に条件を組み合わせてcarsテーブルを検索するための動的SQLサンプル」です。
新人エンジニアは、まず次の3つをしっかり覚えてください。
| 覚えること | 理由 |
|---|---|
| 値はPreparedStatementで渡す | SQLインジェクションを防ぐため |
| カラム名や演算子はホワイトリストでチェックする | ?で置き換えられないため |
| 検索結果はDTOに詰めて返す | DBの結果をJava側で扱いやすくするため |
今後の学習では、まずPreparedStatement、ResultSet、DTO、DAOの基本を復習してください。
その後、動的SQL、SQLインジェクション対策、LIKE検索、ANDとORの優先順位、StringBuilder、論理削除のdeleted_at IS NULLを順番に学ぶとよいです。
まずは今回のコードを動かし、priceだけの検索、nameだけのLIKE検索、priceとnameのAND検索、OR検索を試してみましょう。実際にSQLがどう組み立てられるかを確認すると、動的検索の理解が一気に深まります!
セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。
投稿者プロフィール


