实体框架中的“包含”链接

本文关键字:包含 链接 框架 实体 | 更新日期: 2023-09-27 18:35:31

不确定这是否是标题中的正确术语,但对下面描述的某种行为有疑问。

鉴于:

public class FooBar
{
    // etc
    public virtual ICollection<Foo> Foos { get; set; }
    public virtual ICollection<Bar> Bars { get; set; }
}
public class Foo
{
}
public class Bar
{
}
public class FooBarRepo
{
    private readonly EntitiesContext _context;
    public FooBarRepo()
    {
        this._context = new EntitiesContext();
    }
    public IQueryable<FooBar> GetIncludeFoo()
    {
        return this
            ._context
            .FooBars
            .Include(i => i.Foos);
    }
    public IQueryable<FooBar> GetIncludeBar()
    {
        return this
            ._context
            .FooBars
            .Include(i => i.Bars);
    }
}

我没有测试台来确认这种行为,所以想确保我正确解释/记住 - 但如果我要抛出一个额外的函数,定义如下:

public IQueryable<FooBar> GetIncludeBarChainedWithGetIncludeFoo()
{
    return this
        .GetIncludeFoo()
        .Include(i => i.Bars);
}

我似乎记得在打电话给GetIncludeBarChainedWithGetIncludeFoo()时,我只是得到了我的Bars,而不是我期望从电话中得到的额外Foos GetIncludeFoo()成为电话的一部分。

可以确认/解释这种行为吗?

实体框架中的“包含”链接

免责声明:自从我使用它以来,EF 可能已经发生了变化。我正在 EF 4.x 级别编写。更高版本可能会为向上传播包含查询添加一些更好的支持,我实际上从未检查过。但我对此有些怀疑。所以,请不要把这一切都当作绝对的真理,一定要自己尝试。例如,我不记得 GroupBy 是否真的破坏了包含,但我确信 Select 这会导致投影到匿名类型 - 确实如此。


不,在您的示例中,它会起作用。

在您的示例中,当调用 GetIncludeBarChainedWithGetIncludeFoo 时,您的代码实际上调用/构建以下序列/查询:

    // from GetIncludeFoo
    return this
        ._context
        .FooBars
        .Include(i => i.Foos)
    // from GetIncludeBarChainedWithGetIncludeFoo
        .Include(i => i.Bars);

这将 100% 按照您的想法运行:它将返回预加载 Foos 和 Bars 的 FooBars。简单的链接是绝对可以的,就像你也可以做的那样:

    // from GetIncludeFoo
    return this
        ._context
        .FooBars
        .Include("Foos")
        .Include("Bars");

但是,当您像在示例中所做的那样将这两个包含拆分为多个方法时,就会打开一条通往微妙陷阱的方法。让我们看看:

    return this
        .GetIncludeFoo()       // let's say they all are extensions just for clarity
        .DoSomeMyOtherThings()
        .GetIncludeBar()

如果"DoSomeMyOtherThings"做出任何会导致查询改变其形状并松散其与特定表的紧密绑定(如投影)的操作,那么即使看起来没问题,IncludeBar 也会失败。所有包含(定义要拉取的新源)都应在任何其他操作之前。

    return this
        .GetIncludeFoo()       // let's say they all are extensions just for clarity
        .GetIncludeBar()
        .DoSomeMyOtherThings()

这可能看起来很明显,但是当你开始将 include/wheres/selects/groupbys 包装成漂亮的方法,然后当你开始将方法混合在一起时,很容易忘记这一点:

    return this
        .GetFooThatAre("Green")
        .GetBestBarForEachFoo()
        .GetBuzzFromBar()

第一种方法将能够包含"Foos"。第二种方法可能会成功包含"柱线"。但是,最后一个可能无法包含"Buzz",因为第二种方法中隐藏了分组依据和/或投影。

即使使用漂亮的包装器,查询仍然必须是:

    return this
        .GetIncludeFoo()
        .GetIncludeBar()
        .GetIncludeBuzz()
        .GetFooThatAre("Green")
        .GetBestBarForEachFoo()
        .GetBuzzFromBar()

或类似的东西。

我一定有一个稍微不同的场景,那里的某个地方有一个 ToList(),或者类似的东西,因为我无法重现我有一个问题的场景:

public class FooBarRepo : IFooBarRepo
{
    private readonly Entities _context;
    public FooBarRepo()
    {
        this._context = new Entities();
    }
    public IQueryable<FooBar> Get()
    {
        return this._context.FooBar;
    }
    public IQueryable<FooBar> GetIncludeFoo()
    {
        return this.Get().Include(i => i.Foo);
    }
    public IQueryable<FooBar> GetIncludeBar()
    {
        return this.Get().Include(i => i.Bar);
    }
    public IQueryable<FooBar> GetIncludeFooAndBar()
    {
        return this
            .Get()
            .Include(i => i.Foo)
            .Include(i => i.Bar);
    }
    public IQueryable<FooBar> GetIncludeFooAndChainBar()
    {
        return this.GetIncludeBar().Include(i => i.Foo);
    }
    public void Dispose()
    {
        this._context.Dispose();
    }
}
class Program
{
    static void Main(string[] args)
    {
        IEnumerable<FooBar> get;
        IEnumerable<FooBar> getIncludeFoo;
        IEnumerable<FooBar> getIncludeBar;
        IEnumerable<FooBar> getIncludeFooAndBar;
        IEnumerable<FooBar> getIncludeFooAndChainBar;
        using (var context = new EntityFrameworkTesting.TestIncludeChaining.Repository.FooBarRepo())
        {
            get = context.Get().ToList();
            getIncludeFoo = context.GetIncludeFoo().ToList();
            getIncludeBar = context.GetIncludeBar().ToList();
            getIncludeFooAndBar = context.GetIncludeFooAndBar().ToList();
            getIncludeFooAndChainBar = context.GetIncludeFooAndChainBar().ToList();
        }
    }
}

生成的 SQL:

-- get
SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name]
FROM [dbo].[_FooBar] AS [Extent1]
-- getIncludeFoo
SELECT 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[C1] AS [C1], 
[Project1].[Id1] AS [Id1], 
[Project1].[FooBarId] AS [FooBarId]
FROM ( SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[FooBarId] AS [FooBarId], 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM  [dbo].[_FooBar] AS [Extent1]
    LEFT OUTER JOIN [dbo].[_Foo] AS [Extent2] ON [Extent1].[Id] = [Extent2].[FooBarId]
)  AS [Project1]
ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC
-- getIncludeBar
SELECT 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[C1] AS [C1], 
[Project1].[Id1] AS [Id1], 
[Project1].[FooBarId] AS [FooBarId]
FROM ( SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[FooBarId] AS [FooBarId], 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM  [dbo].[_FooBar] AS [Extent1]
    LEFT OUTER JOIN [dbo].[_Bar] AS [Extent2] ON [Extent1].[Id] = [Extent2].[FooBarId]
)  AS [Project1]
ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC
-- getIncludeFooAndBar
SELECT 
[UnionAll1].[Id] AS [C1], 
[UnionAll1].[Id1] AS [C2], 
[UnionAll1].[Name] AS [C3], 
[UnionAll1].[C1] AS [C4], 
[UnionAll1].[Id2] AS [C5], 
[UnionAll1].[FooBarId] AS [C6], 
[UnionAll1].[C2] AS [C7], 
[UnionAll1].[C3] AS [C8]
FROM  (SELECT 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1], 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Id] AS [Id1], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id2], 
    [Extent2].[FooBarId] AS [FooBarId], 
    CAST(NULL AS int) AS [C2], 
    CAST(NULL AS int) AS [C3]
    FROM  [dbo].[_FooBar] AS [Extent1]
    LEFT OUTER JOIN [dbo].[_Foo] AS [Extent2] ON [Extent1].[Id] = [Extent2].[FooBarId]
UNION ALL
    SELECT 
    2 AS [C1], 
    [Extent3].[Id] AS [Id], 
    [Extent3].[Id] AS [Id1], 
    [Extent3].[Name] AS [Name], 
    CAST(NULL AS int) AS [C2], 
    CAST(NULL AS int) AS [C3], 
    [Extent4].[Id] AS [Id2], 
    [Extent4].[FooBarId] AS [FooBarId]
    FROM  [dbo].[_FooBar] AS [Extent3]
    INNER JOIN [dbo].[_Bar] AS [Extent4] ON [Extent3].[Id] = [Extent4].[FooBarId]) AS [UnionAll1]
ORDER BY [UnionAll1].[Id1] ASC, [UnionAll1].[C1] ASC
-- getIncludeFooAndChainBar
SELECT 
[UnionAll1].[Id] AS [C1], 
[UnionAll1].[Id1] AS [C2], 
[UnionAll1].[Name] AS [C3], 
[UnionAll1].[C1] AS [C4], 
[UnionAll1].[Id2] AS [C5], 
[UnionAll1].[FooBarId] AS [C6], 
[UnionAll1].[C2] AS [C7], 
[UnionAll1].[C3] AS [C8]
FROM  (SELECT 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1], 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Id] AS [Id1], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id2], 
    [Extent2].[FooBarId] AS [FooBarId], 
    CAST(NULL AS int) AS [C2], 
    CAST(NULL AS int) AS [C3]
    FROM  [dbo].[_FooBar] AS [Extent1]
    LEFT OUTER JOIN [dbo].[_Bar] AS [Extent2] ON [Extent1].[Id] = [Extent2].[FooBarId]
UNION ALL
    SELECT 
    2 AS [C1], 
    [Extent3].[Id] AS [Id], 
    [Extent3].[Id] AS [Id1], 
    [Extent3].[Name] AS [Name], 
    CAST(NULL AS int) AS [C2], 
    CAST(NULL AS int) AS [C3], 
    [Extent4].[Id] AS [Id2], 
    [Extent4].[FooBarId] AS [FooBarId]
    FROM  [dbo].[_FooBar] AS [Extent3]
    INNER JOIN [dbo].[_Foo] AS [Extent4] ON [Extent3].[Id] = [Extent4].[FooBarId]) AS [UnionAll1]
ORDER BY [UnionAll1].[Id1] ASC, [UnionAll1].[C1] ASC

如果我再次遇到我无法找到的实际场景,我将更新/提出一个新问题。 但在此期间,感谢您的健全性检查。