Entity Framework:我设置了外键,SaveChanges之后访问导航属性,但它没有加载相关实体。为什么?

39

我正在使用Entity Framework 5 Code First中的此实体类:

public class Survey
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    public string SurveyName { get; set; }

    [Required]
    public int ClientID { get; set; }

    [ForeignKey("ClientID")]
    public virtual Client Client { get; set; }
}

而在我的控制器的Create方法中,我这样做:

    Survey entity = new Survey()
    {
        SurveyName = "Test Name",
        ClientID = 4
    };
    db.Surveys.Add(entity);
    db.SaveChanges();
    Client c1 = entity.Client;                    //Why is this null?
    Client c2 = db.Clients.Find(entity.ClientID); //But this isn't?

    string s2 = c2.ClientName;
    string s1 = c1.ClientName;   //null reference thrown here

在SaveChanges后,Client导航属性仍然为null。我原以为调用会从数据库加载Client,因为外键存在。为什么它没有这样做?

编辑 这里的代码来自于我的控制器依赖于DbContext时。不久之后,我重构了代码,使用了存储库和工作单元。这部分移动是由于当我想要使用new时使用Create感觉不对。然后发生的事情是我遇到了一个问题,即如何确保在使用存储库模式时创建代理

4个回答

64
为了确保在创建父级后,导航属性的延迟加载能够正常工作,您不能使用 new 操作符创建 Survey ,而是必须通过上下文实例来创建它,因为这将实例化一个动态代理,可以惰性加载相关的 Client。这就是 DbSet<T>.Create() 方法的作用:
Survey entity = db.Surveys.Create();

entity.SurveyName = "Test Name";
entity.ClientID = 4;

db.Surveys.Add(entity);
db.SaveChanges();

Client c1 = entity.Client;
string s1 = c1.ClientName;
// will work now if a Client with ID 4 exists in the DB

强调一下:不是这行代码entity.ClientID = 4;或者db.Surveys.Add(entity);或者db.SaveChanges从数据库中加载客户端,而是这行代码Client c1 = entity.Client;(延迟加载)。


1
顺便说一下:我把db.Surveys.Add(entity);和db.SaveChanges();这两行代码移到最后,但仍然出现了空引用错误。我必须在访问属性之前放置db.Surveys.Add(entity);代码才能使延迟加载正常工作...所以看来我必须使用Create和Add。 - Colin
为确保在使用仓储模式时创建代理,请参阅此链接:https://dev59.com/P3LYa4cB1Zd3GeqPYoIC#16811976。 - Colin
这非常有用,太感谢了! - Roman
谢谢!我讨厌使用.Include扩展,因为它给我的存储库模式增加了一层复杂性,我觉得这应该自动发生。正如所述,使用new关键字会阻止db上下文中的延迟加载。因此,我像您在上面的示例中所示一样使用Entity.Create()方法,现在EF领域中一切都很好。 - dynamiclynk
如果EF允许您将所有的代码优先模型定义为抽象类,那么更好的确保代理被创建的方法就是这样。 - eoleary
显示剩余2条评论

11

就像@NicholasButler所说的那样,调用SaveChanges会按字面意思执行 - 如果您调试代码,可以看到Intellitrace输出将显示为插入/更新生成的SQL,但不会有后续的选择。

请记住,除非您使用Include方法进行急切加载,否则在执行检索时不会加载相关实体,因此创建/更新它们也是如此。

我认为,自版本4.1以来,Entity Framework支持惰性加载。这意味着,如果启用了它,则像Client c1 = entity.Client;这样的代码应该加载该Client对象。需要明确的是,此操作与SaveChanges调用无直接关系。

最好检查db.Configuration.LazyLoadingEnabled是否设置为true。如果没有,请尝试将其设置为true,然后查看Client c1 = entity.Client;是否仍为空。

简而言之,调用SaveChanges不会触发加载,但是如果启用了惰性加载,并且实体尚未加载,则访问entity.Client应该会触发实体的加载。

编辑:

我早该想到这个问题了,但您无法在Survey entity对象上获得惰性加载。原因是EF通过创建一个从您的类派生但覆盖标记为virtual的属性以支持懒惰加载的类来实现其懒惰加载魔法。当您执行检索时,它会这样做,因此您的entity对象不会惰性加载任何内容。

在调用SaveChanges后尝试以下操作:

Survey entity2 = db.Surveys.Find(entity.ID);
Client c1 = entity2.Client;

这应该展现出你想要的行为。


我正在使用EF 5.0,我已经检查了db.Configuration.LazyLoadingEnabled为true,但访问entity.Client仍然返回null,我很担心。一定是其他问题... - Colin
3
顿悟!“原因是EF通过创建一个继承自您的类的类,并重写标记为虚拟以支持延迟加载的属性,来实现其延迟加载功能。”谢谢您的解释。这个建议的一个问题是,在SaveChanges之后执行Find会导致额外的数据库访问。 - Colin
我发现如果先获取客户端并将其分配给导航属性,那么它就可以工作了...我也喜欢Slauma的建议。 - Colin
我的建议更多是为了测试懒加载在这个特定情况下是否符合您的要求。在一般情况下,使用Create()方法可能会更有用。 - nick_w

0

除非我漏掉了什么,否则只是说每个导航属性必须声明为虚拟的。尽管如此,我尝试了你说的方法,但仍然出现空引用错误。 - Colin

-1
我期望从数据库中加载客户端的调用,因为外键存在。为什么它没有这样做?
这是因为您没有要求它这样做。在调用SaveChanges()之后,EF没有引用行中的数据,因此它不会进行可能冗余的数据库调用以获取数据。
调用db.Clients.Find(...告诉EF去从数据库中获取该行,这就是为什么它返回对象的原因。

我以为Entity Framework会自动同步外键和关系?请参阅http://msdn.microsoft.com/en-us/data/jj713564.aspx - Colin
加载客户端行需要单独的 SQL 调用,因为您还没有将该数据加载到上下文中。除非您告诉它,否则 EF 不会执行该操作。 - Nick Butler
如果我设置“客户端”导航属性,它会自动填充ClientID属性(好的,它不需要调用数据库来执行此操作)。但我认为,如果外键已经设置,将调用数据库似乎是有意义的。为什么这“可能是多余的”? - Colin
1
有趣的是,新创建的对象会自动获取其主键字段的值,这使得它看起来像是从数据库返回了数据。 - Isaac Kleinman

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