EF codefirst:我应该初始化导航属性吗?
Posted
技术标签:
【中文标题】EF codefirst:我应该初始化导航属性吗?【英文标题】:EF codefirst : Should I initialize navigation properties? 【发布时间】:2014-01-12 12:02:17 【问题描述】:我看过一些书籍(例如 Julia Lerman 首先编写实体框架代码)定义了它们的域类 (POCO),而没有初始化导航属性,例如:
public class User
public int Id get; set;
public string UserName get; set;
public virtual ICollection<Address> Address get; set;
public virtual License License get; set;
其他一些书籍或工具(例如Entity Framework Power Tools)在生成POCO时会初始化该类的导航属性,例如:
public class User
public User()
this.Addresses = new IList<Address>();
this.License = new License();
public int Id get; set;
public string UserName get; set;
public virtual ICollection<Address> Addresses get; set;
public virtual License License get; set;
Q1:哪个更好?为什么?利弊?
编辑:
public class License
public License()
this.User = new User();
public int Id get; set;
public string Key get; set;
public DateTime Expirtion get; set;
public virtual User User get; set;
Q2:在第二种方法中,如果 `License` 类也引用了 `User` 类,则会出现堆栈溢出。这意味着我们应该有单向引用。(?)我们应该如何决定应该删除哪一个导航属性?
【问题讨论】:
没有堆栈溢出的风险。您不是在创建实体实例,而只是创建列表的一个实例。 不,您将初始化列表与初始化列表中的项目混淆了。只要你初始化list,它就是空的,没有元素。 在构造函数中初始化虚拟属性真的是bad bad bad。坦率地说,我很惊讶看到应该更了解的作者将此作为解决方案提出。因为首先构造对象的基础部分,所以当访问这些虚拟成员时,子类构造函数尚未运行。如果虚方法被覆盖并且它们的实现依赖于子类构造函数中的初始化,它们就会中断。 EF 通过在运行时创建子类并覆盖虚拟成员来工作。发生此问题的重大风险。 我一直认为在 EF 中使用虚拟成员很方便,但最终还是存在缺陷。它可能对数据库造成比开发人员预期更多的命中。当您第一次访问数据库时,最好考虑一下您要加载的内容并.Include
。
引用是一个实体。集合包含实体。这意味着初始化集合在业务逻辑方面没有意义:它没有定义实体之间的关联。设置参考确实如此。因此,是否或如何初始化嵌入式列表纯粹是一个偏好问题。至于“如何”,有些人更喜欢惰性初始化: private ICollection _addresses;公共虚拟 ICollection 地址 获取 返回 this._addresses ?? (this._addresses = new HashSet());
【参考方案1】:
收藏:没关系。
作为导航属性的集合和引用之间有明显的区别。引用是一个实体。集合包含个实体。这意味着初始化集合在业务逻辑方面没有意义:它没有定义实体之间的关联。设置参考就可以了。
因此,是否或如何初始化嵌入式列表纯粹是一个偏好问题。
至于“如何”,有些人更喜欢惰性初始化:
private ICollection<Address> _addresses;
public virtual ICollection<Address> Addresses
get return this._addresses ?? (this._addresses = new HashSet<Address>());
它可以防止空引用异常,因此它有助于单元测试和操作集合,但它也可以防止不必要的初始化。当一个类有相对多的集合时,后者可能会有所不同。缺点是它需要相对较多的管道,尤其是。与没有初始化的自动属性相比。此外,C# 中空传播运算符的出现降低了初始化集合属性的紧迫性。
...除非应用显式加载
唯一的问题是初始化集合很难检查实体框架是否加载了集合。如果一个集合被初始化,一个类似...的语句
var users = context.Users.ToList();
...将创建 User
具有空的、非空的 Addresses
集合的对象(延迟加载除外)。检查集合是否已加载需要类似...的代码
var user = users.First();
var isLoaded = context.Entry(user).Collection(c => c.Addresses).IsLoaded;
如果集合未初始化,则可以进行简单的null
检查。 So when selective explicit loading is an important part of your coding practice, i.e. ...
if (/*check collection isn't loaded*/)
context.Entry(user).Collection(c => c.Addresses).Load();
...不初始化集合属性可能更方便。
参考属性:不要
引用属性是实体,因此为它们分配一个空对象是有意义的。
更糟糕的是,如果您在构造函数中启动它们,EF 在具体化您的对象或通过延迟加载时不会覆盖它们。在您主动替换它们之前,它们将始终具有它们的初始值。更糟糕的是,您甚至可能最终将空实体保存在数据库中!
还有另一个效果:关系修复不会发生。关系修复是 EF 通过其导航属性连接上下文中所有实体的过程。当 User
和 Licence
分别加载时,User.License
仍将被填充,反之亦然。当然,除非 License
在构造函数中被初始化。这也适用于 1:n 关联。如果Address
将在其构造函数中初始化User
,则User.Addresses
将不会被填充!
实体框架核心
Entity Framework 核心(撰写本文时为 2.1)中的关系修复不受构造函数中已初始化的引用导航属性的影响。也就是说,当分别从数据库中提取用户和地址时,会填充导航属性。 但是,延迟加载不会覆盖已初始化的参考导航属性。
在 EF-core 3 中,初始化引用导航属性会阻止 Include
正常工作。
因此,总而言之,在 EF-core 中,在构造函数中初始化引用导航属性可能会导致麻烦。不要这样做。反正也没有意义。
【讨论】:
哇,从未见过这样的想法,似乎是个好主意,但从未在任何项目/书籍中见过。 +1特殊答案。但我害怕使用这个。我更新了我的问题,你会为非收集引用属性做同样的技术吗? 不,因为用默认对象设置引用属性是没有用的。这些引用具有商业意义,因此它们应该是null
或引用有意设置的有意义的对象。
离题了,今天晚些时候,但是,你为什么要在这里特别使用 HashSet?
@stovroz 这是几个选项之一。 HashSet 针对快速查找进行了优化,并保证包含唯一的对象。 EF 默认使用 t4 或代码优先从数据库生成 HashSet。
我正在使用 EF Core。当我不初始化集合导航属性Addresses
并调用context.Users.Include(u=>u.Addresses)
时,我发现Addressess
不是空的,而是对于没有地址的用户的空列表。它总是正确的吗?【参考方案2】:
在我的所有项目中我遵循规则 - “集合不应为空。它们要么为空,要么具有值。”
当这些实体的创建由第三方代码(例如 ORM)负责并且您正在处理一个短期项目时,可能会有第一个示例。
第二个例子更好,因为
您确定该实体已设置所有属性 你避免傻NullReferenceException
让代码的使用者更快乐
实践领域驱动设计的人们将集合公开为只读,并避免对其进行设置。 (见What is the best practice for readonly lists in NHibernate)
Q1:哪个更好?为什么?利弊?
最好公开非空集合,因为您可以避免在代码中进行额外检查(例如Addresses
)。在您的代码库中拥有一份很好的合同。但是我可以公开对单个实体的可空引用(例如License
)
Q2:在第二种方法中,如果License
类也引用了User
类,则会出现堆栈溢出。这意味着我们应该有单向引用。(?)我们应该如何决定应该删除哪一个导航属性?
当我自己开发data mapper pattern 时,我尽量避免双向引用,并且很少有从孩子到父母的参考。
当我使用 ORM 时,很容易进行双向引用。
当需要使用双向参考集为我的单元测试构建测试实体时,我遵循以下步骤:
-
我用 emty
children collection
构建 parent entity
。
然后我将引用 parent entity
的每个 child
添加到 children collection
中。
在License
类型中设置无参数构造函数,我将要求user
属性。
public class License
public License(User user)
this.User = user;
public int Id get; set;
public string Key get; set;
public DateTime Expirtion get; set;
public virtual User User get; set;
【讨论】:
我已经更新了我的问题,请您检查第二个问题,并在您的答案中添加更多详细信息。 我已经为你的第二个问题添加了答案。集合不应为空的主要思想,但可以避免从子级引用父级。 感谢您的回答。您能解释一下First example is possible to have when creation of these entities is responsibility of third-part code
和test-entity
的意思吗?请将您的电子邮件地址添加到您的个人资料中。再次感谢。
test-entity
是您在单元测试中使用的虚拟或存根实体。在这种情况下,third-part code
是 ORM。因此,First example is possible
当您使用初始化实体/集合的 ORM 时,当您拥有确保集合不为空的通用基础架构时。【参考方案3】:
new
列表是多余的,因为您的 POCO 依赖于延迟加载。
延迟加载是在第一次访问引用实体或实体的属性时自动从数据库加载实体或实体集合的过程。使用 POCO 实体类型时,通过创建派生代理类型的实例,然后覆盖虚拟属性以添加加载钩子来实现延迟加载。
如果您要删除 virtual 修饰符,那么您将关闭延迟加载,在这种情况下您的代码将不再工作(因为没有任何东西会初始化列表)。
注意延迟加载是实体框架支持的功能,如果你在 DbContext 的上下文之外创建类,那么依赖代码显然会受到NullReferenceException
的影响
HTH
【讨论】:
我很快学会了在许多场景中禁用延迟加载,因为它会迫使您有效地获取数据。所以是的:我总是初始化导航集合(和复杂类型),但从不初始化单数引用属性。 如果为真,这似乎是最好的答案,即使用延迟加载(问题的情况)意味着初始化由 EF 完成,不使用延迟加载需要在用户代码中进行初始化。这个答案很明确,而且直截了当,其他答案试图绕过实际问题并谈论“通常做什么”,因为他们不知道 EF 实际做了什么。不过,对文档的引用会很好,因为有许多教程使用延迟加载和手动初始化,a question 关于空引用。【参考方案4】:Q1:哪个更好?为什么?利弊?
在实体构造函数中设置虚拟属性的第二个变体有一个明确的问题,称为“Virtual member call in a constructor”。
至于第一个没有初始化导航属性的变体,有两种情况取决于谁/什么创建了一个对象:
-
实体框架创建一个对象
代码使用者创建一个对象
第一个变体在 Entity Framework 创建对象时完全有效, 但在代码使用者创建对象时可能会失败。
确保代码使用者始终创建有效对象的解决方案是使用static factory method:
使默认构造函数受保护。 Entity Framework 可以很好地与受保护的构造函数一起使用。
添加一个创建空对象的静态工厂方法,例如User
对象,设置所有属性,例如Addresses
和License
,创建后返回一个完全构造的User
对象
这样,Entity Framework 使用受保护的默认构造函数根据从某些数据源获得的数据创建有效对象,而代码使用者使用静态工厂方法创建有效对象。
【讨论】:
【参考方案5】:我使用这个Why is my Entity Framework Code First proxy collection null and why can't I set it?的答案
构造函数初始化有问题。我这样做的唯一原因是使测试代码更容易。确保集合永远不会为空,这样我就可以在测试等中不断初始化
【讨论】:
【参考方案6】:其他答案完全回答了这个问题,但我想补充一点,因为这个问题仍然相关并且出现在谷歌搜索中。
当您在 Visual Studio 中使用“数据库中的代码优先模型”向导时,所有集合的初始化方式如下:
public partial class SomeEntity
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public SomeEntity()
OtherEntities = new HashSet<OtherEntity>();
public int Id get; set;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<OtherEntity> OtherEntities get; set;
我倾向于将向导输出视为基本上是微软的官方推荐,因此我为什么要添加这个五年前的问题。因此,我会将所有集合初始化为 HashSet
s。
就我个人而言,我认为调整以上内容以利用 C# 6.0 的自动属性初始化器会非常巧妙:
public virtual ICollection<OtherEntity> OtherEntities get; set; = new HashSet<OtherEntity>();
【讨论】:
以上是关于EF codefirst:我应该初始化导航属性吗?的主要内容,如果未能解决你的问题,请参考以下文章