实体框架存在部分自动生成的复合键问题

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期待并且会忽略。

这有一些明显的问题。

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

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

【讨论】:

以上是关于实体框架存在部分自动生成的复合键问题的主要内容,如果未能解决你的问题,请参考以下文章

使用实体框架将行插入到具有复合键的表中

创建复合键实体框架

实体框架代码优先迁移忽略 [Key] 并强制复合键

实体框架:如何从具有复合键的表中返回一行?

学说:具有复合键的实体之间的 ManyToX 关系

自动检索实体框架外键关系模型