当与联接和where子句一起使用时,Linq Include是否已断开
本文关键字:Linq Include 是否 断开 where 一起 子句 | 更新日期: 2023-09-27 18:25:20
我一直在尝试让下面的Linq毫无乐趣地工作。我确信这是对的,但那可能只是我糟糕的林克。我最初添加这个是为了回答这里的一个类似问题:
Linq to entities-Include()方法未加载
但由于这是一个非常古老的问题,而我的问题更具体,我认为作为一个明确的问题会做得更好。
在相关的问题中,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();
下面是两个例子中比较简单的一个。如果没有include,它将生成完全令人尊敬的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])
但是,天哪:
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
垃圾总量。这里需要注意的关键点是,它返回表的外部联接版本,该版本不受状态=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的状态仍将返回到那里。它根本不起作用。如果我哪里出了问题,我会很高兴地发现,因为这是在嘲笑林克。我很喜欢这个主意,但目前执行起来似乎不可行。
出于好奇,我尝试了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]
稍微紧凑一点-奇怪的计数子句,但总体上相同的总失败。
请告诉我,我错过了一些显而易见的东西,因为我真的很想喜欢林克!
好吧,在又一个晚上的挠头之后,我破解了它。
在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
所有这些让我得出结论,林克还远远没有结束。它可以使用,但要格外小心。使用它作为一种强类型和编译时检查,但费力/容易出错的编写糟糕SQL的方法。这是一种权衡。您可以在C#端获得更多的安全性,但伙计,这比编写SQL要困难得多!
再看一眼,我现在明白了Include
难以捉摸的效果。
就像在普通SQL中一样,当连接的右侧是1-n关联的"n"端时,LINQ中的连接将重复结果。
假设您有一个Release
和两个ProductionVersion
s。如果没有Include
,联接将给您两个相同的Release
s,因为在所有语句select
s发布之后。现在,当您添加Include
时,EF不仅会返回两个版本,还会完全填充它们的ProductionVersions
集合。
再深入一点看,在上下文的缓存中,EF似乎只实现了1个Release
和2个ProductionVersion
s。只是在最终结果集中返回了两次释放。
在某种程度上,你得到了你想要的:给我版本,乘以它们的版本数量。但这不是你想要问的。
你(可能)的意图揭示了EF工具箱中的一个弱点:我们不能Include
部分集合。我认为您试图让版本只填充Status
=1的ProductionVersions
。如果可能的话,你宁愿这样做:
context.Releases.Include(r => r.ProductionVersions.Where(v => v.Status == 1))
.Where(r => r.Name == "Hello World")
但这引发了一个例外:
Include路径表达式必须引用在类型上定义的导航属性。对引用导航属性使用虚线路径,对集合导航属性使用Select操作符。参数名称:路径
这个"过滤包含"问题在EF团队(或贡献者)决定抓住这个问题之前就已经被注意到了,我们必须进行详细的解决方案。我在这里描述了一个常见的。