SQL存储过程中数据校验逻辑如何实现?

作者:袖梨 2026-07-21
校验必须前置,SQL Server用IF+THROW(错误号50000–59999、状态值1),MySQL需DECLARE EXIT HANDLER配SIGNAL,字符串判空用IS NULL或TRIM,数值范围直写比较,外键检查用NOT EXISTS加NOLOCK。

校验必须放在 INSERT/UPDATE 之前,否则事务已部分生效,回滚成本高、数据可能污染。

SQL Server 中用 IF + THROW 做前置校验

所有业务规则检查(非空、长度、数值范围、外键存在性)必须在任何写操作前完成。IF 判断不满足就立刻 THROW,SQL Server 会中止批处理并触发客户端异常捕获。

  • THROW 50000, '订单金额超出单笔限额', 1 是推荐写法,错误号限定在 50000–59999,状态值固定填 1
  • 字符串判空别只用 @param = '',要写成 @param IS NULL OR LTRIM(RTRIM(@param)) = ''
  • 数值范围直接写 IF @amount 1000000,避免嵌套 CASE 或隐式转换
  • 外键存在性用 IF NOT EXISTS (SELECT 1 FROM users WITH (NOLOCK) WHERE id = @user_id),加 WITH (NOLOCK) 防阻塞

MySQL 存储过程中 SIGNAL 必须配 DECLARE HANDLER

MySQL 的 SIGNAL 不会自动终止后续语句,没配 handler 就等于白写——过程继续执行,可能造成部分写入。

  • 必须在开头声明 DECLARE EXIT HANDLER FOR SQLEXCEPTION,并在其中 ROLLBACK
  • 校验字符串参数时,用 IF @param IS NULL OR TRIM(@param) = '' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户名不能为空'; END IF;
  • 数值类用 IF @id IS NULL OR @id ,别对 INT 字段做 <code>= '',会触发隐式转换报错
  • MESSAGE_TEXT 控制在 128 字符内,避免被截断;统一用 '45000' 表示通用业务错误

跨库共用校验逻辑别封装成标量函数

标量 UDF 在 SQL Server 里批量调用性能极差,MySQL 根本不支持函数内抛异常——所谓“复用”,本质是复用可预测的 SQL 片段。

  • SQL Server 推荐用内联表值函数(ITVF),如 dbo.tvf_validate_amount(@amount),返回 is_valid BITerror_message NVARCHAR(256)
  • 调用方式是 SELECT * FROM dbo.tvf_validate_amount(@input),不是 SELECT dbo.fn_check_amount(@input)
  • MySQL 没有 ITVF,校验逻辑必须收束到存储过程体内部,无法真正复用,只能靠文档+命名规范约束一致性
  • PostgreSQL 可用 RAISE EXCEPTION + BEGIN ... EXCEPTION 块,但函数定义里不能查表做存在性判断,得靠调用方传入预检结果

CHECK 约束和触发器不是替代品,而是防线分层

数据库约束是第一道防线,存储过程校验是最后一道——二者不互斥,但职责分明。

  • NOT NULLCHECK (age BETWEEN 0 AND 150) 这类基础规则优先走 DDL 约束,SQL Server/PG/MySQL 8.0.16+ 都支持
  • 触发器适合“跨表关联校验”或“变更前后比对”,比如订单插入时扣库存,但触发器本身不能替代存储过程里的业务断言
  • 别在触发器里写复杂逻辑(如调用存储过程、发消息),它运行在行级上下文,高并发下易成瓶颈
  • 动态 SQL 场景最容易漏校验:拼接的列名、表名必须走白名单,例如 CASE @col_name WHEN 'user_id' THEN 'user_id' ELSE SIGNAL ... END

最容易被忽略的是校验与事务边界的耦合——哪怕写了完整 IF 块,如果没把整个校验+写入包在同一个显式事务里,或者 MySQL 没配 EXIT HANDLER,错误发生时仍可能留下脏数据。

相关文章

精彩推荐