首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

MyBatis 動態 SQL 進階:任意 SQL 執行與 FreeMarker 模板引擎整合

     編輯:Java 相關知識

基礎:MyBatis 執行動態 SQL 的兩種方式

方式一:Mapper XML 直接注入


<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} 進行參數綁定(預編譯),有效防注入。

痛點:無法在 ${sql} 中嵌套 MyBatis 標籤(<if>、<where> 等)

MyBatis 解析 XML 時,<select> 內僅識別 ${sql} 為一個 TextSqlNode。運行時替換為字串後,其中的 <if> 等標籤不會被二次解析,直接作為 SQL 文本發送到資料庫報錯。

解決方案:引入 FreeMarker 模板引擎預處理 SQL

依賴引入


<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version>
</dependency>

SQL 模板定義(資源檔或資料庫存儲)


-- 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 層整合調用


@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 字串、必須參數化
跨資料庫方言適配模板引擎 + 方言配置維護多套模板、測試覆蓋

安全紅線

  1. 絕不將未驗證的用戶輸入直接拼接到 ${sql} 或模板中
  2. 所有動態值必須通過 #{param} 參數化傳遞
  3. 模板邏輯僅控制SQL 結構(是否加 WHERE、JOIN、ORDER BY),不處理數值
  4. 生產環境啟用 SQL 審計日誌、WAF 防火牆
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved