多條件搜尋介面常需動態生成 WHERE 子句。濫用 WHERE 1=1 導致全表掃描、索引失效。本文對比錯誤與正確模式,給出 WhereClauseBuilder 設計模式參考實作。
String sql = "SELECT * FROM T_Employee WHERE 1=1";
if (empNoChecked) sql += " AND FNumber BETWEEN '"+v1+"' AND '"+v2+"'";
if (nameChecked) sql += " AND FName LIKE '%"+name+"%'";
if (ageChecked) sql += " AND FAge BETWEEN "+a1+" AND "+a2;
executeSQL(sql);
危害:
1. 優化器識別 1=1 為常數真,無法利用索引範圍掃描 → 全表掃描。
2. 字串拼接極易引發 SQL 注入。
3. 複雜條件下 SQL 可讀性差、難除錯。
public class WhereClauseBuilder {
private final StringBuilder sql = new StringBuilder();
private boolean hasWhere = false;
public WhereClauseBuilder(String baseSql) {
sql.append(baseSql); // e.g. "SELECT * FROM T_Employee"
}
public WhereClauseBuilder addCondition(String condition) {
if (condition == null || condition.trim().isEmpty()) return this;
if (!hasWhere) {
sql.append(" WHERE ").append(condition);
hasWhere = true;
} else {
sql.append(" AND ").append(condition);
}
return this;
}
public String build() { return sql.toString(); }
}
// 使用範例
WhereClauseBuilder builder = new WhereClauseBuilder("SELECT * FROM T_Employee");
if (empNoChecked) builder.addCondition("FNumber BETWEEN ? AND ?");
if (nameChecked) builder.addCondition("FName LIKE ?");
if (ageChecked) builder.addCondition("FAge BETWEEN ? AND ?");
String finalSql = builder.build();
// 使用 PreparedStatement 綁定參數,防注入、利用執行計劃緩存
PreparedStatement ps = conn.prepareStatement(finalSql);
int idx = 1;
if (empNoChecked) { ps.setString(idx++, v1); ps.setString(idx++, v2); }
if (nameChecked) { ps.setString(idx++, "%"+name+"%"); }
if (ageChecked) { ps.setInt(idx++, a1); ps.setInt(idx++, a2); }
ResultSet rs = ps.executeQuery();
| 維度 | WHERE 1=1 | WhereClauseBuilder |
|---|---|---|
| 索引利用 | ❌ 失效 | ✅ 正常範圍/Ref 掃描 |
| SQL 注入 | 高風險 | ✅ PreparedStatement 參數化 |
| 執行計劃緩存 | 不穩定 | ✅ 固定 SQL 模板可復用 |
| 可維護性 | 差 | ✅ 條件邏輯集中、易擴展 |
<where> 標籤自動處理 AND/OR 前綴,<if test="..."> 動態拼接。WHERE 1=1 / WHERE 1=1 AND ...。PreparedStatement / ORM 參數綁定。