基于 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 模板动态生成的完整流程:
- 设计模板 :创建
.ftl
文件,使用 FreeMarker 语法定义动态结构 - 配置引擎:初始化 FreeMarker 配置,设置模板加载路径
- 准备数据:构建包含动态参数的数据模型 Map
- 渲染模板:将数据模型应用到模板生成最终 SQL
- 执行/输出:执行生成的 SQL 或用于调试输出
最佳实践建议:
- 始终对用户输入进行安全过滤
- 为常用模板实现缓存机制
- 创建可复用的宏定义减少重复代码
- 针对不同数据库实现方言支持
- 添加详尽的日志记录和监控
FreeMarker 提供了比传统 SQL 构建工具更强大的动态能力,特别适合需要复杂条件逻辑、动态列选择和跨数据库支持的场景。通过合理的设计和安全措施,可以构建出既灵活又安全的 SQL 生成系统。