使用 INSTEAD OF 触发器合并到视图中

tor*_*vin 11 trigger sql-server entity-framework merge sql-server-2019

我有一个带有触发器的视图instead of,我尝试将其与 EF Core 一起使用,EF Core 尝试以merge语句的形式将批量插入一起。这是我的表格和视图:

create table tbl (id uniqueidentifier not null primary key, data nvarchar(max) null)
go
create view vwTbl as select * from tbl
go
create trigger vwTblInsert on vwTbl instead of insert as
    insert into tbl (id, data)
    select id, data
    from inserted
go
Run Code Online (Sandbox Code Playgroud)

以下是 EF 生成的近似 SQL:

declare @inserted1 table ([id] uniqueidentifier);
merge vwTbl using (
    values (newid(), 'xxx') 
) as i (id, data) on 1=0
when not matched then
    insert (id, data)
    values (i.id, i.data)
    output inserted.Id
    into @inserted1;
select * from @inserted1
Run Code Online (Sandbox Code Playgroud)

这是我收到的错误:

不允许使用列引用“inserted.id”,因为它引用此语句中未修改的基表。

merge声明的帮助页面显示:

如果目标表为 MERGE 语句执行的插入、更新或删除操作定义了启用的 INSTEAD OF 触发器,则它必须为 MERGE 语句中指定的所有操作启用 INSTEAD OF 触发器。

我相信我满足这个条件,并且我看不到任何其他相关限制。为什么不起作用?

生成查询的是 EF,而不是我。我无法控制它。我只是想了解为什么它不起作用以及我是否可以以某种方式修复我的触发器以使其起作用。

Pau*_*ite 12

选项1

这里可能不允许插入的引用,因为 SQL Server 无权访问视图上的stead of 触发器实际插入的值。

SQL Server替代触发器是通过将由于触发语句而发生的更改的信息写入工作表(一个用于插入,一个用于删除)来实现的。

替代触发器不使用行版本控制,就像触发器一样。在触发器触发之前,这些工作表已由触发语句完全填充。

OUTPUT子句是触发语句的一部分。该语句的查询计划只能访问当时存在的值。随后将触发替代触发器。从逻辑上讲,输出子句不能返回尚未计算的值。您可以在示例的执行计划中看到所有这些:

计划

在您的实现中,替代触发器基本上除了常规插入之外不执行任何操作,但并不保证情况一定如此。替代触发器可以代替触发操作执行任何它喜欢的操作,包括不执行任何操作。

您可以通过引用源列值来使语句“工作”,但如果替代触发器恰好执行相同的操作,则这仅代表实际插入的值:

declare @inserted1 table ([id] uniqueidentifier);
merge vwTbl using (
    values (newid(), 'xxx') 
) as i (id, data) on 1=0
when not matched then
    insert (id, data)
    values (i.id, i.data)
    output i.id -- CHANGED
    into @inserted1;
select * from @inserted1
Run Code Online (Sandbox Code Playgroud)

db<>小提琴演示

除了触发器之外,您无法更改视图,因此此模式现在就可以工作。

选项2

另一方面,也完全可以想象这是无意的行为。类似的案例已经有很多了MERGE

写成的等效语句确实INSERT有效:

declare @inserted1 table ([id] uniqueidentifier);

INSERT dbo.vwTbl (id, data)
OUTPUT Inserted.id
INTO @inserted1 (id)
VALUES (NEWID(), 'xxx');
Run Code Online (Sandbox Code Playgroud)

这遵循本例中插入的伪表的记录 OUTPUT语义(添加了强调):

INSERTED
是列前缀,指定插入或更新操作添加的值。以 INSERTED 为前缀的列反映 UPDATE、INSERT 或 MERGE 语句完成之后但执行触发器之前的值。

如果MERGE与此处的工作方式相同INSERT,您的语句将按预期工作,并且该OUTPUT子句将返回预触发值。这可能是故意的设计更改。MERGE推出时制作的产品不止一件。

另一方面,如果替代触发器位于基表而不是视图 ( demoMERGE ) 上,则该语句将按预期工作,因此如果没有其他情况,当前行为是不一致的。

通过注意到可以在示例中引用已删除的伪表(尽管它自然返回空结果),可以增加这是错误的可能性。请参阅此演示。这里的推论是解析器对结果操作(插入,而不是删除)感到困惑。

如果您需要明确的答案,最好的选择可能是向 Microsoft 提出支持案例。


Jos*_*ell 8

这不是问题的答案,但如果您无法通过触发器解决问题,它可能会帮助您解决问题。

正如您所提到的,EF Core 使用此MERGE技术来批量更改并避免多次往返数据库。如果您愿意接受多次往返的开销,可以通过SaveChanges()SaveChangesAsync()每个新“行”添加到DbContext. 执行此操作将导致 EF 将该单行作为INSERT语句刷新到数据库。

举个例子,这种情况会导致MERGE(向 DbContext 添加一堆东西,然后调用 SaveChanges):

using (var db = new BloggingContext())
{
    foreach (var blogUrl in newBlogUrls)
    {
        db.Add(new Blog { Url = blogUrl });
    }
    db.SaveChanges();
}
Run Code Online (Sandbox Code Playgroud)

这就是您如何“强制”将其改为INSERT语句(每次添加后保存更改):

using (var db = new BloggingContext())
{
    foreach (var blogUrl in newBlogUrls)
    {
        db.Add(new Blog { Url = blogUrl });
        db.SaveChanges();
    }
}
Run Code Online (Sandbox Code Playgroud)