关于C#:ef codefirst:我应该初始化导航属性吗?

EF codefirst : Should I initialize navigation properties?

我看过一些书籍(例如,Programming Entity Framework Code First Julia Lerman)定义了它们的域类(POCO),但没有初始化导航属性,例如:

1
2
3
4
5
6
7
8
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; }
}

当生成poco时,其他一些书籍或工具(例如实体框架电动工具)初始化类的导航属性,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:哪一个更好?为什么?利弊?

编辑:

1
2
3
4
5
6
7
8
9
10
11
12
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
2
3
4
5
6
private ICollection<Address> _addresses;

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

它可以防止空引用异常,这样可以方便单元测试和操作集合,但也可以防止不必要的初始化。当一个类有相对多的集合时,后者可能会产生差异。缺点是它需要相对多的管道,特别是与没有初始化的自动属性相比。此外,C中的空传播运算符的出现使得初始化集合属性变得不那么紧迫。

…除非应用显式加载

唯一的问题是,初始化集合会使检查集合是否由实体框架加载变得困难。如果集合已初始化,则类似…

1
var users = context.Users.ToList();

…将创建具有空的、不为空的Addresses集合的User对象(延迟加载除外)。检查集合是否已加载需要类似以下代码…

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

如果集合未初始化,将执行简单的null检查。因此,当选择性显式加载是编码实践的重要组成部分时,即……

1
2
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

实体框架核心

实体框架核心(写入时为2.1)中的关系修正不受构造函数中初始化的引用导航属性的影响。也就是说,当分别从数据库中提取用户和地址时,将填充导航属性。但是,延迟加载不会覆盖初始化的引用导航属性。因此,总之,在ef core中初始化构造函数中的引用导航属性可能会导致问题。不要这样做。不管怎么说都没有道理,


在我的所有项目中,我都遵循规则-"集合不应为空"。它们要么是空的,要么是有值的。"

当创建这些实体是第三方代码(例如ORM)的职责,并且您正在处理一个短期项目时,第一个示例是可能的。

第二个例子更好,因为

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

实践领域驱动设计的人将集合公开为只读的,并避免在其上设置设置者。(请参阅nhibernate中只读列表的最佳实践)

问题1:哪一个更好?为什么?利弊?

最好公开非空的collections,因为您避免在代码中进行额外的检查(例如Addresses)。这是一个很好的合同在你的代码库。但我可以公开对单个实体的空引用(如License)

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

当我自己开发数据映射器模式时,我试图避免双向引用,很少有从子对象到父对象的引用。

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

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

  • 我用emty children collection建造了parent entity
  • 然后我在children collection中加上evey childparent entity
  • 如果在License类型i中引入无参数构造函数,则需要User属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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; }
    }


    Q1: Which one is better? why? Pros and Cons?

    当在实体构造函数中设置虚拟属性时,第二个变量有一个明确的问题,称为"构造函数中的虚拟成员调用"。

    对于没有初始化导航属性的第一个变量,根据对象的创建者/创建者,有两种情况:

  • 实体框架创建对象
  • 代码使用者创建对象
  • 当实体框架创建一个对象时,第一个变量是完全有效的。但在代码使用者创建对象时可能会失败。

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

  • 使默认构造函数受保护。实体框架可以与受保护的构造函数一起使用。

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

  • 这样,实体框架使用受保护的默认构造函数从某些数据源获取的数据创建有效对象,代码使用者使用静态工厂方法创建有效对象。


    这对new列表来说是多余的,因为您的poco依赖于延迟加载。

    Lazy loading is the process whereby an entity or collection of entities is automatically loaded from the database the first time that a property referring to the entity/entities is accessed. When using POCO entity types, lazy loading is achieved by creating instances of derived proxy types and then overriding virtual properties to add the loading hook.

    如果删除虚拟修饰符,那么将关闭延迟加载,在这种情况下,代码将不再工作(因为没有任何东西可以初始化列表)。

    注意,延迟加载是实体框架支持的一个特性,如果您在dbContext上下文之外创建类,那么依赖代码显然会受到NullReferenceException的影响。

    高温高压


    我使用的答案是:为什么我的实体框架代码第一个代理集合为空?为什么我不能设置它?

    构造函数初始化有问题。我这样做的唯一原因是使测试代码更容易。确保集合从不为空,这样可以在测试等中不断初始化


    其他答案完全回答了这个问题,但我想补充一些内容,因为这个问题仍然是相关的,并在谷歌搜索中出现。

    在Visual Studio中使用"数据库中的代码第一模型"向导时,所有集合的初始化方式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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; }
    }

    我倾向于将向导输出作为微软的官方建议,因此我为什么要在这个五年前的问题上增加内容。因此,我将所有集合初始化为HashSets。

    就个人而言,我认为调整上述内容以利用C 6.0的自动属性初始值设定项是相当巧妙的:

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