当触发器在表上时,不能将UPDATE与OUTPUT子句一起使用

时间:2012-11-02 15:38:40

标签: sql sql-server sql-server-2008-r2

我正在使用UPDATE查询执行OUTPUT

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid

这句话很好,很好;直到在表上定义了一个触发器。然后,我的UPDATE语句将收到错误334

  

如果语句包含没有INTO子句的OUTPUT子句,则DML语句的目标表'BatchReports'不能具有任何已启用的触发器

现在a blog post by the SQL Server team

解释了这个问题
  

错误消息不言自明

他们也提供解决方案:

  

应用程序已更改为使用INTO条款

除了我无法对整个博客文章做出正面或反面。

所以让我问一下我的问题:我应该将UPDATE更改为什么才能生效?

另见

3 个答案:

答案 0 :(得分:42)

要解决此限制,您需要OUTPUT INTO ...。例如声明一个中间表变量作为目标,然后SELECT。{/ p>

DECLARE @T TABLE (
  BatchFileXml    XML,
  ResponseFileXml XML,
  ProcessedDate   DATE,
  RowVersion      BINARY(8) )

UPDATE BatchReports
SET    IsProcessed = 1
OUTPUT inserted.BatchFileXml,
       inserted.ResponseFileXml,
       deleted.ProcessedDate,
       inserted.Timestamp
INTO @T
WHERE  BatchReports.BatchReportGUID = @someGuid

SELECT *
FROM   @T 

正如在另一个答案中提醒的那样,如果你的触发器回写到UPDATE语句本身修改的行,它会影响你OUTPUT的列,那么你可能不会找到有用的结果,但这只是触发器的一个子集。上述技术在其他情况下工作正常,例如触发器记录到其他表以进行审计,或者返回插入的标识值,即使原始行被写回触发器中也是如此。

答案 1 :(得分:30)

可见性警告:请勿使用highest voted answer。它会给出不正确的值。继续阅读它的错误方式。


考虑到UPDATE OUTPUT在SQL Server 2008 R2中工作所需的kludge,我改变了我的查询:

UPDATE BatchReports  
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid

为:

SELECT BatchFileXml, ResponseFileXml, ProcessedDate FROM BatchReports
WHERE BatchReports.BatchReportGUID = @someGuid

UPDATE BatchReports
SET IsProcessed = 1
WHERE BatchReports.BatchReportGUID = @someGuid

基本上我停止使用OUTPUT。这并不是很糟糕,因为实体框架本身使用这个非常相同的黑客!

希望 2012 2014 2016 2018将有更好的实施。


更新:使用OUTPUT是有害的

我们开始使用的问题是尝试使用OUTPUT子句检索表中的值之后:

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
WHERE BatchReports.BatchReportGUID = @someGuid

然后在SQL Server中遇到众所周知的限制(不会修复错误):

  

如果语句包含没有INTO子句的OUTPUT子句,则DML语句的目标表'BatchReports'不能具有任何已启用的触发器

解决方法尝试#1

因此,我们尝试使用中间TABLE变量来保存OUTPUT结果:

DECLARE @t TABLE (
   LastModifiedDate datetime,
   RowVersion timestamp, 
   BatchReportID int
)

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
INTO @t
WHERE BatchReports.BatchReportGUID = @someGuid

SELECT * FROM @t

除非失败,因为您不允许在表中插入timestamp(甚至是临时表变量)。

解决方法尝试#2

我们秘密地知道timestamp实际上是64位(也称为8字节)无符号整数。我们可以将临时表定义更改为使用binary(8)而不是timestamp

DECLARE @t TABLE (
   LastModifiedDate datetime,
   RowVersion binary(8), 
   BatchReportID int
)

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
INTO @t
WHERE BatchReports.BatchReportGUID = @someGuid

SELECT * FROM @t

这样可行,除了值错误

我们返回的时间戳RowVersion不是UPDATE完成后存在的时间戳值:

  • 返回时间戳0x0000000001B71692
  • 实际时间戳0x0000000001B71693

这是因为我们表格中的OUTPUT值与UPDATE语句末尾的值相同:

  • UPDATE语句开始
    • 修改行
    • 时间戳已更新
    • 检索新时间戳
    • 触发器运行
      • 修改行
      • 时间戳已更新
  • UPDATE语句完成

这意味着:

  • 我们没有得到UPDATE语句末尾存在的时间戳
  • 我们得到UPDATE语句中不确定中间的时间戳
  • 我们没有得到正确的时间戳

修改行中任何值的任何触发器也是如此。 OUTPUT不会在UPDATE结束时输出该值。

  

这意味着您不信任OUTPUT返回任何正确的值

这个痛苦的现实记录在BOL中:

  

从OUTPUT返回的列反映了在INSERT,UPDATE或DELETE语句完成之后但在执行触发器之前的数据。

实体框架是如何解决的?

.NET实体框架使用rowversion进行乐观并发。 EF取决于在发出更新后知道timestamp的值。

由于您无法将OUTPUT用于任何重要数据,因此Microsoft的Entity Framework使用与我相同的解决方法:

解决方法#3 - 最终

为了检索之后的值,实体框架问题:

UPDATE [dbo].[BatchReports]
SET [IsProcessed] = @0
WHERE (([BatchReportGUID] = @1) AND ([RowVersion] = @2))

SELECT [RowVersion], [LastModifiedDate]
FROM [dbo].[BatchReports]
WHERE @@ROWCOUNT > 0 AND [BatchReportGUID] = @1

请勿使用OUTPUT

是的,它受到竞争条件的影响,但这是最好的SQL Server可以做到的。

答案 2 :(得分:1)

为什么将所有需要的列放入表变量?我们只需要主键,就可以在UPDATE之后读取所有数据。使用交易时没有种族:

DECLARE @t TABLE (ID INT PRIMARY KEY);

BEGIN TRAN;

UPDATE BatchReports SET 
    IsProcessed = 1
OUTPUT inserted.ID INTO @t(ID)
WHERE BatchReports.BatchReportGUID = @someGuid;

SELECT b.* 
FROM @t t JOIN BatchReports b ON t.ID = b.ID;

COMMIT;