我有一个过滤的唯一索引定义为:
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 |
那就是唯一约束开始起作用并引发错误的时候。 我已经开始尝试拦截器,但它越来越脏,肯定有人在我之前遇到过这个问题?
谢谢!
提供商和版本信息:
Microsoft.EntityFrameworkCore.SqlServer
我对可以更改的单个选择有类似的要求。我能想到的最可靠的解决方案是在数据库上使用触发器。 该代码不需要清除该标志,只需设置它即可。就我而言,它用于保存的搜索,可以记录默认选择。我避免使用触发器,但对于这样的场景,这对于可靠性和简单性来说是最有意义的。
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));
触发器方法的好处是,无论行的更新内容如何,都会应用它。