SQL存储过程数据校验逻辑实现
在SQL存储过程中实现数据校验,核心原则是校验必须前置,在所有INSERT/UPDATE操作之前完成。SQLServer使用IF+THROW语句中止批处理,注意字符串判空需同时检查NULL和空白。MySQL需配合DECLAREEXITHANDLER实现回滚,SIGNAL不会自动终止后续语句。跨库校验逻辑不建议用标量函数,SQLServer推荐内联表值函数,M
先说个经验之谈:在存储过程里写数据校验,最怕的就是“校验写在后面”。很多人图省事,把校验和写入混在一起,结果事务写到一半才发现数据有问题,回滚成本高不说,更麻烦的是可能留下脏数据。其实,解决这个问题很简单——校验必须前置,在所有INSERT/UPDATE操作之前完成。

有一条铁律得刻在脑子里:校验必须在所有INSERT/UPDATE之前完成。否则,一旦事务部分生效,回滚的代价就大了,搞不好还会污染数据。
SQL Server:IF + THROW 是标配
在SQL Server里,处理业务规则校验(像非空、长度、数值范围、外键存在性这些)有一套标准打法。诀窍就是:把所有可能出问题的规则,都摆在INSERT/UPDATE的前面检查。如果不满足,二话不说,直接THROW。SQL Server的THROW语句会立即中止当前批处理,客户端就能捕获到这个异常,知道哪里出错了。
- 推荐写法是
THROW 50000, '订单金额超出单笔限额', 1。错误号要控制在50000到59999之间,状态值固定填1,别乱改。 - 字符串判空这块有个坑:别只用
@param = ''来判断。正确的做法是@param IS NULL OR LTRIM(RTRIM(@param)) = '',这样能同时处理NULL值和空白字符串。 - 数值范围检查直接写比较表达式就行,比如
IF @amount < 0 OR @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,那SIGNAL基本等于白写了——过程继续执行,该写的数据照样写,部分写入的风险很大。
- 必须在存储过程开头声明
DECLARE EXIT HANDLER FOR SQLEXCEPTION,在里面加个ROLLBACK,确保异常发生时能回滚事务。 - 字符串校验的写法是
IF @param IS NULL OR TRIM(@param) = '' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户名不能为空'; END IF;,注意要用TRIM而不是简单的判空。 - 数值类字段用
IF @id IS NULL OR @id < 1来检查,千万别对INT字段搞= '',那会触发隐式转换报错,非常隐蔽。 - 还有一点要注意:
MESSAGE_TEXT最好控制在128个字符以内,避免被截断。统一用'45000'表示通用业务错误,简单明了。
跨库共用校验逻辑:别用标量函数
很多人喜欢把校验逻辑封装成标量函数,觉得这样能复用。但实际效果并不好——标量UDF在SQL Server里批量调用时性能极差,MySQL根本不支持在函数里抛异常。所谓的“复用”,其实只是重复使用可预测的SQL片段而已。
- SQL Server推荐用内联表值函数(ITVF),比如
dbo.tvf_validate_amount(@amount),返回is_valid BIT和error_message NVARCHAR(256)。调用方式是用SELECT * FROM dbo.tvf_validate_amount(@input),而不是SELECT dbo.fn_check_amount(@input)。 - MySQL没有ITVF,校验逻辑只能收束到存储过程体内部,没法真正复用。唯一能靠的是文档和命名规范来约束一致性。
- PostgreSQL可以用
RAISE EXCEPTION配合BEGIN ... EXCEPTION块,但函数定义里不能查表做存在性判断,得靠调用方传入预检结果。
约束和触发器不是替代品,而是防线分层
数据库约束是第一道防线,存储过程校验是最后一道。二者不冲突,但职责要分清。
- 基础规则像
NOT NULL、CHECK (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,错误发生时仍可能留下脏数据。这才是真正的关键所在。
