基于 FreeMarker 实现 SQL 模板动态生成的完整指南

基于 FreeMarker 实现 SQL 模板动态生成的完整指南

下面详细介绍使用 FreeMarker 实现动态 SQL 生成的完整步骤,包含最佳实践和高级技巧:

一、整体实现步骤

创建SQL模板 准备数据模型 配置FreeMarker引擎 处理模板生成SQL 执行SQL或输出

二、详细实现流程

1. 添加 Maven 依赖

xml 复制代码
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

2. 创建 SQL 模板文件 (.ftl)

路径: src/main/resources/sql-templates/user_query.ftl

sql 复制代码
SELECT 
    u.id, 
    u.username,
    u.email,
    p.phone
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
WHERE 1=1
<#-- 条件判断 -->
<#if minAge??>
    AND u.age >= ${minAge}
</#if>
<#if maxAge??>
    AND u.age <= ${maxAge}
</#if>
<#if username??>
    AND u.username LIKE '%${username?replace("'", "''")}%'
</#if>
<#-- 循环迭代 -->
<#if roles?has_content>
    AND u.role IN (
    <#list roles as role>
        '${role}'<#sep>, </#sep>
    </#list>
    )
</#if>
<#-- 排序处理 -->
<#if sortField??>
ORDER BY ${sortField} <#if sortOrder == "DESC">DESC<#else>ASC</#if>
<#else>
ORDER BY u.created_at DESC
</#if>
<#-- 分页支持 -->
<#if pageSize gt 0>
LIMIT ${pageSize}
OFFSET ${(pageNo - 1) * pageSize}
</#if>

3. 创建模板处理器工具类

java 复制代码
import freemarker.template.*;
import java.io.*;
import java.util.*;

public class SqlTemplateEngine {
    
    private final Configuration cfg;
    
    public SqlTemplateEngine() throws IOException {
        cfg = new Configuration(Configuration.VERSION_2_3_31);
        // 设置模板目录
        cfg.setDirectoryForTemplateLoading(
            new File("src/main/resources/sql-templates")
        );
        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        cfg.setLogTemplateExceptions(false);
        cfg.setWrapUncheckedExceptions(true);
        cfg.setFallbackOnNullLoopVariable(false);
        
        // 自定义SQL安全方法
        cfg.setSharedVariable("sqlSafe", new TemplateMethodModelEx() {
            @Override
            public Object exec(List args) throws TemplateModelException {
                if (args.size() != 1) 
                    throw new TemplateModelException("sqlSafe requires exactly one argument");
                String input = ((SimpleScalar) args.get(0)).getAsString();
                return input.replace("'", "''");
            }
        });
    }
    
    public String generateSql(String templateName, Map<String, Object> params) {
        try {
            Template template = cfg.getTemplate(templateName);
            StringWriter writer = new StringWriter();
            template.process(params, writer);
            return writer.toString();
        } catch (Exception e) {
            throw new RuntimeException("SQL模板处理失败: " + templateName, e);
        }
    }
    
    // 高级功能:带SQL注入防护的方法
    public String generateSafeSql(String templateName, Map<String, Object> params) {
        String sql = generateSql(templateName, params);
        return protectSql(sql);
    }
    
    private String protectSql(String sql) {
        // 实现SQL注入防护逻辑
        return sql.replace(";", "")   // 移除语句分隔符
                 .replace("--", "")   // 移除单行注释
                 .replace("/*", "");  // 移除多行注释开始
    }
}

4. 准备数据模型并生成 SQL

java 复制代码
public class SqlGeneratorApp {
    public static void main(String[] args) throws Exception {
        SqlTemplateEngine engine = new SqlTemplateEngine();
        
        // 构建查询参数
        Map<String, Object> params = new HashMap<>();
        params.put("minAge", 18);
        params.put("maxAge", 30);
        params.put("username", "john");
        params.put("roles", Arrays.asList("admin", "editor"));
        params.put("sortField", "u.created_at");
        params.put("sortOrder", "DESC");
        params.put("pageSize", 10);
        params.put("pageNo", 1);
        
        // 生成SQL
        String sql = engine.generateSql("user_query.ftl", params);
        
        System.out.println("Generated SQL:\n" + sql);
    }
}

5. 输出结果示例

sql 复制代码
SELECT 
    u.id, 
    u.username,
    u.email,
    p.phone
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
WHERE 1=1
    AND u.age >= 18
    AND u.age <= 30
    AND u.username LIKE '%john%'
    AND u.role IN (
        'admin', 'editor'
    )
ORDER BY u.created_at DESC
LIMIT 10
OFFSET 0

三、FreeMarker SQL 模板高级技巧

1. 复杂条件处理

ftl 复制代码
<#-- 嵌套条件判断 -->
<#if (filterType == "date")>
    <#if dateRange == "today">
        AND created_at >= CURRENT_DATE
    <#elseif dateRange == "week">
        AND created_at >= CURRENT_DATE - INTERVAL '7 days'
    <#else>
        AND created_at BETWEEN '${startDate}' AND '${endDate}'
    </#if>
<#elseif (filterType == "amount")>
    AND amount BETWEEN ${minAmount} AND ${maxAmount}
</#if>

2. 动态列选择

ftl 复制代码
SELECT 
    id
    <#if includeName>, name</#if>
    <#if includeEmail>, email</#if>
    <#if includePhone>, phone</#if>
FROM users

3. JOIN 动态生成

ftl 复制代码
FROM orders o
<#if joinCustomers>
    INNER JOIN customers c ON o.cust_id = c.id
</#if>
<#if joinProducts>
    LEFT JOIN products p ON o.product_id = p.id
</#if>

4. 宏定义复用组件

ftl 复制代码
<#-- 定义分页宏 -->
<#macro pagination>
    <#if pageSize gt 0>
    LIMIT ${pageSize}
    OFFSET ${(pageNo - 1) * pageSize}
    </#if>
</#macro>

<#-- 使用分页宏 -->
<@pagination />

5. 安全处理函数

ftl 复制代码
<#-- 使用自定义安全函数 -->
AND description LIKE '%${sqlSafe(userInput)}%'

四、最佳实践与安全建议

1. 安全防护策略

风险类型 防护措施 实现方式
SQL注入 输入值转义 ${value?replace("'", "''")}
移除危险字符 工具类中的 protectSql 方法
敏感数据 模板与数据分离 禁止在模板中硬编码敏感数据
权限控制 模板文件访问控制 限制模板目录权限

2. 性能优化技巧

java 复制代码
// 重用 Configuration 实例
private static final SqlTemplateEngine ENGINE = new SqlTemplateEngine();

// 模板缓存配置
cfg.setCacheStorage(new StrongCacheStorage());
cfg.setTemplateUpdateDelayMilliseconds(3600000); // 1小时更新一次

// 预编译常用模板
Map<String, Template> templateCache = new ConcurrentHashMap<>();

public Template getTemplate(String name) {
    return templateCache.computeIfAbsent(name, n -> {
        try {
            return cfg.getTemplate(n);
        } catch (IOException e) {
            throw new RuntimeException("模板加载失败: " + n, e);
        }
    });
}

3. 调试与日志

java 复制代码
// 启用调试模式
cfg.setLogTemplateExceptions(true);
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);

// 添加模板处理日志
public String generateSql(String templateName, Map<String, Object> params) {
    long start = System.currentTimeMillis();
    try {
        // ...模板处理逻辑
        return sql;
    } finally {
        long duration = System.currentTimeMillis() - start;
        logger.debug("SQL模板 {} 处理耗时: {}ms", templateName, duration);
    }
}

五、扩展应用场景

1. 多数据库方言支持

ftl 复制代码
<#-- MySQL分页 -->
<#if dbType == "mysql">
    LIMIT ${pageSize} OFFSET ${(pageNo-1)*pageSize}
<#-- Oracle分页 -->
<#elseif dbType == "oracle">
    ) WHERE rn BETWEEN ${(pageNo-1)*pageSize+1} AND ${pageNo*pageSize}
</#if>

2. 批量操作模板

ftl 复制代码
INSERT INTO users (name, email) VALUES
<#list users as user>
    ('${user.name}', '${user.email}')<#sep>,</#sep>
</#list>

3. DDL 语句生成

ftl 复制代码
CREATE TABLE ${tableName} (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
    <#if addTimestamp>
    , created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    </#if>
    <#if addIndex??>
    , INDEX idx_name (name)
    </#if>
);

4. 与 Spring Boot 集成

java 复制代码
@Configuration
public class FreemarkerConfig {
    
    @Bean
    public SqlTemplateEngine sqlTemplateEngine() throws IOException {
        return new SqlTemplateEngine();
    }
}

@Service
public class UserService {
    
    @Autowired
    private SqlTemplateEngine sqlEngine;
    
    public List<User> searchUsers(SearchCriteria criteria) {
        Map<String, Object> params = convertToParams(criteria);
        String sql = sqlEngine.generateSql("user_search.ftl", params);
        return jdbcTemplate.query(sql, new UserRowMapper());
    }
}

六、与 MyBatis 的比较

特性 FreeMarker SQL MyBatis XML
学习曲线 简单 (模板语法) 中等 (XML + 标签)
动态能力 强大 (完整编程能力) 较强 (有限标签)
性能 高 (模板预编译) 中等 (XML解析)
安全性 需手动处理 内置参数绑定
复杂逻辑支持 优秀 (条件/循环/宏) 良好 (需要插件扩展)
集成难度 简单 (独立库) 中等 (需要框架集成)
调试便利性 直接查看生成的SQL 需要特殊工具

总结

使用 FreeMarker 实现 SQL 模板动态生成的完整流程:

  1. 设计模板 :创建 .ftl 文件,使用 FreeMarker 语法定义动态结构
  2. 配置引擎:初始化 FreeMarker 配置,设置模板加载路径
  3. 准备数据:构建包含动态参数的数据模型 Map
  4. 渲染模板:将数据模型应用到模板生成最终 SQL
  5. 执行/输出:执行生成的 SQL 或用于调试输出

最佳实践建议

  • 始终对用户输入进行安全过滤
  • 为常用模板实现缓存机制
  • 创建可复用的宏定义减少重复代码
  • 针对不同数据库实现方言支持
  • 添加详尽的日志记录和监控

FreeMarker 提供了比传统 SQL 构建工具更强大的动态能力,特别适合需要复杂条件逻辑、动态列选择和跨数据库支持的场景。通过合理的设计和安全措施,可以构建出既灵活又安全的 SQL 生成系统。