0

我一直在寻找一段时间试图弄清楚这一点。我正在尝试使用复合主键创建一个表。键的第一部分也是父表的外键。第二部分是在 SQL Server 上自动生成的。所以,我有一个应该是这样的表:

ParentId ChildId
-------- -------
 1        1
 1        2
 1        3
 2        1
 2        2
 2        3
 2        4

ChildId 列仅在 ParentId 的上下文中是唯一的。这些值是使用 INSTEAD OF INSERT 触发器在服务器上自动生成的,因此每个 ChildId 都有自己的序列。

我的问题是,虽然这在 SQL Server 和经典 ADO.NETSqlCommand语句中很有效,但 Entity Framework 不想使用它。

如果我将 ChildId 列的 StoreGeneratedPattern 设置为 Identity,则 EF 会生成如下所示的 SQL:

insert [dbo].[ChildTable]([ParentId], [Name])
values (@0, @1)
select [ChildId]
from [dbo].[ChildTable]
where @@ROWCOUNT > 0 and [ParentId] = @0 and [Id] = scope_identity()

这只会产生一个错误:

System.Data.Entity.Infrastructure.DbUpdateConcurrencyException:存储更新、插入或删除语句影响了意外的行数 (0)。自加载实体后,实体可能已被修改或删除。刷新 ObjectStateManager 条目。
----> System.Data.OptimisticConcurrencyException:存储更新、插入或删除语句影响了意外的行数 (0)。自加载实体后,实体可能已被修改或删除。刷新 ObjectStateManager 条目。

但是,如果我使用基于 GUID 的键创建测试表并将 StoreGeneratedPattern 设置为 Identity,则生成的 SQL 如下所示:

declare @generated_keys table([Id] uniqueidentifier)
insert [dbo].[GuidTable]([Name])
output inserted.[Id] into @generated_keys
values (@0)
select t.[Id]
from @generated_keys as g join [dbo].[GuidTable] as t on g.[Id] = t.[Id]
where @@ROWCOUNT > 0

并且我的应用程序中的实体使用 SQL Server 生成的 GUID 的值进行更新。

因此,这表明该列不必是 IDENTITY 列,以便实体框架返回值,但是,由于它使用逻辑表inserted,因此 ChildId 的值不会是它被更改为的值由触发器。此外,该inserted表不能应用 UPDATE 操作以将值推回触发器内(试过了,它说“无法更新逻辑表 INSERTED 和 DELETED。”)

我觉得我已经把自己逼到了一个角落,但在我重新考虑设计之前,有什么办法可以通过实体框架将 ChildId 值返回到应用程序中?

4

1 回答 1

0

我发现这篇文章提供了一个建议:http ://wiki.alphasoftware.com/Scope_Identity+in+SQL+Server+with+nested+and+INSTEAD+OF+triggers

TL;DR 版本是在最后INSTEAD OF INSERT执行 aSELECT以返回密钥。这篇文章是针对SCOPE_IDENTITY()由于触发器而导致的价值损失,但它也适用于此。

所以,我所做的是:

触发器现在显示为

ALTER TRIGGER dbo.IOINS_ChildTable
ON  dbo.ChildTable
  INSTEAD OF INSERT
AS 
BEGIN
SET NOCOUNT ON;
-- Acquire the lock so that no one else can generate a key at the same time.
-- If the transaction fails then the lock will automatically be released.
-- If the acquisition takes longer than 15 seconds an error is raised.
DECLARE @res INT;
EXEC @res = sp_getapplock @Resource = 'IOINS_ChildTable', 
  @LockMode = 'Exclusive', @LockOwner = 'Transaction', @LockTimeout = '15000',
  @DbPrincipal = 'public'
IF (@res < 0)
BEGIN
  RAISERROR('Unable to acquire lock to update ChildTable.', 16, 1);
END

-- Work out what the current maximum Ids are for each parent that is being
-- inserted in this operation.
DECLARE @baseId TABLE(BaseId int, ParentId int);
INSERT INTO @baseId
SELECT MAX(ISNULL(c.Id, 0)) AS BaseId, i.ParentId
  FROM  inserted i
  LEFT OUTER JOIN ChildTable c ON i.ParentId = c.ParentId
  GROUP BY i.ParentId

-- The replacement insert operation
DECLARE @keys TABLE (Id INT);
INSERT INTO ChildTable
OUTPUT inserted.Id INTO @keys
SELECT 
  i.ParentId, 
  ROW_NUMBER() OVER(PARTITION BY i.ParentId ORDER BY i.ParentId) + b.BaseId 
    AS Id,
  Name
FROM inserted i
INNER JOIN @baseId b ON b.ParentId = i.ParentId

-- Release the lock.
EXEC @res = sp_releaseapplock @Resource = 'IOINS_ChildTable', 
  @DbPrincipal = 'public', @LockOwner = 'Transaction'

SELECT Id FROM @keys
END
GO

实体模型的 Id 列StoreGeneratedPattern设置为Identity。这意味着当实体框架尝试读取时,SCOPE_IDENTITY()它将获得SELECT触发器中的语句提供的值,而不是它自己SELECT ... SCOPE_IDENTITY()提供的值,它现在位于 EF 不期望的下一个结果集中并将忽略。

这有一些明显的问题。

因为触发器现在选择要从触发器返回的数据,这意味着插入一些数据并执行自己的选择的其他代码(例如存储过程)将把来自自己选择的数据推出。因此,如果您的代码只需要一个数据库操作的结果集,那么它现在有一个额外的结果集。

如果您只打算使用实体框架,那么这一切都可以。但是,我不能说未来会怎样,所以我对这个解决方案并不完全满意。

于 2012-12-29T21:39:23.453 回答