具有过滤索引的实体框架 - “无法在对象中插入重复的键行”

Posted

技术标签:

【中文标题】具有过滤索引的实体框架 - “无法在对象中插入重复的键行”【英文标题】:Entity Framework with filtered index - "Cannot insert duplicate key row in object" 【发布时间】:2016-02-02 10:09:38 【问题描述】:

我在使用 Entity Framework 6 的 MVC Web 应用程序后面的数据库中有以下简化的表定义和过滤的唯一索引。

CREATE TABLE [dbo].[ItemImage](
    [ItemId] [int] NOT NULL,
    [stream_id] [uniqueidentifier] ROWGUIDCOL NOT NULL,
    [Primary] [bit] NOT NULL,
    [Caption] [nvarchar](1000) NULL,
    CONSTRAINT [PK_ItemImage_ItemId_stream_id] PRIMARY KEY CLUSTERED ([ItemId] ASC, [stream_id] ASC)
    CONSTRAINT [FK_ItemImage_ItemId_Item] FOREIGN KEY([ItemId]) REFERENCES [dbo].[Item] ([ItemId])
);
GO

CREATE UNIQUE NONCLUSTERED INDEX [UXF_ItemImage_ItemId_Primary]
    ON [dbo].[ItemImage] ([ItemId] ASC)
    WHERE ([Primary] = 1);
GO

唯一索引可防止多个[ItemId] 设置[Primary] 位标志。

在 MVC 控制器中,我有一组 ItemImage 视图模型,我正在更新 EF 模型,如下所示:

...
foreach (var img in itemViewModel.ItemImages)

    var itemImage = item.ItemImages.First(i => i.stream_id == img.stream_id);

    itemImage.Primary = img.Primary;
    itemImage.Caption = img.Caption;

...
await db.SaveChangesAsync();

db.SaveChangesAsync() 被调用时,我得到以下异常:

无法在具有唯一索引“UXF_ItemImage_ItemId_Primary”的对象“dbo.ItemImage”中插入重复的键行。重复键值为 (146)。 声明已终止。

我在更新之前有防止Item 拥有多个“主要”ItemImages 的逻辑。

我认为这是因为当实体框架试图更新数据库中的 ItemImages 集合时,它会在取消设置当前设置的行之前将另一行的 [Primary] 标志设置为 1

有没有办法在实体框架中强制更新顺序?或者有什么我可以实施的解决方法?

【问题讨论】:

【参考方案1】:

不确定这是否是最有效的方法,但现在可以使用以下方法。

foreach (var img in itemViewModel.ItemImages.Where(i => ! i.Primary))

    var itemImage = item.ItemImages.First(i => i.stream_id == img.stream_id);
    itemImage.Primary = img.Primary;
    itemImage.Caption = img.Caption;


await db.SaveChangesAsync();

var primaryImage = itemViewModel.ItemImages.First(i => i.Primary);
var itemImagePrimary = item.ItemImages.First(i => i.stream_id == primaryImage.stream_id);
itemImagePrimary.Primary = primaryImage.Primary;
itemImagePrimary.Caption = primaryImage.Caption;

await db.SaveChangesAsync();

基本上我只是首先批量更新所有“非主要”ItemImages,提交更改,然后更新新的“主要”。

【讨论】:

这看起来是正确的方法,AFAIK,您无法控制命令的顺序。我会将这一切都包装在一个事务中(使用db.Database.BeginTransaction())只是为了防止在出现其他任何问题时进行半生不熟的更新【参考方案2】:

更有效的方法是通过存储过程将更新推送到数据库层。

CREATE PROCEDURE [dbo].[uspItemImage_SetPrimary]
    @itemId INT
,   @stream_id UNIQUEIDENTIFIER
AS
BEGIN

    SET NOCOUNT ON;

    IF NOT EXISTS ( SELECT NULL FROM [dbo].[ItemImage] WHERE [ItemId] = @itemId AND [stream_id] = @stream_id )
    BEGIN
        DECLARE @stream_id_vc NVARCHAR(36) = CONVERT(NVARCHAR(36), NEWID());
        RAISERROR(  N'No ItemImage exists with ItemId = %d and stream_id = ''%s''',
                11,
                1,
                @itemId,
                @stream_id_vc);
    END;

    BEGIN TRANSACTION;

    UPDATE
        [dbo].[ItemImage]
    SET
        [Primary] = 0
    WHERE
        [ItemId] = @itemId;

    UPDATE
        [dbo].[ItemImage]
    SET
        [Primary] = 1
    WHERE
        [ItemId] = @itemId AND
        [stream_id] = @stream_id;

    COMMIT TRANSACTION;

    RETURN 0;

END;

将存储过程以SetPrimaryItemImage的形式导入实体框架模型后,我可以简单地执行以下操作:

...
foreach (var img in itemViewModel.Images)

    var itemImage = item.ItemImages.First(i => i.stream_id == img.stream_id);
    itemImage.Caption = img.Caption;

await db.SaveChangesAsync();

db.SetPrimaryItemImage(item.ItemId, itemViewModel.Images.First(i => i.Primary).stream_id);   
...

【讨论】:

以上是关于具有过滤索引的实体框架 - “无法在对象中插入重复的键行”的主要内容,如果未能解决你的问题,请参考以下文章

SQLSTATE[23000]:无法在对象中插入重复的键行

实体框架过滤器索引

在实体框架中加载具有过滤条件的子对象记录

无法在实体框架中更新具有唯一约束索引的实体

创建索引时,具有 mysql 数据库迁移的实体框架失败

使用 .net 函数过滤实体框架信息