EF CodeFirst:我应该初始化导航属性吗?

81

我看过一些书(例如programming entity framework code first 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; }
}

在生成POCOs时,一些其他的书籍或工具(例如Entity Framework Power Tools)会初始化类的导航属性,例如:

一些其他的书籍或工具,例如Entity Framework Power Tools,在生成POCOs时会初始化类的导航属性。

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; }
}

问题1:哪一个更好?为什么?优缺点是什么?

编辑:

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; }
}

问题2:如果`License`类也引用了`User`类,则在第二种方法中会出现堆栈溢出。这意味着我们应该有单向引用。我们应该如何决定哪个导航属性应该被删除?


1
不,你混淆了初始化列表和初始化列表上的项目。只要你初始化列表,它就是空的,没有元素。 - Wiktor Zychla
@WiktorZychla: 哦,伙计,你说得对,集合不应该导致 stackoverflow,因为我们只是新建了集合而不是集合中的项。我的问题是关于一对一关系,比如 personaccount 之间的关系确实会导致 stackoverflow。我已经更新了问题。 - Iman Mahmoudinasab
5
在构造函数中初始化虚属性真的非常糟糕。坦白地说,我很惊讶看到这被一些应该更好知道的作者提出作为解决方案。因为对象的基础部分是首先构建的,当访问这些虚成员时,子类构造函数尚未运行。如果虚方法被覆盖并且它们的实现依赖于子类构造函数中的初始化,则它们将崩溃。EF通过在运行时创建一个子类并覆盖虚成员来工作。出现这个问题的风险非常大。 - spender
3
我一直认为在EF中使用虚拟成员很方便,但最终存在缺陷。它可能会导致许多比开发人员预期的更多的数据库查询。最好在第一次查询数据库时考虑要加载的内容,并使用.Include来进行。 - spender
1
一个引用是一个实体。集合包含实体。这意味着在业务逻辑方面初始化集合是没有意义的:它不定义实体之间的关联。设置引用则有意义。因此,初始化嵌套列表纯粹是个人喜好问题,无论是否以及如何进行。至于“如何”,有些人更喜欢延迟初始化:private ICollection<Address> _addresses;public virtual ICollection<Address> Addresses { get { return this._addresses ?? (this._addresses = new HashSet<Address>()); } - user16559547
显示剩余2条评论
6个回答

99

集合:它并不重要。

集合和导航属性中的引用有着明显的区别。一个引用是一个实体,而一个集合包含多个实体。这意味着在业务逻辑层面上初始化一个集合是没有任何意义的,因为它不能定义实体之间的关联。设置一个引用可以定义关联。

所以,是否以及如何初始化嵌套列表仅仅是个人偏好。

至于“如何”,有些人更喜欢延迟初始化:

private ICollection<Address> _addresses;

public virtual ICollection<Address> Addresses
{ 
    get { return this._addresses ?? (this._addresses = new HashSet<Address>());
}

它可以防止空引用异常,因此有助于单元测试和操作集合,但也可以防止不必要的初始化。当一个类有相对较多的集合时,后者可能会产生差异。缺点是它需要相对较多的管道,特别是与没有初始化的自动属性相比。此外,在C#中出现了空传播运算符,使初始化集合属性变得不那么紧急。
除非应用了显式加载,否则...
唯一的问题是,初始化集合会使检查Entity Framework是否已加载集合变得困难。如果初始化了一个集合,像这样的语句...
var users = context.Users.ToList();

...将创建具有空的、非空的Addresses集合的User对象(除了懒加载)。检查集合是否已加载需要编写以下代码...

var user = users.First();
var isLoaded = context.Entry(user).Collection(c => c.Addresses).IsLoaded;

如果集合未初始化,则只需进行简单的null检查即可。因此,当有选择性的显式加载是您编码实践的重要部分时,即...
if (/*check collection isn't loaded*/)
    context.Entry(user).Collection(c => c.Addresses).Load();

...不初始化集合属性可能更方便。

引用属性:不要这样做

引用属性是实体,因此将空对象分配给它们是有意义的

更糟糕的是,如果在构造函数中初始化它们,EF在实例化对象或延迟加载时不会覆盖它们。它们始终具有其初始值,直到你主动替换它们。更糟糕的是,你甚至可能最终在数据库中保存空实体!

还有另一个影响:关系修复不会发生。关系修复是EF通过导航属性连接上下文中的所有实体的过程。当分别加载UserLicence时,User.License仍将被填充,反之亦然。除非当然,如果License在构造函数中初始化。对于1:n关联,这也是正确的。如果Address在其构造函数中初始化一个User,则User.Addresses将不被填充!

Entity Framework core

在Entity Framework core(2.1撰写时),初始化引用导航属性不会影响关系修复。也就是说,当用户和地址分别从数据库中提取时,导航属性会被填充。
但是,延迟加载不会覆盖初始化的引用导航属性。

在EF-core 3中,初始化引用导航属性会阻止Include正常工作。

因此,总之,在EF-core中初始化引用导航属性可能会引起问题。不要这样做。它毫无意义。


哇,从未见过这样的东西,看起来是个好主意,但在任何项目/书籍中都没有见过。特别回答+1。但我害怕使用这个。我更新了我的问题,你会对非集合引用属性采用相同的技术吗? - Iman Mahmoudinasab
3
不行,因为使用默认对象设置参考属性是没有意义的。这些引用具有业务含义,所以它们应该是 null 或者指向有意义的对象,而不是默认对象。 - Gert Arnold
2
离题并且时间有点晚了,但是,为什么你会在这里特别使用 HashSet? - stovroz
2
@stovroz 这是其中几个选项之一。HashSet 优化了快速查找,并保证包含唯一对象。EF 默认使用 t4 或从数据库生成代码时会生成 HashSets。 - Gert Arnold
1
我正在使用EF Core。当我不初始化集合导航属性“Addresses”并调用“context.Users.Include(u => u.Addresses)”时,我发现对于没有地址的用户,“Addressess”不是null,而是一个空列表。这总是正确的吗? - Second Person Shooter
显示剩余2条评论

9

在我所有的项目中,我遵循这个规则 -“集合不应该为null。它们要么为空,要么有值。”

第一个示例可能会出现在创建这些实体是第三方代码(例如ORM)的责任,而您正在短期项目上工作。

第二个例子更好,因为:

  • 您确定实体已设置了所有属性
  • 您避免了愚蠢的NullReferenceException
  • 您使您代码的消费者更快乐

练习领域驱动设计的人将集合公开为只读,并避免对其进行设置器。(见NHibernate中只读列表的最佳实践是什么

Q1:哪个更好? 为什么? 优缺点是什么?

公开非空集合更好,因为您可以避免在代码中进行额外的检查(例如Addresses)。 在您的代码库中拥有良好的契约是很好的。 但是对于单个实体的可空引用(例如License),对我来说也可以。

Q2:如果License类也引用了User类,则第二种方法将出现堆栈溢出。 这意味着我们应该有单向引用。(?)我们应该如何决定哪个导航属性应该被移除?

当我自己开发数据映射器模式时,我尝试避免双向引用,并且很少从子级到父级引用。

当我使用ORM时,很容易拥有双向引用。

当需要构建具有双向引用集的测试实体以进行单元测试时,我遵循以下步骤:

  1. 我使用空的children collection构建parent entity
  2. 然后,我将每个具有对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; }
}

我已经更新了我的问题,请您检查第二个问题,并在您的答案中添加更多细节。 - Iman Mahmoudinasab
我已经回答了你的第二个问题。主要思想是集合不应该为空,但可以避免从子对象引用父对象。 - Ilya Palkin
谢谢您的回答。您能否解释一下“第一个例子是在这些实体的创建是第三方代码的责任时可能存在”的意思以及“test-entity”是什么?并且,您能否将您的电子邮件地址添加到您的个人资料中。再次感谢。 - Iman Mahmoudinasab
1
test-entity 是您在单元测试中使用的虚拟或存根实体。在这种情况下,third-part code 是 ORM。因此,在使用 ORM 初始化实体/集合时,当您具有确保集合不为空的通用基础设施时,第一个示例是可能的 - Ilya Palkin

4
其他回答已经完全回答了这个问题,但是我想补充一些内容,因为这个问题仍然相关,并且在谷歌搜索中出现。
当您在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; }
}

我倾向于将向导输出视为来自Microsoft的官方建议,因此我正在回答这个五年前的问题。因此,我会将所有集合初始化为HashSet

而且,我个人认为,利用C# 6.0的自动属性初始化程序对上述内容进行微调是相当不错的:

    public virtual ICollection<OtherEntity> OtherEntities { get; set; } = new HashSet<OtherEntity>();

4
“new”列表是多余的,因为您的POCO依赖于延迟加载。
延迟加载是指在第一次访问引用实体/实体集合的属性时,自动从数据库中加载该实体或实体集合的过程。使用POCO实体类型时,通过创建派生代理类型的实例,然后重写虚拟属性以添加加载钩子来实现延迟加载。
如果您删除虚拟修饰符,则会关闭延迟加载,在这种情况下,您的代码将不再起作用(因为没有任何内容初始化列表)。
请注意,延迟加载是Entity Framework支持的功能,如果在DbContext之外创建类,则相关代码显然会遭受NullReferenceException的影响。
希望这能帮到您。

我很快学会了在许多情况下禁用延迟加载,因为它强制你高效地获取数据。所以是的:我总是初始化导航集合(和复杂类型),但从不初始化单数引用属性。 - Dabblernl
如果为真,这似乎是最好的答案,即使用延迟加载(问题的情况)意味着初始化由EF完成,不使用延迟加载需要在用户代码中进行初始化。这个答案很清晰明了,其他答案试图绕过实际问题并谈论“通常做什么”,因为他们不知道EF实际上做了什么。虽然有许多使用手动初始化和关于空引用的问题的教程使用延迟加载,但对文档的引用也是好的。 - mins

3

问:哪一个更好?为什么?有什么优点和缺点?

第二种方法在实体构造函数内设置虚拟属性时存在一定的问题,被称为"在构造函数中调用虚拟成员"。

至于第一种方法,即不初始化导航属性,在对象创建者是谁/什么的情况下有两种情况:

  1. Entity Framework创建一个对象
  2. 代码消费者创建一个对象

第一种方法在Entity Framework创建对象时完全有效,但是在代码消费者创建对象时可能会失败。

确保代码消费者始终创建有效对象的解决方案是使用静态工厂方法

  1. 将默认构造函数设置为protected。Entity Framework可以使用受保护的构造函数工作。

  2. 添加一个静态工厂方法来创建一个空对象,例如一个User对象,在创建后设置所有属性,例如AddressesLicense ,并返回一个完整构造的User对象

这样,Entity Framework使用受保护的默认构造函数从某些数据源获取数据来创建一个有效的对象,而代码消费者使用静态工厂方法创建一个有效的对象。


2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接