具有虚拟导航属性的深度复制/复制对象
Posted
技术标签:
【中文标题】具有虚拟导航属性的深度复制/复制对象【英文标题】:Deep Copy/Duplicating Object with Virtual Navigation Properties 【发布时间】:2022-01-08 10:08:49 【问题描述】:我在 C#/Blazor 中工作
我有一个对象,比如Project
,我从带有外键及其相关导航属性的数据库中获取该对象。我正在获取对象,然后在断开连接状态下使用它。
一旦对象被提取,它就会被输入到一个表单中,以便根据需要显示/编辑/更新。我想创建一个单独的 Project
克隆,以在表单中用作 DTO,以便可以丢弃任何更改,而不会对原始获取的 Project
产生引用问题。
例如,这是一个简化的Project
类:
public partial class Project
[Key]
public int Id get; set;
[Required]
[StringLength(150)]
public string ProjectName get; set;
[Column("UpdatedBy_Fk")]
public int UpdatedByFk get; set;
[ForeignKey(nameof(UpdatedByFk))]
[InverseProperty(nameof(UserData.ProjectUpdatedByFkNavigations))]
public virtual UserData UpdatedByFkNavigation get; set;
在表单中,我使用@project.UpdatedByFkNavigation.FullName
显示最后一个更新Project
的人的全名。用户根本无法修改导航字段,它只是显示。
我的问题是关于复制导航项。现在为简单起见,在表单的OnInitialized
中,我将原始project
对象传递给表单,并使用如下构造函数创建一个新的objProject
:
Project objProject = new() Id = project.Id,
ProjectName = project.ProjectName,
UpdatedByFk = project.UpdatedByFk,
UpdatedByFkNavigation = project.UpdatedByFkNavigation,
这似乎正在工作并创建一个单独的 Project
对象,它不是引用并且我可以用作我的 DTO,但是我不确定以这种方式分配 virtual
属性是否合适。
这种方法是否遵循创建具有虚拟导航字段的对象的非引用副本的最佳实践,还是我应该采用不同的方法?
【问题讨论】:
【参考方案1】:这取决于关系。引用在 EF 中很重要,因此您需要考虑是否希望新克隆引用 same UserData 或具有相同数据的新且不同的 UserData。通常在多对一关系中,您希望使用相同的引用,或更新引用以匹配。如果原版被“John Smith” ID #201 修改,则克隆将被“John Smith” ID #201 修改,或者更改为当前用户“Jane Doe” ID #405,这将是相同的“Jane Doe”引用作为用户修改的任何其他记录。您可能不希望 EF 创建一个以 ID #545 结尾的新“John Doe”,因为 EF 获得了对具有“John Doe”副本的 UserData 的全新引用。
因此,在您的情况下,我假设您希望引用相同的现有用户实例,因此您的方法是正确的。您需要小心的是使用序列化/反序列化等快捷方式进行克隆时。在这种情况下,序列化项目和任何加载的 UpdatedBy 引用将创建一个具有相同字段甚至 PK 值的 UserData 的新实例。但是,当您使用新的 UserData 引用保存这个新项目时,您要么会遇到重复的 PK 异常、“已跟踪具有相同键的对象”异常,要么会发现自己遇到新的“John Doe " 如果该实体设置为期望其 PK 的标识列,则记录 ID 为 #545。
关于使用导航属性与 FK 字段的典型建议:我的建议是使用其中一个,而不是两者都使用。这样做的原因是,当您同时使用两者时,您有两个关系的真实来源,并且根据实体的状态,当您更改一个时,另一个不一定会自动反映更改。例如,我通过以下方式查看关系的一些代码:project.UpdatedByFk
,而其他代码可能使用project.UpdatedByFkNavigation.Id
。当涉及到导航属性时,您的命名约定有点奇怪。对于您的示例,我本来期望:
public virtual UserData UpdatedBy get; set;
一般来说,我会单独使用导航属性,并依赖 EF 中的阴影属性作为 FK。这看起来像:
public partial class Project
[Key]
public int Id get; set;
[Required]
[StringLength(150)]
public string ProjectName get; set;
[ForeignKey("UpdatedBy_Fk")] // EF Core.. For EF6 this needs to be done via configuration using .Map(MapKey()).
public virtual UserData UpdatedBy get; set;
在这里,我们定义了导航属性,并通过指定 FK 列名称,EF 将在幕后为该 FK 创建一个不可直接访问的字段。我们的代码揭示了这种关系的一种真实来源。
在某些速度很重要并且我几乎不需要相关数据的情况下,我将声明 FK 属性并且没有导航属性。
参考这个:
[InverseProperty(nameof(UserData.ProjectUpdatedByFkNavigations))]
我还建议避免双向引用,除非出于同样的原因绝对必要。如果我希望所有项目都由给定用户最后修改,我真的不打算通过以下方式获得任何收益:
var projects = context.Users
.Where(x => x.Id == userId)
.SelectMany(x => x.UpdatedProjects)
.ToList();
我只会使用:
var projects = context.Projects
.Where(x => x.UpdatedBy.Id == userId)
.ToList();
一般而言,您应该通过聚合根来组织您的域和其中的关系:本质上是在应用程序中具有***重要性的实体。双向引用具有类似的问题,即在从一侧修改这些关系时,两个事实来源不一定在给定时间点匹配。这在很大程度上取决于是否所有关系都被预先加载。
如果两个实体都是聚合根并且关系足够重要,那么这可以提供双向引用和应有的额外关注。一个很好的例子可能是多对多关系,例如 CourseClass(即数学 A 类)和 Student 之间的关系,其中 CourseClass 有很多学生,而 Student 有很多 CourseClass,从 CourseClass 的角度来看,列出是有意义的是学生,并从学生的角度列出他们的 CourseClasses。
【讨论】:
哇,史蒂夫,这真是太有用了。谢谢你。我确实想为此更新引用同一个 UserData 实例,并且我只是尝试更新Project
而不是任何关联的 Navigation 对象。导航对象或多或少地用于显示目的,但我理解您所说的可能会在保存时创建未来错误。我将重新阅读这个答案几次,并确保我正确地实施它。这里有很多值得思考的地方。
为了清楚了解导航属性和字段的创建方式,我们使用 EF Core Power Tools 的 Db-first 方法对类和 DbContext 进行逆向工程。我创建了一个数据库表Project
,其中包括CreatedByFk
和约束引用UserData
。逆向工程师根据约束自动吐出 InverseProperty 和所有这些。我需要重新评估这一点,并确保它没有做不恰当的事情。
啊,这可以解释命名。可能有一些方法可以配置该工具以期望和处理命名约定。 IE。它可能期待像“CreatedById”这样的东西,并且可以更好地处理该名称,但可能能够配置为处理“*Fk”。老实说,我从来都不喜欢使用设计器/工具进行 EF 映射。在 FK 名称期望方面,EF 本身有一些约定上的奇怪之处。在最坏的情况下,您可以使用它来帮助设置初始映射,修复它建议的奇怪映射,然后接管映射的所有权以手动管理。
此外,工具通常希望在不知道哪些对有用的双向引用实际上足够重要的情况下映射关系,因此它们映射一切。将它们用作第一遍然后删除任何不必要的东西的另一个原因。引用不会破坏任何内容,但在处理具有从未使用过的属性的域时,它们可能会增加“噪音”,或者当有多种方法来解释获取可能最终导致意外延迟加载的数据时会造成混乱。跨度>
肯定有命名约定的配置选项和你可以设置的东西,但我没有深入研究它们。我对 EF 还很陌生,所以我只是假设该工具比我知道的更多,并让它以标准默认值运行,直到我能更快地掌握它。可能不是一个很好的长期战略 :) 我将再看看双向参考。以上是关于具有虚拟导航属性的深度复制/复制对象的主要内容,如果未能解决你的问题,请参考以下文章
如何在不替换 ES6/Javascript 中的整个属性的情况下深度复制对象 [重复]
深度复制的坑1对象assign复制的假深度,2数组slice复制的坑,3还有数组map复制的坑