当使用连接和where子句时Linq是否会被破坏?

时间:2014-10-01 22:10:03

标签: c# linq linq-to-sql linq-to-entities

我一直在尝试让以下Linq工作,没有快乐。我确信它是对的,但那可能只是我的坏Linq。我最初在这里添加了这个作为类似问题的答案:

Linq-to-entities - Include() method not loading

但由于这是一个非常古老的问题,而且我的问题更具体,我认为它会作为一个明确的问题做得更好。

在链接的问题中,Alex James提供了两个有趣的解决方案,但是如果你尝试并检查SQL,那就太可怕了。

我正在研究的例子是:

        var theRelease = from release in context.Releases
                         where release.Name == "Hello World"
                         select release;

        var allProductionVersions = from prodVer in context.ProductionVersions
                                    where prodVer.Status == 1
                                    select prodVer;

        var combined = (from release in theRelease
                        join p in allProductionVersions on release.Id equals p.ReleaseID
                        select release).Include(release => release.ProductionVersions);              

        var allProductionsForChosenRelease = combined.ToList();

这遵循两个例子中较简单的一个。没有包含它会产生完全可敬的sql:

SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name]
    FROM  [dbo].[Releases] AS [Extent1]
    INNER JOIN [dbo].[ProductionVersions] AS [Extent2] ON [Extent1].[Id] = [Extent2].[ReleaseID]
    WHERE ('Hello World' = [Extent1].[Name]) AND (1 = [Extent2].[Status])

但是,OMG:

SELECT 
[Project1].[Id1] AS [Id], 
[Project1].[Id] AS [Id1], 
[Project1].[Name] AS [Name], 
[Project1].[C1] AS [C1], 
[Project1].[Id2] AS [Id2], 
[Project1].[Status] AS [Status], 
[Project1].[ReleaseID] AS [ReleaseID]
FROM ( SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id1], 
    [Extent3].[Id] AS [Id2], 
    [Extent3].[Status] AS [Status], 
    [Extent3].[ReleaseID] AS [ReleaseID],
    CASE WHEN ([Extent3].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM   [dbo].[Releases] AS [Extent1]
    INNER JOIN [dbo].[ProductionVersions] AS [Extent2] ON [Extent1].[Id] = [Extent2].[ReleaseID]
    LEFT OUTER JOIN [dbo].[ProductionVersions] AS [Extent3] ON [Extent1].[Id] = [Extent3].[ReleaseID]
    WHERE ('Hello World' = [Extent1].[Name]) AND (1 = [Extent2].[Status])
)  AS [Project1]
ORDER BY [Project1].[Id1] ASC, [Project1].[Id] ASC, [Project1].[C1] ASC

垃圾总量。这里要注意的关键点是它返回表的外连接版本,该版本不受status = 1的限制。

这会导致返回错误的数据:

Id  Id1 Name        C1  Id2 Status  ReleaseID
2   1   Hello World 1   1   2       1
2   1   Hello World 1   2   1       1

请注意,尽管我们有限制,但仍会在那里返回2的状态。它根本不起作用。 如果我在某个地方出错了,我会很高兴发现,因为这是对Linq的嘲弄。我喜欢这个想法,但目前执行似乎并不可用。

出于好奇,我尝试了LinqToSQL dbml,而不是产生上述混乱的LinqToEntities edmx:

SELECT [t0].[Id], [t0].[Name], [t2].[Id] AS [Id2], [t2].[Status], [t2].[ReleaseID], (
    SELECT COUNT(*)
    FROM [dbo].[ProductionVersions] AS [t3]
    WHERE [t3].[ReleaseID] = [t0].[Id]
    ) AS [value]
FROM [dbo].[Releases] AS [t0]
INNER JOIN [dbo].[ProductionVersions] AS [t1] ON [t0].[Id] = [t1].[ReleaseID]
LEFT OUTER JOIN [dbo].[ProductionVersions] AS [t2] ON [t2].[ReleaseID] = [t0].[Id]
WHERE ([t0].[Name] = @p0) AND ([t1].[Status] = @p1)
ORDER BY [t0].[Id], [t1].[Id], [t2].[Id]

略微更紧凑 - 怪异的计数条款,但整体相同的总失败。

请告诉我,我错过了一些明显的东西,因为我真的很想要Linq!

2 个答案:

答案 0 :(得分:0)

好的,经过另一个搔痒的夜晚,我破了它。

在LinqToSQL中:

        using (var context = new TestSQLModelDataContext())
        {
            context.DeferredLoadingEnabled = false;
            DataLoadOptions ds = new DataLoadOptions();                
            ds.LoadWith<ProductionVersion>(prod => prod.Release);
            context.LoadOptions = ds;

            var combined = from release in context.Releases
                             where release.Name == "Hello World"
                             select from prodVer in release.ProductionVersions
                                    where prodVer.Status == 1
                                    select prodVer;

            var allProductionsForChosenRelease = combined.ToList();
        }

这会产生更合理的SQL:

SELECT [t2].[Id], [t2].[Status], [t2].[ReleaseID], [t0].[Id] AS [Id2], [t0].[Name], (
    SELECT COUNT(*)
    FROM [dbo].[ProductionVersions] AS [t3]
    WHERE ([t3].[Status] = 1) AND ([t3].[ReleaseID] = [t0].[Id])
    ) AS [value]
FROM [dbo].[Releases] AS [t0]
OUTER APPLY (
    SELECT [t1].[Id], [t1].[Status], [t1].[ReleaseID]
    FROM [dbo].[ProductionVersions] AS [t1]
    WHERE ([t1].[Status] =1) AND ([t1].[ReleaseID] = [t0].[Id])
    ) AS [t2]
WHERE [t0].[Name] = 'Hello World'
ORDER BY [t0].[Id], [t2].[Id]

产生正确的结果:

Id  Status  ReleaseID   Id2 Name        value
2   1       1           1   Hello World 1

在LinqToEntities中(我无法使Include语法起作用,所以我使用了结果中包含所需表格的怪癖):

        using (var context = new TestEntities1())
        {
            var combined = (from release in context.Releases
                            where release.Name == "Hello World"
                            select from prodVer in release.ProductionVersions
                                   where prodVer.Status == 1
                                   select new { prodVer, Release =prodVer.Release });

            var allProductionsForChosenRelease = combined.ToList();
        }

这会产生SQL:

SELECT 
    [Project1].[Id] AS [Id], 
    [Project1].[C1] AS [C1], 
    [Project1].[Id1] AS [Id1], 
    [Project1].[Status] AS [Status], 
    [Project1].[ReleaseID] AS [ReleaseID], 
    [Project1].[Id2] AS [Id2], 
    [Project1].[Name] AS [Name]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Join1].[Id1] AS [Id1], 
        [Join1].[Status] AS [Status], 
        [Join1].[ReleaseID] AS [ReleaseID], 
        [Join1].[Id2] AS [Id2], 
        [Join1].[Name] AS [Name], 
        CASE WHEN ([Join1].[Id1] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM  [dbo].[Releases] AS [Extent1]
        LEFT OUTER JOIN  (SELECT [Extent2].[Id] AS [Id1], [Extent2].[Status] AS [Status], [Extent2].[ReleaseID] AS [ReleaseID], [Extent3].[Id] AS [Id2], [Extent3].[Name] AS [Name]
            FROM  [dbo].[ProductionVersions] AS [Extent2]
            INNER JOIN [dbo].[Releases] AS [Extent3] ON [Extent2].[ReleaseID] = [Extent3].[Id] ) AS [Join1] ON ([Extent1].[Id] = [Join1].[ReleaseID]) AND (1 = [Join1].[Status])
        WHERE 'Hello World' = [Extent1].[Name]
    )  AS [Project1]
    ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC

这是相当精神上的,但确实有效。

Id  C1  Id1 Status  ReleaseID   Id2 Name
1   1   2   1       1           1   Hello World

所有这些都让我得出Linq远未完成的结论。它可以使用,但要非常谨慎。使用它作为强类型和编译时检查,但是很费力/容易出错,编写错误的SQL的方法。这是一种权衡。你在C#端获得了更多的安全性,但是比编写SQL要困难得多!

答案 1 :(得分:0)

再看看,我现在明白了Include的难以捉摸的效果。

正如在纯SQL中一样,当连接的右侧是&#34; n&#34;时,LINQ中的连接将重复结果。结束1-n关联。

假设您有一个Release,其中有两个ProductionVersion。如果没有Include,则连接将为您提供两个相同的Release,因为在所有语句select发布之后。现在,当您添加Include时,EF不仅会返回两个版本,还会完全填充其ProductionVersions个集合。

更深入一点,在上下文缓存中,看起来EF实际上只实现了1 Release和2 ProductionVersion s。它只是在最终结果集中返回两次。

在某种程度上,你得到了你要求的东西:给我发布,乘以他们的版本数量。但那不是你想要的要求的。

你(可能)想要揭示EF工具箱中的弱点:我们不能Include部分收藏。我认为您尝试仅使用ProductionVersions = 1的Status填充版本。如果可能的话,你宁愿这样做:

context.Releases.Include(r => r.ProductionVersions.Where(v => v.Status == 1))
       .Where(r => r.Name == "Hello World")

但这引发了一个例外:

  

Include路径表达式必须引用在类型上定义的导航属性。使用虚线路径作为参考导航属性,使用Select运算符作为集合导航属性。   参数名称:路径

此&#34;过滤包括&#34;在EF团队(或贡献者)决定抓住this issue之前,我们已经注意到问题,我们必须采用精心设计的解决方法。我描述了一个常见的here