插入后获取实体导航属性

63

我有以下两个类:

public class Reward 
{
    public int Id { get; set; }
    public int CampaignId { get; set;
    public virtual Campaign Campaign { get; set; }
}

public class Campaign 
{
    public int Id { get; set; }
    public virtual ICollection<Reward> Rewards { get; set; }
}

我已经有了所有明显必要的东西,例如 DbContext 和映射。

现在假设我创建了一个奖励实体并像这样插入它:

var reward = new Reward { CampaignId = 1 };
context.Set<Reward>().Add(reward);
context.SaveChanges();

reward = context.Set<Reward>().SingleOrDefault(a => a.Id == reward.Id);
//reward.Campaign is null

显然我有一个id为1的广告系列,因此FK约束没问题。在此插入后,我的奖励实体已经设置了新的标识ID。现在的问题是,reward仍然只是我创建的Reward实体。而且这时候,reward.Campaign属性是null。EF似乎在内存中保留插入的实体,当我使用.SingleOrDefault(a => a.Id == reward.Id)时,它只是返回内存中的实体,而不是新的代理。这可能是一件好事。

所以问题是:如何在插入后访问或加载导航属性,或获取具有导航属性作为代理的新代理。

我是否可能是以错误的方式进行插入?


6个回答

98
如果我理解正确,您正在尝试在通过外键属性建立关系后急切地加载一个复杂的属性。 SaveChanges() 不会以任何方式加载复杂的属性。最多,如果您正在添加新对象,它将设置您的主键属性。
您的代码行 reward = context.Set<Reward>().SingleOrDefault(a => a.Id == reward.Id); 也不会以任何方式加载 Campaign,因为您的奖励对象未附加到上下文。 您需要明确地告诉EF加载该复杂对象或附加它,然后让延迟加载发挥其作用。
因此,在 context.SaveChanges();之后,您有三个选项来加载 reward.Campaign
  1. Attach() 奖励到上下文,以便可以惰性加载Campaign(在访问时加载)
  2.  context.Rewards.Attach(reward);
    

    注意:只有在上下文范围内才能惰性加载reward.Campaign,因此如果您不会在上下文寿命内访问任何属性,请使用选项2或3。

  3. 手动Load() Campaign属性

  4.  context.Entry(reward).Reference(c => c.Campaign).Load();
    

    或者如果Campaign是一个集合,例如Campaigns

     context.Entry(reward).Collection(c => c.Campaigns).Load();
    
  5. 手动Include() Campaign属性

  6.  reward = context.Rewards.Include("Campaigns")
         .SingleOrDefault(r => r.Id == reward.Id);
    

    鉴于您的内存中已经有reward,我建议使用Load

    有关更多信息,请查看此msdn文档中的加载相关对象部分


11
第二个选择对我也是最好的。 - Reuel Ribeiro
第二个有效!_db.Entry(product).Reference(b => b.Category).Load(); 最后它会加载类别信息和产品。谢谢! - hubert17
参考扩展位于哪个程序集中?context.Entry(reward).Reference(c => c.Campaign).Load(); 我只看到Reference()方法仅接受导航属性名称作为字符串。 - Dave Lawrence
1
我想提一下,因为我遇到了这个问题:当使用.Load()加载集合导航属性时,你需要调用.Collection()而不是.Reference(),否则会出现运行时错误。 - ZeRemz
当我想要加载另一个相关实体的相关实体或集合,例如“Campaigns”时,第二个选项如何工作? - Chris

11

如果您将您的reward对象创建为new Reward(),EF 将不会使用代理。相反,您应该像这样使用 DbSet.Create 创建它:

var reward = context.Set<Reward>().Create();
reward.CampaignId = 5;
context.SaveChanges();

接下来将它附加到您的 DbSet:

context.Rewards.Attach(reward);

最后,现在您可以使用延迟加载来获取相关实体:

var campaign = reward.Campaign;

我无法修改实体添加的方式。在保存后还有其他方法获取代理版本吗? - xr280xr

1
除了Carrie Kendall和DavidG(在VB.NET中),还有:
Dim db As New MyEntities
Dim r As Reward = = db.Set(Of Reward)().Create()
r.CampaignId = 5
db.Reward.Add(r) ' Here was my problem, I was not adding to database and child did not load
db.SaveChanges()

然后,属性r.Campaign可用。

1
我可以协助您进行翻译。以下是内容的汉语翻译:

我有一个简单的解决方案来解决这个问题。

不要将 CampaignID 添加到奖励中,而是添加活动对象 .. 所以:

var _campaign = context.Campaign.First(c=>c.Id == 1);//how ever you get the '1'
var reward = new Reward { Campaign = _campaign };
context.Set<Reward>().Add(reward);
context.SaveChanges();

//reward.Campaign is not null

实体框架在这里承担了所有的重活。

你可能认为加载整个Campaign对象是一种浪费,但如果你要使用它(从看起来的情况来看,似乎是的),那么我不明白为什么不能这样做。 如果需要访问Campaign对象的导航属性,甚至可以在上面获取时使用include语句...

var _campaign = context.Campaign.include(/*what ever you may require*/).First(c=>c.Id = 1);

0

如果您有多个导航属性或想要添加多条记录,可能很难通过这些方式实现。

因此,我建议如果内存不重要,在添加记录后创建一个新的上下文对象,并使用它代替原来的对象。


是的,我也提到了如果在另一个上下文中进行查询,则一切都正常工作,但是分离实体方法呢?为什么它在一个案例中起作用,在另一个案例中却抛出异常?也许整个问题,即返回导航属性为空,应该被归类为向实体开发团队报告的错误吗?看起来像是一个错误。在SaveChanges之后,实体跟踪会中断。我认为它应该发送一个查询并检索数据,但看起来对象被视为完整的。 - Alex

0
你尝试过使用Include()吗?像这样:
reward = context.Set<Reward>().Include("Campaigns").SingleOrDefault(a => a.Id == reward.Id);

我没有尝试过那个,但我肯定启用了惰性加载,除了首次将实体插入到数据库时,一切都正常工作。即使它能够工作,它也会破坏代码的一致性。基本上,我必须在插入后这样做,但在其他情况下我不需要它。执行分离更加一致,因为我可以在SaveChanges之后立即执行它。我不知道下次何时需要选择数据,以及何时真正需要包含和何时不需要。你可以说,每次都这样做,但是为什么我在这种情况下启用了惰性加载呢? - Alex

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