基于 FreeMarker 实现 SQL 模板动态生成的完整指南
基于 FreeMarker 实现 SQL 模板动态生成的完整指南
下面详细介绍使用 FreeMarker 实现动态 SQL 生成的完整步骤,包含最佳实践和高级技巧:
一、整体实现步骤
二、详细实现流程
1. 添加 Maven 依赖
<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
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. 创建模板处理器工具类
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() {@Overridepublic 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
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);// 生成SQLString sql = engine.generateSql("user_query.ftl", params);System.out.println("Generated SQL:\n" + sql);}
}
5. 输出结果示例
SELECT u.id, u.username,u.email,p.phone
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
WHERE 1=1AND u.age >= 18AND u.age <= 30AND u.username LIKE '%john%'AND u.role IN ('admin', 'editor')
ORDER BY u.created_at DESC
LIMIT 10
OFFSET 0
三、FreeMarker SQL 模板高级技巧
1. 复杂条件处理
<#-- 嵌套条件判断 -->
<#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. 动态列选择
SELECT id<#if includeName>, name</#if><#if includeEmail>, email</#if><#if includePhone>, phone</#if>
FROM users
3. JOIN 动态生成
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. 宏定义复用组件
<#-- 定义分页宏 -->
<#macro pagination><#if pageSize gt 0>LIMIT ${pageSize}OFFSET ${(pageNo - 1) * pageSize}</#if>
</#macro><#-- 使用分页宏 -->
<@pagination />
5. 安全处理函数
<#-- 使用自定义安全函数 -->
AND description LIKE '%${sqlSafe(userInput)}%'
四、最佳实践与安全建议
1. 安全防护策略
风险类型 | 防护措施 | 实现方式 |
---|---|---|
SQL注入 | 输入值转义 | ${value?replace("'", "''")} |
移除危险字符 | 工具类中的 protectSql 方法 | |
敏感数据 | 模板与数据分离 | 禁止在模板中硬编码敏感数据 |
权限控制 | 模板文件访问控制 | 限制模板目录权限 |
2. 性能优化技巧
// 重用 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. 调试与日志
// 启用调试模式
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. 多数据库方言支持
<#-- 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. 批量操作模板
INSERT INTO users (name, email) VALUES
<#list users as user>('${user.name}', '${user.email}')<#sep>,</#sep>
</#list>
3. DDL 语句生成
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 集成
@Configuration
public class FreemarkerConfig {@Beanpublic SqlTemplateEngine sqlTemplateEngine() throws IOException {return new SqlTemplateEngine();}
}@Service
public class UserService {@Autowiredprivate 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 模板动态生成的完整流程:
- 设计模板:创建
.ftl
文件,使用 FreeMarker 语法定义动态结构 - 配置引擎:初始化 FreeMarker 配置,设置模板加载路径
- 准备数据:构建包含动态参数的数据模型 Map
- 渲染模板:将数据模型应用到模板生成最终 SQL
- 执行/输出:执行生成的 SQL 或用于调试输出
最佳实践建议:
- 始终对用户输入进行安全过滤
- 为常用模板实现缓存机制
- 创建可复用的宏定义减少重复代码
- 针对不同数据库实现方言支持
- 添加详尽的日志记录和监控
FreeMarker 提供了比传统 SQL 构建工具更强大的动态能力,特别适合需要复杂条件逻辑、动态列选择和跨数据库支持的场景。通过合理的设计和安全措施,可以构建出既灵活又安全的 SQL 生成系统。