Entity Framework 父子表仅返回直接子级

3
我有一个标准的父/子EF模型,如下所示。
public class DataDictionary 
{
    public int Id { get; set; }
    public String Name { get; set; }

    public int? ParentId { get; set; }

    [JsonIgnore]
    public virtual DataDictionary Parent { get; set; }

    public virtual ICollection<DataDictionary> Children { get; set; }
}

我正在通过WebApi将此作为REST API公开,并且当前在获取节点时,它将返回完整的父子层次结构。
{
"Id": 1,
"Name": "root",
"SegmentKey": null,
"ParentId": null,
"Children": [{
    "Id": 2,
    "Name": "Demographics",
    "SegmentKey": null,
    "ParentId": 1,
    "Children": [{
        "Id": 3,
        "Name": "Gender",
        "ParentId": 2,
        "Children": []
    }, {
        "Id": 4,
        "Name": "Age",
        "ParentId": 2,
        "Children": []
    }, {
        "Id": 5,
        "Name": "Income",
        "ParentId": 2,
        "Children": []
    }]
}, {
    "Id": 6,
    "Name": "Activity",
    "SegmentKey": null,
    "ParentId": 1,
    "Children": [{
        "Id": 7,
        "Name": "Navigation",
        "SegmentKey": null,
        "ParentId": 6,
        "Children": []
    }, {
        "Id": 8,
        "Name": "Behaviour",
        "SegmentKey": null,
        "ParentId": 6,
        "Children": []
    }]
}]
}

然而我需要获取请求的对象和仅返回其直接子项,以便我的消费者可以在用户浏览数据时构建可视化表示。

更新:感谢大家的评论,看起来很好,去掉虚拟的部分后,我遇到了困难。由于我在异步方法中,所以查找操作返回了对象并且我丢失了上下文。

    [ResponseType(typeof(DataDictionary))]
    public async Task<IHttpActionResult> GetDataDictionary(int id)
    {
        DataDictionary dataDictionary = await db.DataDictionaries.FindAsync(id);
        if (dataDictionary == null)
        {
            return NotFound();
        }
        return Ok(dataDictionary);
    }

非常感谢您的帮助


你可以禁用延迟加载并对你的子级进行急切加载,但只限于1级。你能承担得起吗? - Red
是的,我可以 - 我需要做什么才能触发仅一级的急切加载? - Mark Ruse
我认为像这样加载它:dbContext.DataDictionaries.Include(x=>x.Children); 应该足够了,Include关键字是使其被急切加载的原因。我认为现在启用了延迟加载时,序列化器会遍历每个属性,导致对您的懒惰子集合进行枚举,从数据库中加载它们并导致同样的过程重复发生于每个子项,从而产生一个老生常谈的N+1选择问题。 - Red
谢谢您的回复 - 像Sergio提到的那样去掉virtual看起来是个好主意,但是我无法弄清楚如何让Include语句填充子项,因为我在一个异步方法中找到了父项,但是我失去了上下文。请参见更新的问题。 - Mark Ruse
能否提供该方法的代码示例? - Red
2个回答

3
你遇到这个问题是因为在实体中使用了"virtual"关键字。这个关键字启用了集合的延迟加载,所以当序列化程序开始序列化子集合时,它会尝试枚举此集合,导致从数据库中加载。然后,对于每个元素递归序列化,导致每个子集合再次从数据库加载(出现 N+1选择问题)。
要实现你想要的效果,你需要:
首先,从你的Children属性中删除virtual关键字:
public class DataDictionary 
{
    public int Id { get; set; }
    public String Name { get; set; }

    public int? ParentId { get; set; }

    [JsonIgnore]
    public virtual DataDictionary Parent { get; set; }

    public ICollection<DataDictionary> Children { get; set; }
}

其次,您需要在控制器中急切地加载此集合。此代码将导致仅为数据字典类实例加载1个级别:

[ResponseType(typeof(DataDictionary))]
public async Task<IHttpActionResult> GetDataDictionary(int id)
{
    DataDictionary dataDictionary = await db.DataDictionaries
        .Include(x=>x.Children)
        .FirstOrDefaultAsync(x=>x.Id == id);
    if (dataDictionary == null)
    {
        return NotFound();
    }
    return Ok(dataDictionary);
 }

不要忘记在文件开头添加using System.Data.Entity,以便使用.Include()函数。
此外,考虑不要在api中使用Entity Framework实体 - 最好创建DTO,这将使您对API的DB结构依赖性更少 - API仅具有EF实体的子集字段。您还可以在此限制树深度,创建一个没有Children集合的子类。
希望这能帮到您!

谢谢Raderick - 正是我所需要的。同意你关于紧耦合和松耦合的观点 - 我当时有些懒惰 :) - Mark Ruse
很高兴能帮到你,@MarkRuse :) - Red
只是为了完整性,当我尝试上述代码时,由于使用IQueryable而不是DbSet(具有findAsync方法),我遇到了包含链接问题。以下解决了该问题 DataDictionary dataDictionary = await db.DataDictionaries.Include(x => x.Children).SingleOrDefaultAsync(i => i.Id == id); - Mark Ruse
@MarkRuse 抱歉我没看到这个问题,也没有在 IDE 中检查,你是完全正确的,我会相应地修复答案! - Red
@MarkRuse 还要记住,SingleOrDefault 可能比 FirstOrDefault 效率低,因为它会生成 SELECT TOP 2 查询,而不是像 FirstOrDefault 那样只选择一个出现。 - Red
谢谢Raderick - 我觉得很奇怪EF TOP 2适用于SingleOrDefault,但我检查了一下,你是对的。已更改为FirstOrDefault。 - Mark Ruse

0
如果你不想检索整个Children列表,可以去掉virtual关键字。
并且独立地编写一个函数来加载第一个Child。

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