动态表名与列名
MyBatis 的 #{} 预编译参数无法用于表名、列名、ORDER BY 字段等 SQL 结构,必须使用 ${} 进行字符串替换。本文梳理动态结构拼接的使用方法和安全防范。
#{ } 与 ${ } 的本质区别
| 特性 | #{} 预编译参数 | ${} 字符串替换 |
|---|---|---|
| 处理方式 | PreparedStatement ? 占位符 | 直接字符串拼接 |
| SQL 注入 | 天然免疫 | 存在风险 |
| 适用场景 | 值参数(WHERE/INSERT/UPDATE) | 表名、列名、ORDER BY 字段 |
| 性能 | 预编译可复用执行计划 | 每次生成新 SQL |
| 示例 | WHERE id = #{id} | FROM ${tableName} |
#{}仅适用于值替换,不能用于表名和列名,否则会导致 SQL 语法错误:SELECT * FROM ?是无效 SQL。
动态表名拼接
分表场景
Java
public interface UserMapper {
// 按月份分表:user_202401, user_202402 ...
List<User> selectFromTable(@Param("tableSuffix") String tableSuffix,
@Param("id") Long id);
}
XML
<select id="selectFromTable" resultType="User">
SELECT * FROM user_${tableSuffix} WHERE id = #{id}
</select>
调用示例:
Java
// 查询 2024 年 3 月的用户表
String suffix = "202403";
User user = mapper.selectFromTable(suffix, 1001L);
多环境表路由
XML
<select id="selectByEnv" resultType="Config">
SELECT * FROM config_${env} WHERE key = #{key}
</select>
Java
// 开发环境查 config_dev,生产环境查 config_prod
mapper.selectByEnv("dev", "app.name");
动态列名拼接
动态排序字段
XML
<select id="selectSorted" resultType="User">
SELECT id, name, create_time FROM user
ORDER BY ${sortColumn} ${sortDirection}
LIMIT #{limit} OFFSET #{offset}
</select>
Java
public interface UserMapper {
List<User> selectSorted(@Param("sortColumn") String sortColumn,
@Param("sortDirection") String sortDirection,
@Param("limit") int limit,
@Param("offset") int offset);
}
ORDER BY 和 LIMIT 后的值不能使用
#{},必须使用${},因为它们是 SQL 结构而非值参数。
动态选择查询列
XML
<select id="selectColumns" resultType="map">
SELECT ${columns} FROM user WHERE id = #{id}
</select>
Java
// 只查询指定列,减少数据传输
String cols = "id, name, email";
Map<String, Object> result = mapper.selectColumns(cols, 1001L);
分库分表实战
水平分表路由
SQL
-- order_0 ~ order_15,共 16 张分表
CREATE TABLE order_0 ( ... );
CREATE TABLE order_1 ( ... );
...
CREATE TABLE order_15 ( ... );
Java
public class ShardingRouter {
// 按 user_id 哈希取模路由
public static String routeTable(Long userId, String baseTable, int shardCount) {
int index = (int) (Math.abs(userId.hashCode()) % shardCount);
return baseTable + "_" + index;
}
}
XML
<select id="selectOrder" resultType="Order">
SELECT * FROM ${tableName} WHERE order_id = #{orderId}
</select>
Java
// 调用
String table = ShardingRouter.routeTable(userId, "order", 16);
Order order = mapper.selectOrder(table, orderId);
按时间分表
Java
public class DateShardingRouter {
public static String routeByMonth(String baseTable, LocalDate date) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMM");
return baseTable + "_" + date.format(fmt);
}
public static String routeByYear(String baseTable, int year) {
return baseTable + "_" + year;
}
}
XML
<!-- 查询特定月份的数据 -->
<select id="selectByMonth" resultType="Log">
SELECT * FROM ${tableName}
<where>
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<if test="endTime != null">
AND create_time <= #{endTime}
</if>
</where>
</select>
SQL 注入防范
风险场景
XML
<!-- 危险:用户可控的表名直接拼接 -->
<select id="unsafeSelect" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
攻击者传入 tableName = "user; DROP TABLE user" 会导致恶意 SQL 执行。
防范策略一:白名单校验
Java
public class TableNameValidator {
// 允许的表名白名单(正则匹配)
private static final Pattern TABLE_PATTERN =
Pattern.compile("^(user|order|log)_[0-9]{4,6}$");
public static String validate(String tableName) {
if (tableName == null || !TABLE_PATTERN.matcher(tableName).matches()) {
throw new IllegalArgumentException("Invalid table name: " + tableName);
}
return tableName;
}
}
Java
public Order selectOrder(String tableSuffix, Long orderId) {
String table = "order_" + tableSuffix;
TableNameValidator.validate(table); // 校验通过后拼接
return mapper.selectOrder(table, orderId);
}
防范策略二:枚举约束
Java
public enum SortColumn {
ID("id"), NAME("name"), CREATE_TIME("create_time"), UPDATE_TIME("update_time");
private final String columnName;
SortColumn(String columnName) {
this.columnName = columnName;
}
public String getColumnName() {
return columnName;
}
}
XML
<select id="selectSorted" resultType="User">
SELECT id, name, create_time FROM user
ORDER BY ${sortColumn} ${sortDirection}
LIMIT #{limit} OFFSET #{offset}
</select>
Java
public List<User> selectSorted(SortColumn sortColumn, String direction, int limit, int offset) {
// sortColumn 枚举值安全,无需额外校验
return mapper.selectSorted(sortColumn.getColumnName(),
"ASC".equalsIgnoreCase(direction) ? "ASC" : "DESC", limit, offset);
}
防范策略三:MyBatis 拦截器过滤
Java
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class SqlInjectionInterceptor implements Interceptor {
private static final Pattern DANGER_PATTERN =
Pattern.compile("(;|--|/\\*|\\*/|xp_|exec|drop|delete|update|insert)",
Pattern.CASE_INSENSITIVE);
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
if (DANGER_PATTERN.matcher(sql).find()) {
throw new SecurityException("Potential SQL injection detected: " + sql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}
${ } 使用场景汇总
| 场景 | 使用方式 | 安全等级 |
|---|---|---|
| 表名拼接 | ${tableName} | 高(需白名单) |
| 列名拼接 | ${columnName} | 高(需白名单) |
| ORDER BY | ${sortColumn} ${direction} | 中(枚举约束) |
| LIMIT/OFFSET | 使用 #{} | 安全(值参数) |
| GROUP BY | ${groupByColumn} | 中(枚举约束) |
| 动态 SQL 结构 | ${sqlFragment} | 低(内部使用) |
要点总结
#{}是预编译参数,仅适用于值替换;${}是字符串拼接,用于表名、列名、排序字段等 SQL 结构。- 分表场景必须使用
${}拼接表名,配合路由算法实现数据分片查询。 - SQL 注入防范三板斧:白名单正则校验、枚举约束排序方向、拦截器过滤危险字符。
- 动态列名建议使用枚举类约束,避免外部可控值直接参与拼接。
- LIMIT/OFFSET 参数使用
#{}即可,它们是值而非 SQL 结构。 - 生产环境建议所有
${}使用前必须经过校验,不可直接信任外部输入。
📝 发现内容有误?点击此处直接编辑