使用过滤唯一索引时我们如何管理更新顺序?

问题描述 投票:0回答:1

我有一个过滤的唯一索引定义为:

builder
    .HasIndex(e => new { e.UserId, e.IsDefault })
    .HasFilter($"[{nameof(MyEntity.IsDefault)}] = 1")
    .IsUnique()
    .HasDatabaseName($"UX_{nameof(MyEntity)}_{nameof(MyEntity.UserId)}_{nameof(MyEntity.IsDefault)}");

翻译为:

CREATE UNIQUE NONCLUSTERED INDEX [UX_MyEntity_UserId_IsDefault] 
ON [dbo].[MyEntity] ([UserId] ASC,
                     [IsDefault] ASC)
WHERE ([IsDefault] = (1))

因为我只能有 1 条记录,且标志

IsDefault
设置为
true
,所以更新顺序很重要,我首先需要将非默认记录设置为
false
,然后设置默认记录。由于 EF Core 并不真正了解条件/过滤器,因此它无法正确排序更新,并且我经常收到错误消息

无法在具有唯一索引“UX_MyEntity_UserId_IsDefault”的对象“dbo.MyEntity”中插入重复的键行

示例

在数据库中我有:

身份证 用户ID 是默认
1 5 0
2 5 1
3 5 0

在代码中,我们有一个

Parent
和一个
Child
对象,
Parent
有一个
Child
ren 的集合,它是这样的:

var parent = await parentEfRepository.GetByIdAsync(5, cancellationToken);

var newDefaultChild = parent.Children.Where(c => ...);
newDefaultChild.SetAsDefault();

await context.SaveChangesAsync(cancellationToken);

SetAsDefault()
方法包含将其他
Child
ren设置为非默认值的逻辑,结果是只有1个
Child
被标记为默认值。

当我们到达

SaveChanges()
时,EF Core 会生成一个包含更新的 SQL 脚本,如下所示:

SET NOCOUNT ON;

UPDATE [MyEntity] 
SET [IsDefault] = @p0
OUTPUT INSERTED.[PeriodEnd], INSERTED.[PeriodStart]
WHERE [Id] = @p1;

UPDATE [MyEntity] 
SET [IsDefault] = @p2
OUTPUT INSERTED.[PeriodEnd], INSERTED.[PeriodStart]
WHERE [Id] = @p3;

因为它首先尝试更新 ID 为 1 的记录,所以我们最终会得到以下结果:

身份证 用户ID 是默认
1 5 1
2 5 1
3 5 0

那就是唯一约束开始起作用并引发错误的时候。 我已经开始尝试拦截器,但它越来越脏,肯定有人在我之前遇到过这个问题?

谢谢!

提供商和版本信息:

  • EF核心版本:8.0.11
  • 数据库提供商:
    Microsoft.EntityFrameworkCore.SqlServer
  • 目标框架:.NET 8.0
  • 操作系统:Windows
  • IDE:骑士
c# sql-server entity-framework-core .net-8.0 unique-constraint
1个回答
0
投票

我对可以更改的单个选择有类似的要求。我能想到的最可靠的解决方案是在数据库上使用触发器。 该代码不需要清除该标志,只需设置它即可。就我而言,它用于保存的搜索,可以记录默认选择。我避免使用触发器,但对于这样的场景,这对于可靠性和简单性来说是最有意义的。


CREATE TRIGGER [dbo].[SearchesSingleIsSelected] 
   ON  [dbo].[Searches] 
   AFTER INSERT, UPDATE
AS 
BEGIN

    SET NOCOUNT ON;

    DECLARE @isSelected AS BIT,
        @searchId AS INT,
        @userId AS INT,
        @appId AS INT,
        @typeName AS VARCHAR(500);

    -- Don't allow this trigger to trip recursively as we will be updating rows.
    IF TRIGGER_NESTLEVEL(@@PROCID) > 1
        RETURN;

    IF NOT UPDATE(IsSelected)
        RETURN;

    SELECT @searchId = SearchId, @isSelected = IsSelected, @userId = UserId, @appId = AppId, @typeName = TypeName 
    FROM INSERTED;

    IF @isSelected = 1
    BEGIN
        UPDATE [dbo].Searches SET IsSelected = 0 
        WHERE UserId = @userId AND AppId = @appId AND TypeName = @typeName AND IsSelected = 1 AND SearchId <> @searchId
    END;

END
GO

ALTER TABLE [dbo].[Searches] ENABLE TRIGGER [SearchesSingleIsSelected]
GO

如果您想强制必须选择一行,则需要对此进行修改。就我而言,你没有选择。

然后从实体方面,只需配置它以适应触发器:

// example from EntityTypeConfiguration 
builder.ToTable(tb => tb.UseSqlOutputClause(false));

https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.sqlserverentitytypeextensions.usesqloutputclause?view=efcore-8.0

触发器方法的好处是,无论行的更新内容如何,都会应用它。

© www.soinside.com 2019 - 2024. All rights reserved.