动态insert唯一安全路径是字段名和占位符动态拼接、值全部走preparedstatement绑定;字符串拼接值必然导致sql注入、类型错乱、空值处理失当等问题。

直接拼接 INSERT 字符串并执行是高危操作,绝大多数场景下应避免——它绕过参数绑定,必然引入 SQL 注入、类型错乱、空值/NULL 处理失当、字段名大小写/下划线不一致等连锁问题。真正可落地的“动态插入”,只有一种安全路径:字段名和占位符动态拼,值全部走 PreparedStatement 或框架级参数绑定。
为什么不能用字符串拼接值
拼接值(比如 "'"+user.getName()+"'")等于主动放弃数据库驱动的类型校验和转义能力。一旦 user.getName() 是 O'Reilly 或 ; DROP TABLE users--,语句就直接崩或被注入。SQL Server 会报 Unclosed quotation mark,MySQL 报 You have an error in your SQL syntax,而业务层看到的只是“插入失败”,根本无法定位是数据问题还是逻辑漏洞。更隐蔽的是时区、小数点、空字符串与 NULL 的语义混淆——字符串拼接无法区分 "" 和 NULL,但数据库对二者处理完全不同。
SQL Server 中用系统视图安全生成 INSERT 脚本
适用于导出表数据(如环境同步),必须过滤掉不可插入列:
- 查
sys.columns,加条件is_identity = 0 AND is_computed = 0 AND is_rowguidcol = 0 - 字段名统一用
QUOTENAME(column_name)包裹,防关键字冲突(如字段叫order) - 值部分不用拼字符串,改用
CONVERT(varchar, value, 120)控制日期格式,数字用STR()避免科学计数法 - 批量导出时,每 1000 行包一个
BEGIN TRY ... END TRY BEGIN CATCH ... END CATCH,防单行失败中断全量
MyBatis 动态 INSERT 必须用 <trim></trim> + <if></if>
硬编码字段或漏判空值,会导致插入空串或跳过非空字段。正确写法核心就两点:
-
suffixOverrides=","必须写在每个<trim></trim>里,否则末尾多逗号直接语法错误 -
test表达式必须同时判!= null和!= '',例如test="userName != null and userName != ''";只判null会导致空字符串插入到非空约束字段上 - 自增主键字段(如
id)如果实体里有值,需额外加test="id == null"过滤,否则 SQL Server 报Cannot insert explicit value for identity column
Java 反射动态构建 INSERT 的关键控制点
反射不是万能的,字段映射必须可控:
- 跳过
null和空字符串用Objects.nonNull(val) && !(val instanceof String && ((String) val).isEmpty()),别只用val != null - 字段名转换必须走统一规则,如
BeanUtils.getPropertyDescriptor获取属性后,用CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, "userName")得到user_name -
StringBuilder拼字段列表时,最后用deleteCharAt(sb.length() - 1)删逗号,比正则或 if 判断更稳——避免空字段导致长度为 0 时抛StringIndexOutOfBoundsException - 所有值一律进
ps.setObject(idx++, val),绝不调ps.setString()等类型强绑方法,让 JDBC 驱动自己推断
动态 INSERT 的复杂点从来不在“怎么拼”,而在于“哪些字段该出现、以什么形式出现、值如何保真”。字段过滤逻辑、空值语义、数据库侧约束(identity/computed/default)三者必须对齐,缺一不可。漏掉任意一个,表面能跑,线上必出数据异常或安全问题。










