SQL Server INSTEAD OF INSERT 触发器失败

Posted

技术标签:

【中文标题】SQL Server INSTEAD OF INSERT 触发器失败【英文标题】:SQL Server INSTEAD OF INSERT trigger failure 【发布时间】:2012-01-12 20:53:31 【问题描述】:

我有两张桌子:

CREATE TABLE users
(
    id int PRIMARY KEY IDENTITY,
    firstname varchar(30),
    lastname varchar(30),
    age int
)

CREATE TABLE rejectedUsers
(
    id int PRIMARY KEY IDENTITY,
    firstname nvarchar,
    lastname nvarchar,
    age nvarchar
)

还有一个触发器:

CREATE TRIGGER checkNumeric
ON users
INSTEAD OF INSERT
AS
INSERT INTO users SELECT firstname, lastname, age FROM inserted WHERE ISNUMERIC(age) = 1;
INSERT INTO rejectedUsers SELECT firstname, lastname, age FROM inserted WHERE ISNUMERIC(age) = 0;

-- goes fine
INSERT INTO users
VALUES
('Vlad', 'P', 21)

-- fails

INSERT INTO users
VALUES
('Chuck', 'Norris', 'abc')

第二个语句给我一个错误:

消息 245,第 16 级,状态 1,第 1 行 将 varchar 值 'abc' 转换为数据类型 int 时转换失败。

我希望触发器在 rejectedUsers 上执行插入操作,怎么了?

【问题讨论】:

也许是个愚蠢的问题,但触发器主体是否可能包含四个插入语句,而不是两个?我注意到你那里没有GO... 【参考方案1】:

这是一个运行时错误,而不是编译器错误。在运行时将类型与基表进行比较,而不管您可能在代替触发器中编写的任何帮助逻辑。插入的伪表是根据基表创建的,而不是根据您传递给任意插入语句的变量类型。

我认为你不能通过简单地在它前面拍一个而不是触发器来提供类型透明度。如果您想允许人们编写可以在其中放入任何数据类型的即席查询,您将需要首先在容纳垃圾的中间表中使用这些查询。或者做大多数人做的事情:让接口强制执行数据类型。如果您使用接受@age 参数作为INT 的存储过程,则它们无法到达带有'abc' 的表附近的任何地方......年龄也没有什么价值,因为它可能会在明天或第二天发生变化。为什么不接受生日呢?

最后,请不要将列或变量定义为没有长度的 varchar/nvarchar。在某些情况下,这些是 1 个字符,在其他情况下是 30 个字符。要明确并告诉每个人您的真正意思。

【讨论】:

+1;用户不应将文本插入以年为单位的名为“年龄”的列中 (int)。 +1 表示 明确 注释 - 在 SQL Server 编程中应该始终明确:-) 未提供长度的空白 varchar 会导致表或参数定义中出现 1 个字符的列。在 CASTCONVERT 语句中,默认长度为 30(为什么在 CAST/CONVERT 中只有 30 以及为什么它必须是 30 个字符 - 我不知道)跨度> @marc_s 我们中的一些人为此游说了 4-5 年:connect.microsoft.com/SQL/feedback/…connect.microsoft.com/SQL/feedback/… 谢谢,@AaronBertrand 当您说“在运行时将类型与基表进行比较...”时,这正是我需要的答案。我也曾尝试在插入之前使用触发器预转换数据,但遇到了同样的错误。【参考方案2】:

你的代码在这里:

INSERT INTO users
VALUES('Chuck', 'Norris', 'abc')

将尝试将abc 插入age 类型为int 的列中- 这真的是一个int 吗?我相信即使您有一个 INSTEAD OF INSERT 触发器,位于触发器内的 Inserted 表将具有与您的 Users 表相同的结构 - 并且该列 age 是一个 INT 列并且可以'不要把abc当作一个值来处理!

作为一般建议,我会始终明确指定您在插入中所指的列 - 所以我会将上述查询编写为:

INSERT INTO users(firstname, lastname, age)
VALUES('Chuck', 'Norris', 71)

我会改变这个:

INSERT INTO users 
    SELECT firstname, lastname, age 
    FROM inserted 
    WHERE ISNUMERIC(age) = 1;

到这里:

INSERT INTO users(firstname, lastname, age)
    SELECT firstname, lastname, age 
    FROM inserted 
    WHERE ISNUMERIC(age) = 1;

这样,如果某个表被修改并突然获得一个您的“通用”INSERT 未填写的新列,您将不会有任何令人讨厌的意外......

附带说明:我希望您通过指定以下内容了解这一点:

CREATE TABLE rejectedUsers
(
    id int PRIMARY KEY IDENTITY,
    firstname nvarchar,
    lastname nvarchar,
    age nvarchar
)

您实际上是在创建一个包含许多 nvarchar(1) 列的表格,每个列最多可以容纳 1 个字符!您还应该始终为您的 varcharnvarchar 列提供明确的长度.....

【讨论】:

+1...inserted 采用基表的数据类型,在这种情况下是用户,据我所见。因此错误。 +1 用于命名列并提及不向 varchar 列提供长度参数的后果 - 我什至不知道可以这样做! @marc_s 顺便说一句,查克·诺里斯 71 岁)+1【参考方案3】:

您不能将“abc”插入age,因为它是int 字段。如果您真的想这样做,请将 users 中年龄的数据类型更改为 VARCHAR(10) 或类似的东西,或者插入 int

【讨论】:

【参考方案4】:

只是一个疯狂的猜测,但也许尝试将被拒绝的语法更改为以下

INSERT INTO rejectedUsers SELECT firstname, lastname, age FROM inserted WHERE ISNUMERIC(age) >= 0 and ISNUMERIC(age) > 120;

这将阻止人们输入大于 120 或小于 0 的年龄(或者您决定应该是最小/最大年龄),但理论上如果它允许 varchar 传递到 int 也会将“abc”也推到拒绝。

如果您仍然遇到转换问题,请尝试以下操作:

INSERT INTO rejectedUsers SELECT firstname, lastname, case when age between 0 and 120 then age else 0 end as age FROM inserted WHERE ISNUMERIC(age) = 0;

如果我正确理解了您的触发器用法(从未真正使用过它们),这将再次检查年龄是否在范围内,否则发送 0 但您会丢失年龄中包含的数据

希望有所帮助,

马库斯

【讨论】:

以上是关于SQL Server INSTEAD OF INSERT 触发器失败的主要内容,如果未能解决你的问题,请参考以下文章

MySQL 触发器中的“INSTEAD OF”,从 SQL Server 转换而来

INSTEAD OF与AFTER触发器

sql - INSTEAD OF INSERT 触发器 - 插入前清除值

Rename a Computer that Hosts a Stand-Alone Instance of SQL Server

Rename a Computer that Hosts a Stand-Alone Instance of SQL Server

T-SQL INSTEAD OF DELETE 触发器在没有“挑衅”的情况下触发