c# - Entity Framework Core:自引用 FK 列值被替换为 `NULL`
问题描述
目标框架:.Net Core 2.1
实体框架核心:2.1.4
我在打电话时遇到了一些我不明白的行为SaveChanges()
。正如标题所说:插入新记录时,我有有效的 GUID 与同一个表的外键关系被 NULL 替换。
不过,这只发生在特定列上,并且仅在 EF 为上下文中所做的更改生成 SQL 时发生。我可以在 SSMS 中插入相同的值。
出于演示的目的,我在 GitHub 上创建了一个最小存储库,您可以使用它来重新创建我遇到的确切问题,但简而言之,如果您有这样的实体:
public class Account
{
[Key]
public Guid AccountId { get; set; }
#region Audit
public Guid? AddedByAccountId { get; set; }
public DateTime AddedOnUtc { get; set; }
public Guid? ModifiedByAccountId { get; set; }
public DateTime ModifiedOnUtc { get; set; }
#endregion Audit
#region Navigation Properties
[ForeignKey(nameof(AddedByAccountId))]
public virtual Account AddedByAccount { get; set; }
[ForeignKey(nameof(ModifiedByAccountId))]
public virtual Account ModifiedByAccount { get; set; }
#endregion Navigation Properties
}
以及像这样的派生 DbContext 实现:
public class EntityFrameworkDbContext : DbContext
{
public EntityFrameworkDbContext(DbContextOptions<EntityFrameworkDbContext> options)
: base(options)
{
}
public DbSet<Account> Accounts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Account>(entity =>
{
// For some reason this self referencing key generates a unique
// constraint in the migration script if we don't set this here.
// "ModifiedByAccountId" does not have this issue! I suspect this might
// the root cause of the NULL data issue.
entity.HasIndex(e => e.AddedByAccountId).IsUnique(false);
});
}
}
将任何内容插入上下文应该会产生相同的结果。例如:
private void InitialiseDatabase()
{
var systemAccount = new Account
{
AccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
AddedByAccountId = null,
AddedOnUtc = DateTime.UtcNow,
ModifiedByAccountId = null,
ModifiedOnUtc = DateTime.UtcNow
};
var otherAccounts = new List<Account>
{
new Account
{
AccountId = Guid.Parse("015b76fc-2833-45d9-85a7-ab1c389c1c11"),
AddedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
AddedOnUtc = DateTime.UtcNow,
ModifiedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
ModifiedOnUtc = DateTime.UtcNow
},
new Account
{
AccountId = Guid.Parse("538ee0dd-531a-41c6-8414-0769ec5990d8"),
AddedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
AddedOnUtc = DateTime.UtcNow,
ModifiedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
ModifiedOnUtc = DateTime.UtcNow
},
new Account
{
AccountId = Guid.Parse("8288d9ac-fbce-417e-89ef-82266b284b78"),
AddedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
AddedOnUtc = DateTime.UtcNow,
ModifiedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
ModifiedOnUtc = DateTime.UtcNow
},
new Account
{
AccountId = Guid.Parse("4bcfe9f8-e4a5-49f0-b6ee-44871632a903"),
AddedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
AddedOnUtc = DateTime.UtcNow,
ModifiedByAccountId = Guid.Parse("35c38df0-a959-4232-aadd-40db2260f557"),
ModifiedOnUtc = DateTime.UtcNow
}
};
_dbContext.Add(systemAccount);
_dbContext.AddRange(otherAccounts);
try
{
_dbContext.SaveChanges();
}
catch (Exception ex)
{
// Just checking to see if anything was being raised.
}
}
将生成以下 SQL(使用 SQL Server Profiler 捕获并格式化以提高可读性):
exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Accounts]
(
[AccountId],
[AddedByAccountId],
[AddedOnUtc],
[ModifiedByAccountId],
[ModifiedOnUtc]
)
VALUES
(
@p5,
@p6,
@p7,
@p8,
@p9
),
(
@p10,
@p11,
@p12,
@p13,
@p14
),
(
@p15,
@p16,
@p17,
@p18,
@p19
),
(
@p20,
@p21,
@p22,
@p23,
@p24
);
',N'@p5 uniqueidentifier,
@p6 uniqueidentifier,
@p7 datetime2(7),
@p8 uniqueidentifier,
@p9 datetime2(7),
@p10 uniqueidentifier,
@p11 uniqueidentifier,
@p12 datetime2(7),
@p13 uniqueidentifier,
@p14 datetime2(7),
@p15 uniqueidentifier,
@p16 uniqueidentifier,
@p17 datetime2(7),
@p18 uniqueidentifier,
@p19 datetime2(7),
@p20 uniqueidentifier,
@p21 uniqueidentifier,
@p22 datetime2(7),
@p23 uniqueidentifier,
@p24 datetime2(7)',
@p5='015B76FC-2833-45D9-85A7-AB1C389C1C11',
@p6=NULL,
@p7='2018-11-10 14:29:25.5363017',
@p8='35C38DF0-A959-4232-AADD-40DB2260F557',
@p9='2018-11-10 14:29:25.5363022',
@p10='538EE0DD-531A-41C6-8414-0769EC5990D8',
@p11=NULL,
@p12='2018-11-10 14:29:25.5363031',
@p13='35C38DF0-A959-4232-AADD-40DB2260F557',
@p14='2018-11-10 14:29:25.5363034',
@p15='8288D9AC-FBCE-417E-89EF-82266B284B78',
@p16=NULL,
@p17='2018-11-10 14:29:25.5363039',
@p18='35C38DF0-A959-4232-AADD-40DB2260F557',
@p19='2018-11-10 14:29:25.5363042',
@p20='4BCFE9F8-E4A5-49F0-B6EE-44871632A903',
@p21='35C38DF0-A959-4232-AADD-40DB2260F557',
@p22='2018-11-10 14:29:25.5363047',
@p23='35C38DF0-A959-4232-AADD-40DB2260F557',
@p24='2018-11-10 14:29:25.5363047'
我怀疑这个问题的根源在于"IsUnique(false")
我在第二个片段中突出显示的代码行,因为这只发生在AddedByAccountId
属性/列上。但是,如果我不包括这一行,那么创建的迁移脚本将添加一个唯一约束(我不想要)。
是否有人遇到过不应该创建约束或(更重要的是)NULL 值替换插入时的实际数据的问题?
干杯。
解决方案
// 出于某种原因
,如果我们不在这里设置,这个自引用键会在迁移脚本中生成一个唯一的 // 约束。// “ModifiedByAccountId”没有这个问题!我怀疑这可能
是 // NULL 数据问题的根本原因。
你怀疑是对的。EF Core 对 2 个自引用导航属性感到困惑,并错误地决定(可能是一个错误)它们代表一个单一的一对一关系:
一对一关系在双方都有一个参考导航属性。它们遵循与一对多关系相同的约定,但在外键属性上引入了唯一索引,以确保只有一个依赖项与每个主体相关。
当然,您想要的是两个一对多的关系,因此不要修复索引(正如您已经看到的那样,这将无济于事),只需明确地映射它们:
modelBuilder.Entity<Account>().HasOne(e => e.AddedByAccount).WithMany();
modelBuilder.Entity<Account>().HasOne(e => e.ModifiedByAccount).WithMany();
推荐阅读
- python - Python zip shutil
- c# - Linq没有分组
- vue.js - 错误:您不能在没有路径的集合上调用“get”。相反,首先检查“长度”属性以验证至少存在 1 个路径
- python - InvalidArgumentError when yielding List of Dicts inside TensorFlow 2.3.1 Dataset graph
- python-3.x - PythonBasics: How to select a substring from a series in a dataframe
- kubernetes - Kubernetes ClusterIP not balancing load across Pods
- automation - Appium Unknown server-side error occurred - did not start application
- android - 如何在android中更改日期选择器对话框的默认年份(1900)?
- python - 多产品定价问题的约束优化
- c# - MSI wix 升级 - 不时收到 FileNotFoundException