实体框架存在部分自动生成的复合键问题
Posted
技术标签:
【中文标题】实体框架存在部分自动生成的复合键问题【英文标题】:Entity Framework has issues with composite key which is part auto generated 【发布时间】:2012-12-29 17:55:32 【问题描述】:我一直在寻找一段时间试图弄清楚这一点。我正在尝试使用复合主键创建一个表。键的第一部分也是父表的外键。第二部分是在 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.NET SqlCommand
语句中很有效,但 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 值返回到应用程序中?
【问题讨论】:
【参考方案1】:我发现这篇文章提供了一个建议:http://wiki.alphasoftware.com/Scope_Identity+in+SQL+Server+with+nested+and+INSTEAD+OF+triggers
TL;DR 版本是 INSTEAD OF INSERT
在最后执行 SELECT
以返回密钥。这篇文章是针对由于触发器导致的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 所在的下一个结果集中。 t期待并且会忽略。
这有一些明显的问题。
因为触发器现在选择要从触发器返回的数据,这意味着插入一些数据并执行自己的选择的其他代码(例如存储过程)将把来自自己选择的数据推出。因此,如果您的代码只需要一个数据库操作的结果集,它现在有一个额外的结果集。
如果您只打算使用实体框架,那么这一切都可以。但是,我不能说未来会怎样,所以我对这个解决方案并不完全满意。
【讨论】:
以上是关于实体框架存在部分自动生成的复合键问题的主要内容,如果未能解决你的问题,请参考以下文章