【问题标题】:EF Core 6 not loading child entitiesEF Core 6 不加载子实体
【发布时间】:2023-02-04 11:27:39
【问题描述】:

我正在开发一个简单的 Web API,它只是用来解析 JSON 树,并将其保存到数据库中。我正在使用 EF Core 6.0.4,我的应用程序显示了一个非常奇怪的行为:在保存树之后,它从上下文中加载得很好。但是当我调用不同的端点并使用新初始化的上下文加载数据时,子项将不会加载。我相信这是由于 EF 配置,但我不知道如何加载孩子。

我尝试使用 _context.Entity(returnValue).Collection(x => x.Children) 但 lambda 中的 x 只有 ICollection 扩展方法,而不是我看到的代码示例中的模型字段。 我也尝试使用.Include,但这似乎是来自常规 EF 的东西。

这是我的一些代码:

控制器

public class CategoryTreeManagerController : ControllerBase
{
    private readonly CategoryTreeManagerService _service;
    public CategoryTreeManagerController(CategoryTreeManagerService service)
    {
        _service = service;
    }

    [HttpGet]
    public IEnumerable<CategoryTreeNode> GetTree()
    {
        return _service.GetTree(); //this only returns the root node without any children
    }

    [HttpPost]
    public IEnumerable<CategoryTreeNode> SaveTree(CategoryTreeNode[] nodes)
    {
        _service.SaveTree(nodes[0]);

        return _service.GetTree(); //this correctly returns the tree
    }
}

服务

public class CategoryTreeManagerService
{
    private readonly CategoryTreeManagerApiDbContext _context;
    public CategoryTreeManagerService(CategoryTreeManagerApiDbContext context)
    {
        _context = context;
    }

    public IEnumerable<CategoryTreeNode> GetTree()
    {
        CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
            .Where(x => x.Parent == null) //just return the root node
            .ToArray(); //easier for frontend

        return returnValue;
    }

    public void SaveTree(CategoryTreeNode node)
    {
        if (_context.CategoryTreeNodes.Any(x => x.Id == node.Id))
        {
            _context.Update(node);
        }
        else
        {
            _context.CategoryTreeNodes.Add(node);
        }

        _context.SaveChanges();
    }
}

数据库上下文

public class CategoryTreeManagerApiDbContext : DbContext
{
    public CategoryTreeManagerApiDbContext() : base ()
    {
        Database.EnsureCreated();
    }
    
    public CategoryTreeManagerApiDbContext(DbContextOptions<CategoryTreeManagerApiDbContext> options) : base(options)
    {
        Database.EnsureCreated();
    }

    public DbSet<CategoryTreeNode> CategoryTreeNodes { get; set; }
    public DbSet<TreeNodeDetail> TreeNodeDetails { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<CategoryTreeNode>(entity =>
        {
            entity.HasKey(x => x.Id);
            entity.HasOne(x => x.Parent)
                .WithMany(x => x.Children)
                .IsRequired(false)
                .OnDelete(DeleteBehavior.Restrict);
        });
    }

模型类

public class CategoryTreeNode
{
    public int Id { get; set; }
    public string Label { get; set; }
    public string Story { get; set; }
    public string Icon { get; set; }
    public ICollection<TreeNodeDetail> Details { get; set; }
    public ICollection<CategoryTreeNode> Children { get; set; }

    [JsonIgnore]
    public CategoryTreeNode? Parent { get; set; }
}

public class TreeNodeDetail
{
    [JsonPropertyName("detailId")]
    public string Id { get; set; }
    [JsonPropertyName("detailTitle")]
    public string Title { get; set; }
    [JsonPropertyName("detailValue")]
    public string Value { get; set; }

    [JsonIgnore]
    [ForeignKey("CategoryTreeNode")]
    public int CategoryTreeNodeId { get; set; }
}

【问题讨论】:

  • 尝试以下技巧CategoryTreeNode[] returnValue = _context.CategoryTreeNodes.ToList().Where(x =&gt; x.Parent == null).ToArray();
  • @SvyatoslavDanyliv 有用,谢谢!知道导致此问题的后台发生了什么吗?
  • 这不是问题。 EF Core 无法通过Include 或其他技术一次加载整个树,但当您加载所有实体时,它可以自动修复/填充导航属性。所以在你的情况下,树被正确填充。当您尝试加载树的叶子时,这将是一个问题,您可能需要创建存储过程。

标签: entity-framework-core configuration


【解决方案1】:

Include 将适用于一个级别,如果您展开表达式,则可以适用于更多级别,但它不是深度可变的树结构的理想解决方案。 (即孩子的孩子的孩子......)

例如:

CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
    .Include(x => x.Children)      
    .Where(x => x.Parent == null) //just return the root node
    .ToArray(); //easier for frontend

将加载所有父母和他们的第一级孩子。加载二级孩子:

CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
    .Include(x => x.Children)      
    .ThenInclude(x => x.Children)
    .Where(x => x.Parent == null) //just return the root node
    .ToArray(); //easier for frontend

问题是知道要加载多少个级别,每个级别都会生成一个笛卡尔积供 EF 处理,从而以指数方式增加加载的数据量以构建树。一次快速加载整个表成为一种更有效的解决方案。

如果你有一个单一的树结构,你希望只有一个顶级条目,或者想要加载一整套合理的顶级节点然后加载全部内存中的条目将起作用,因为 EF 将跟踪所有实体,并且它可以在构建实体结构时解析所有各种引用。这必须加载整个集合,即使您只想要几个可能的父母中的一个特定父母。

如果您有多个顶级父节点和一个相当大的整体表大小需要处理,并且确实希望能够加载单个父节点及其子节点,那么一个选择是向树添加非规范化的顶级 ID 引用节点。

public int Id { get; set; }
public int? TopLevelId { get; set; }

这将是一个可为 null 的 FK,但不需要导航属性。当前的 Parent 引用将继续使用影子属性。 (即 ParentId)通过这种方式,一旦您拥有顶级父级的 ID,您就可以为其加载一棵树:

_context.CategoryTreeNodes.Where(x => x.TopLevelId == topLevelId).ToList();
var topLevelNode = _context.CategoryTreeNodes.Single(x => x.Id == topLevelId);

第一条语句将使 DBContext 加载并跟踪该顶级树节点下的所有节点。然后,当您调用 DbContext 获取该顶级节点时,跟踪的相关实体将全部被填充。

这种方法的警告是它是一种非规范化,因为没有 DB 级断言 TopLevelId 已设置或保持正确设置。例如,如果节点可以在顶级实体之间移动,而您忘记更新此值,则不会使用上述负载在新父节点下加载和关联此节点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-16
    • 2021-04-14
    • 1970-01-01
    • 2023-02-10
    • 2018-04-05
    • 2023-03-29
    • 1970-01-01
    相关资源
    最近更新 更多