<select id="executeSql" resultType="map">
${_parameter}
</select>
呼叫:sqlSession.selectList("executeSql", "SELECT * FROM sys_user WHERE enabled = 1")
缺點:SQL 硬編碼在 Java 代碼,難以維護,注入風險高。
<select id="executeSql" resultType="map" parameterType="map">
${sql}
</select>
Map<String, Object> params = new HashMap<>();
params.put("sql", "SELECT * FROM sys_user WHERE enabled = #{enabled} AND user_name LIKE CONCAT('%', #{userName}, '%')");
params.put("enabled", 1);
params.put("userName", "admin");
List<Map> list = sqlSession.selectList("executeSql", params);
原理:MyBatis 先解析 ${sql} 為純文本,再對 #{enabled}、#{userName} 進行參數綁定(預編譯),有效防注入。
MyBatis 解析 XML 時,<select> 內僅識別 ${sql} 為一個 TextSqlNode。運行時替換為字串後,其中的 <if> 等標籤不會被二次解析,直接作為 SQL 文本發送到資料庫報錯。
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
-- user_query.ftl
SELECT * FROM sys_user WHERE 1=1
<#if enabled??> AND enabled = #{enabled} </#if>
<#if userName?? && userName != ''> AND user_name LIKE CONCAT('%', #{userName}, '%') </#if>
<#if startDate??> AND create_time >= #{startDate} </#if>
<#if endDate??> AND create_time <= #{endDate} </#if>
ORDER BY create_time DESC
public final class SqlTemplateEngine {
private static final Configuration CFG = new Configuration(Configuration.VERSION_2_3_32);
static {
CFG.setClassForTemplateLoading(SqlTemplateEngine.class, "/templates/sql");
CFG.setDefaultEncoding("UTF-8");
CFG.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
CFG.setLogTemplateExceptions(false);
CFG.setWrapUncheckedExceptions(true);
}
/**
* 渲染 SQL 模板
* @param templateName 模板檔名(如 "user_query.ftl")
* @param params 參數 Map,鍵對應模板變數
* @return 渲染後的 SQL 字串(含 #{param} 佔位符)
*/
public static String render(String templateName, Map<String, Object> params) {
try {
Template template = CFG.getTemplate(templateName);
StringWriter writer = new StringWriter();
template.process(params, writer);
return writer.toString();
} catch (IOException | TemplateException e) {
throw new IllegalStateException("SQL 模板渲染失敗: " + templateName, e);
}
}
}
@Service
public class DynamicQueryService {
@Autowired private SqlSessionTemplate sqlSession;
public List<Map> queryUsers(Map<String, Object> conditions) {
// 1. 渲染 SQL 模板
String sql = SqlTemplateEngine.render("user_query.ftl", conditions);
// 2. 將渲染後 SQL 與參數打包
Map<String, Object> params = new HashMap<>(conditions);
params.put("sql", sql);
// 3. 執行
return sqlSession.selectList("dynamicSql.executeSql", params);
}
}
利用攔截器修改 MappedStatement 的 resultMap 類型:
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
public class ResultTypeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
MetaObject meta = SystemMetaObject.forObject(handler);
MappedStatement ms = (MappedStatement) meta.getValue("delegate.mappedStatement");
// 從參數中獲取目標類型
Object param = handler.getBoundSql().getParameterObject();
if (param instanceof Map) {
String className = (String) ((Map<?>) param).get("resultClass");
if (StringUtils.isNotBlank(className)) {
// 複製並修改 resultMap
ResultMap newResultMap = buildResultMap(ms.getConfiguration(), className, ms.getResultMaps().get(0));
meta.setValue("delegate.mappedStatement.resultMaps", Collections.singletonList(newResultMap));
}
}
return invocation.proceed();
}
// ... buildResultMap 實現略
}
使用:params.put("resultClass", "com.example.entity.User")
| 場景 | 推薦方案 | 注意事項 |
|---|---|---|
| 固定查詢、簡單條件 | MyBatis 原生 <where>、<if> | 類型安全、IDE 支援、易維護 |
| 高度動態、用戶自定義查詢、報表引擎 | FreeMarker + #{param} 參數化 | 嚴禁拼接用戶輸入到 SQL 字串、必須參數化 |
| 跨資料庫方言適配 | 模板引擎 + 方言配置 | 維護多套模板、測試覆蓋 |
${sql} 或模板中#{param} 參數化傳遞