【问题标题】:EF code first one to many navigate from child to parentEF 代码第一个一对多从子导航到父
【发布时间】:2021-07-26 04:32:14
【问题描述】:

我有点不明白为什么这段代码不起作用。我有一个基本的一对多关系,我加载一个父母并包括它的孩子。后来我试图从孩子导航回父母,但父母是空的,我不知道为什么。

问题:

为什么我不能查询父/子对象的图表并在它们之间从子对象向后导航到父对象?父级始终为空。

这是实体。

public class Budget
{
    [Key]
    public int Id { get; set; }

    public ICollection<Expense> Expenses { get; set; }

    public ICollection<Income> Incomes { get; set; }
}

public class Expense
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    [StringLength(200)]
    public string ExpenseName { get; set; }

    [Required]
    public decimal Cost { get; set; }

    [StringLength(800)]
    public string Notes { get; set; }

    public DateTime? DueDate { get; set; }

    public Budget Budget { get; set; }
}

public class Income
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    public decimal Amount { get; set; } = 0;

    [Required]
    [StringLength(200)]
    public string Source { get; set; }

    [Required]
    public DateTime? PayDate { get; set; } = DateTime.Now;

    public Budget Budget { get; set; }
}

这是存储库查询。

    public async Task<Budget> GetBudget(int id)
    {
        try
        {
            return await context.Budget
            .Include(e => e.Expenses)
            .Include(i => i.Incomes)
            .SingleAsync(b => b.Id == id);
        }
        catch(Exception x)
        {
            x.ToString();
        }

        return null;
    }

我希望能够通过从费用到预算的关系重新导航以获取 Budget.Income 集合。

预期结果:

foreach(Expense expense in Budget.Expenses)
{
    if (expense.Budget is not null)
    {
        ICollection<Income> paychecks = expense.Budget.Incomes; // Why is Budget always null?
    }
}

我希望即使我不使用 ThenInclude(e => e.Budget),我仍然应该能够从子导航回到父 {var budget = fee.Budget}。我很惊讶这不起作用。

我没有在此处包含收入实体,但我的目标是遍历费用.Budget.Incomes 以获取我只能访问费用实例的代码中的收入集合。

删除 ThenInclude(e => e.Budget) 后我不再收到错误,但费用.Budget 属性仍然为空。

更新

我相信我找到了问题的根本原因。当我将属性 Budget 添加到 Expense 类时,反序列化来自 API 的对象时开始出现错误。由于循环引用,HttpClient 抛出错误。

因为我正在获取根预算实体及其相关费用,并且费用有对预算的引用,所以我得到了周期性引用错误。

我将此代码添加到启动 Blazor 服务器启动类以修复它。我想这是我的问题。

services.AddControllersWithViews().AddJsonOptions(x =>
    x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);

如果我更改为 ReferenceHandler.Preserve 我会得到一个不同的错误。

'JSON 值无法转换为 System.Collections.Generic.ICollection`1[BlazorApp.Data.Models.Expense]。路径:$.expenses |行号:0 | BytePositionInLine:34。'

我不知道但想解决的是如何进行这项工作,以便我可以获得预算 -> 费用并让 Expense.Budget 属性指向它的父预算实例。我真正的问题可能与 json 序列化和反序列化更相关。

【问题讨论】:

  • 实体模型有问题。 Income.Expenses集合的目的是什么以及它是如何映射的?
  • 嗯,这就是你的想法。但是我(根据我的 EF Core 经验,正如您从我的个人资料中看到的那样)发现该确切属性存在问题,因为它会破坏关系,并且可能是您遇到问题的原因。
  • @IvanStoev 是的,我同意你的看法。
  • 至少它清楚地表明您应该始终显示重现问题的代码。我想知道你问题的第一个版本是否有。该版本看起来不错,但仍然触发了一个带有错误的长镜头答案,然后是一长串编辑(问题和答案)导致无处可去,但所有这些都表明您仍在开发您的代码。我建议您返回一个您已测试的最小示例来重现该问题。
  • 重点是 - 你要问的是证明可以在 EF Core 中工作,所以显然你的真实模型/配置有问题。我敢打赌,有些东西你还没有展示出来。最初的错误消息(您已删除)非常清楚 - “表达式 'e.Budget' 在 'Include' 操作中无效,因为它不代表属性访问:” 这可能发生例如,如果你用[NotMapped] 装饰Budget 属性。许多人这样做,然后忘记提及那个“小”细节。那么,你有类似的东西吗?还是流畅的配置代码?

标签: entity-framework entity-framework-core


【解决方案1】:

感谢大家的帮助。您的意见帮助我知道出了什么问题。

这解决了我的问题。

WebAPI : JSON ReferenceHandler.Preserve

我最初的问题与实际答案无关。我没有意识到真正发生了什么。真正的问题是序列化问题。我正在使用托管的 Blazor 应用并拥有客户端和服务器项目。

这个问题涉及多个层面。我将 Budget 属性添加到 Expense 类,以便我可以遍历预算。添加预算属性后,我开始收到有关周期性引用的错误。在 HttpClient 调用 Server 控制器期间,此错误出现在 Client 项目中。我通过将以下代码添加到 Server Startup 类来修复此错误。

services.AddControllersWithViews().AddJsonOptions(options => {
    options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
    options.JsonSerializerOptions.PropertyNamingPolicy = null; // prevent camel case
});

几天后,当我开始做更多工作时,我尝试引用费用.Budget 属性。发现它始终为空,并开始尝试在错误位置的实体中修复它。

事实证明,在调用控制器的过程中,我还需要将 JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve 传递给 HttpClient 代码。

public async Task<Budget> GetBudget(int id)
{
    Budget budget = null;
    try
    {
        budget = await httpClient.GetFromJsonAsync<Budget>("api/Budget/" + id, new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve });
    }
    catch(Exception x)
    {
        x.ToString();
    }
    return budget;
}

这个 foreach 语句现在可以按预期工作了。

foreach(Expense expense in Budget.Expenses)
{
    if (expense.Budget is not null)
    {
        ICollection<Income> paychecks = expense.Budget.Incomes; // How do I populate budget?
    }
}

【讨论】:

    【解决方案2】:

    您的查询中有错误。收入不取决于支出,只取决于预算。费用也是如此。

    所以你的代码可以翻译成这个

     var budget= await context.Incomes
                .Include(e => e.Expenses)
                .Include(i => i.Incomes)
                .SingleAsync(b => b.Id == id);
    
    foreach (Expense expense in budget.Expenses)
    {
          var paychecks = budget.Incomes; 
    }
    

    在这种情况下,这个 foreach 没有任何意义,因为它只是重复 “var paychecks = 预算。收入;”很多次。它甚至没有保存在任何地方。

    同样可以一行完成

    var paychecks= await context.Incomes
                   .Where(b => b.BudgetId == id);
                   .ToListAsync();
    

    或者如果预算已经下载

    var paychecks = budget.Incomes;
    

    您不能使用 .ThenInclude(e => e.Budget) 因为您已经在查询 context.Budget 并且您可以看到 Budget 类没有任何额外的 Budget 属性。

    并且费用是清单。列表也没有任何预算属性,只有列表中的项目有。如果您尝试 .Include(e => e.Expenses.Budget) 它将给出语法错误。但是由于 Expense 或 Income 类具有 Budget 属性,因此该查询将是有效的

    return await context.Incomes
                        .Include(i => i.Budget)
              ....
    

    我认为您只需要这个将预算、收入和支出合并在一起的查询

    var budget= await context.Budgets
            .Include(e => e.Expenses)
             .Include(i => i.Incomes)
            .FirstOrDefaultAsync(b => b.Id == id);
    

    如果您出于某些原因需要,也可以从 Income 中删除公共 ICollection&lt;Expense&gt; Expenses { get; set; } 或改为 [NotMapped]

    如果您使用 EF Core 5+,EF Core 会自动创建外键作为影子属性。但我个人更喜欢明确地拥有它们。 我建议将它们添加到您的课程中(或者您仍然可以保留当前版本)

    public class Budget
    {
        [Key]
        public int Id { get; set; }
         .............
       
        [InverseProperty("Budget")]
        public  virtual ICollection<Expense> Expenses { get; set; }
    
        [InverseProperty("Budget")]
        public  virtual ICollection<Income> Incomes { get; set; }
    }
    
    public class Expense
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
    
        ...........
    
        public int BudgetId { get; set; }
    
        [ForeignKey(nameof(BudgetId ))]
        [InverseProperty("Expenses")]
        public virtual Budget Budget { get; set; }
    }
    public class Income
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
    
       ............
    
        public int BudgetId { get; }
    
        [ForeignKey(nameof(BudgetId ))]
        [InverseProperty("Expenses")]
        public virtual Budget Budget { get; set; }
    
    }
    

    【讨论】:

    • @Aaron 我为您提供了包含所有内容且有效的查询。如果你想要费用。预算你需要这个上下文。费用。包括 (i=>i.Budget)。其中(i=i.BudgetId==id).Tolist()。就是这样。
    • 谢尔盖,谢谢。我想要的是查询预算并获得相关的费用和收入。然后我希望能够从费用导航回到预算。是否不能查询根实体并包含其子实体,然后从子实体导航回父实体?
    • 是的,有些时候反向查询非常有用。
    • @Aaron 请不要忘记接受答案。你只需要一些时间来消化这些新信息。迟早你会意识到所有这些都是有道理的。
    • 好吧,最后 OP 说“现在是完全不同的东西”。正如已经怀疑的那样,与问题中显示的代码无关。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多