我最近更新了Entity Framework
(版本6.2.0)映射,以提高应用程序的性能,但未修改数据库架构。基本上,我有一个One-to-Many
关系,后来变成了One-to-One
关系。
[当我使用此新映射创建新对象时,一切都会按预期方式进行:这些对象将以正确的值保存在数据库中,并位于外键列等中。这些对象,我实体的导航属性就实现了。
当我尝试访问此EF映射更新之前创建的对象时,会发生问题。对于那些旧对象,导航属性为null,我无法访问关系的另一端。
具体地,我有两个这样定义的表:
______ _____________
|Sample| |Sample_Result|
|------| |-------------|
|Id |<--------|Result_Of |
|______| Fk |_____________|
[从业务角度来看,一个样本只能有一个结果。但是我们曾经将它映射为Entity Framework中的一对多关系,如下所示:
[Table("Sample")]
public class SampleEntity
{
public virtual IList<SampleResultEntity> Results { get; set; }
}
[Table("Sample_Result")]
public class SampleResultEntity
{
[Required]
[Column("Result_Of")]
public Guid ResultOfFk { get; set; }
[ForeignKey(nameof(ResultOfFk))]
public virtual SampleEntity ResultOf { get; set; }
}
我们在此映射上的表现很差,所以我最近将其更新为一对一关系:
[Table("Sample")]
public class SampleEntity
{
public virtual SampleResultEntity Result { get; set; }
}
[Table("Sample_Result")]
public class SampleResultEntity
{
[Required]
[Column("Result_Of")]
public Guid ResultOfFk { get; set; }
public virtual SampleEntity ResultOf { get; set; }
}
并且在我的dbContext中,我添加了以下映射:
modelBuilder.Entity<SampleResultEntity>()
.HasRequired(sr => sr.ResultOf)
.WithRequiredDependent(s => s.Result);
当我访问映射更新后创建的样本时,定义了sample.Result
。
当我访问映射更新之前创建的样本时,sample.Result
为空。
当我使用两个表的内部联接查询数据库时,如下所示:
SELECT *
FROM Sample s
INNER JOIN Sample_Result sr ON s.Id = sr.Result_Of;
我不会“丢失”行(如果我有40个样本,我会得到40行)。因此,外键设置正确。
有人可以向我解释这种行为吗?
您可能需要概述完整的表结构,因为根据您的描述,您不能简单地基于模式将映射从1对多更改为1对1。
一对多模式将看起来像这样:
Sample
SampleID (PK)
SampleResult
SampleResultID (PK)
SampleID (FK)
一对一模式将如下所示:样品SampleID(PK)
SampleResult
SampleID (PK + FK)
因此,在实际上包含1到1行的1对多模式中,您将拥有Sample ID = 1,且SampleResult的SampleResultID = 16且SampleID = 1。如果仅将映射更改为1对1,EF会期望两个表上的PK相等。由于SampleResult的PK为SampleResultId,因此无法获得正确的记录,因为它会比较Sample.SampleId / w SampleResult.SampleResultId(而非SampleResult.SampleId)
您的新测试记录可能会起作用,因为您可能会发现:
Sample.SampleID = **220**
SampleResult.SampleResultID = **220**
SampleResult.SampleID = 220
如果您使用身份(Int)列作为键,则在出现空链接或获取错误结果的链接时,此问题可能会有所不同。因为您使用的是GUID,并且每个ID相对通用,所以结果将是空链接。
我建议在加载样本/结果对以验证是否连接了正确的列时,在测试数据库上使用探查器捕获正在使用的SQL。
无需更改架构,只要将SampleResult.SampleResultId设置为Identity / Default,和您就可以保证不存在一对多实例(单个样本有多个结果)能够更新EF中的映射,以将SampleResult上的SampleId视为键。这样,使用新的映射EF会将2链接在一起。这将意味着您之前的“新”测试记录将不再起作用,因为它们会将Sample.SampleID与SampleResult.SampleResultID结合使用,但是您的原始记录应该起作用,而新记录也应该起作用。 EF的键不必与表上的PK匹配,只要您使用的是DB-First(无迁移)并且满足模式即可。如果SampleResultId的DB列默认为NewSequentialId()
或NewId()
,则无需担心。如果PK是用代码设置的,则可能需要将该属性映射为常规列,并记下使用SampleId的注意事项。如果您有任何其他基于SampleResultId链接到SampleResult的实体,则可能会“中断”。加入时,EF将把SampleId作为键。
[最后一点:我对Sample和SampleResult之间的一对多关系会导致性能问题这一结论感到有些警觉,可以通过重新映射为1-to-1来解决该问题。虽然一对一关系仅包含一个孩子会产生误导,但最终,EF在这两种情况下都将使用内部联接。我强烈怀疑还有其他事情在起作用。如果将NewId()
/ Guid.New
用于新行,则随着系统的增长,GUID键可能会出现问题。这是因为聚簇索引中128位密钥的相对随机性可能导致索引表中的大量碎片。最好使用NewSequentialId()
或基于代码的NewSequentialId()
实现来生成唯一的但可排序的GUID,以将这种影响最小化,并将其与数据库表的常规索引维护相结合。