EntityFramework – 包含组合键的查询

给出一个ID列表,我可以查询所有相关的行:

context.Table.Where(q => listOfIds.Contains(q.Id)); 

但是当表有复合键时,你如何实现相同的function呢?

这是一个讨厌的问题,我不知道任何优雅的解决scheme。

假设你有这些组合键,你只想select标记的(*)。

 Id1 Id2 --- --- 1 2 * 1 3 1 6 2 2 * 2 3 * ... (many more) 

如何做到这一点Entity框架是快乐的方式? 让我们看看一些可能的解决scheme,看看他们是否有什么好处。

解决scheme1: Join (或Contains )对

最好的解决scheme是创build一个你想要的对的列表,例如元组(Tuple)( List<Tuple<int,int>> ),并用这个列表连接数据库数据:

 from entity in db.Table // db is a DbContext join pair in Tuples on new { entity.Id1, entity.Id2 } equals new { Id1 = pair.Item1, Id2 = pair.Item2 } select entity 

在LINQ中,对象是完美的,但是太糟糕了,EF会抛出exception

无法创buildtypes为“System.Tuple”的常量值(…)在此上下文中仅支持基本types或枚举types。

这是一个相当笨拙的方式告诉你,它不能将这个语句翻译成SQL,因为Tuples不是原始值列表(如intstring )。 1 。 出于同样的原因,使用Contains (或任何其他LINQ语句)的类似语句将会失败。

解决scheme2:内存中

当然,我们可以把这个问题变成简单的LINQ to像这样的对象:

 from entity in db.Table.AsEnumerable() // fetch db.Table into memory first join pair Tuples on new { entity.Id1, entity.Id2 } equals new { Id1 = pair.Item1, Id2 = pair.Item2 } select entity 

不用说,这不是一个好的解决scheme。 db.Table可能包含数百万条logging。

解决scheme3:两个Contains语句

所以让我们提供EF两个原始值列表, [1,2]Id1[2,3]Id2 。 我们不想使用join(参见附注),所以让我们使用Contains

 from entity in db.Table where ids1.Contains(entity.Id1) && ids2.Contains(entity.Id2) select entity 

但是现在结果还包含实体{1,3} ! 那当然,这个实体完全符合这两个谓词。 但是让我们记住,我们正在接近。 而不是把数百万个实体拉进内存,我们现在只能得到其中的四个。

解决scheme4:一个Contains计算值

解决scheme3失败,因为两个单独的Contains语句不仅过滤它们的值的组合 。 如果我们先创build一个组合列表并尝试匹配这些组合,该怎么办? 我们从解决scheme1知道这个列表应该包含原始值。 例如:

 var computed = ids1.Zip(ids2, (i1,i2) => i1 * i2); // [2,6] 

和LINQ声明:

 from entity in db.Table where computed.Contains(entity.Id1 * entity.Id2) select entity 

这种方法有一些问题。 首先,你会看到这也返回实体{1,6} 。 组合函数(a * b)不会生成唯一标识数据库中的一对的值。 现在我们可以创build一个类似["Id1=1,Id2=2","Id1=2,Id2=3]"的string列表,

 from entity in db.Table where computed.Contains("Id1=" + entity.Id1 + "," + "Id2=" + entity.Id2) select entity 

(这可以在EF6中工作,而不是在早期版本中)。

这变得相当混乱。 但是一个更重要的问题是这个解决scheme不可靠 ,这意味着:它绕过了Id1Id2上任何可能被使用的数据库索引。 这将performance得非常糟糕。

解决scheme5:最好的2和3

所以,我能想到的唯一可行的解​​决scheme是Contains和内存Contains的组合:首先做解决scheme3中的contains语句。请记住,它使我们非常接近我们想要的。 然后通过将结果作为内存列表加以细化来优化查询结果:

 var rawSelection = from entity in db.Table where ids1.Contains(entity.Id1) && ids2.Contains(entity.Id2) select entity; var refined = from entity in rawSelection.AsEnumerable() join pair in Tuples on new { entity.Id1, entity.Id2 } equals new { Id1 = pair.Item1, Id2 = pair.Item2 } select entity; 

这可不是优雅的,也可能是混乱的,但到目前为止,它是我发现的这个问题的唯一可扩展的2解决scheme,并应用于我自己的代码中。

解决scheme6:使用OR子句构build查询

使用诸如Linqkit之类的谓词构build器或替代方法,可以为组合列表中的每个元素构build一个包含OR子句的查询。 这可能是一个真正的短名单可行的select。 有几百个元素,查询将开始非常糟糕的performance。 所以我不认为这是一个好的解决scheme,除非你能100%确定总会有less量的元素。 这个选项的一个细节可以在这里find。


1有趣的是,EF在join一个原始列表时创build一个SQL语句,就像这样

 from entity in db.Table // db is a DbContext join i in MyIntegers on entity.Id1 equals i select entity 

但是生成的SQL是荒谬的。 一个真正的例子,其中MyIntegers只包含5(!)整数看起来像这样:

 SELECT [Extent1].[CmpId] AS [CmpId], [Extent1].[Name] AS [Name], FROM [dbo].[Company] AS [Extent1] INNER JOIN (SELECT [UnionAll3].[C1] AS [C1] FROM (SELECT [UnionAll2].[C1] AS [C1] FROM (SELECT [UnionAll1].[C1] AS [C1] FROM (SELECT 1 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable1] UNION ALL SELECT 2 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable2]) AS [UnionAll1] UNION ALL SELECT 3 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable3]) AS [UnionAll2] UNION ALL SELECT 4 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable4]) AS [UnionAll3] UNION ALL SELECT 5 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable5]) AS [UnionAll4] ON [Extent1].[CmpId] = [UnionAll4].[C1] 

有n-1个UNION 。 当然,这是不可扩展的。

后来增加:
在EF版本6.1.3的道路上,这已经有了很大的改进。 UNION变得更简单,不再嵌套。 以前,查询会放弃less于50个本地序列中的元素(SQLexception: 您的SQL语句的某些部分嵌套太深 )。非嵌套的UNION允许本地序列多达几千(!)个元素。 虽然“很多”元素仍然很慢。

2 Contains语句是可扩展的: Scalable包含LINQ对SQL后端的方法

在组合键的情况下,你可以使用另一个idlist,并在你的代码中添加一个条件

 context.Table.Where(q => listOfIds.Contains(q.Id) && listOfIds2.Contains(q.Id2)); 

或者你可以使用另一个窍门通过添加它们来创build你的密钥列表

 listofid.add(id+id1+......) context.Table.Where(q => listOfIds.Contains(q.Id+q.id1+.......)); 

您需要一组代表您要查询的键的对象。

 class Key { int Id1 {get;set;} int Id2 {get;set;} 

如果你有两个列表,你只要检查每个值出现在它们各自的列表中,那么你就得到了列表的笛卡尔乘积 – 这可能不是你想要的。 相反,您需要查询所需的特定组合

 List<Key> keys = // get keys; context.Table.Where(q => keys.Any(k => k.Id1 == q.Id1 && k.Id2 == q.Id2)); 

我不完全确定这是有效使用entity framework; 您可能在将Keytypes发送到数据库时遇到问题。 如果发生这种情况,你可以创造性:

 var composites = keys.Select(k => p1 * k.Id1 + p2 * k.Id2).ToList(); context.Table.Where(q => composites.Contains(p1 * q.Id1 + p2 * q.Id2)); 

您可以创build一个同构函数(素数对此很有用),就像一个哈希码,您可以使用它来比较这对值。 只要乘法因子是共素,这个模式将是同构的(一对一) – 即只要质数是p1*Id1 + p2*Id2就会唯一地识别Id1Id2的值正确select。

但是,如果你正在实施复杂的概念,那么你最终会遇到一些需要支持的概念。 编写一个采用有效密钥对象的存储过程可能会更好。

你可以用这两个键创build一个string集合(我假设你的键是inttypes):

 var id1id2Strings = listOfIds.Select(p => p.Id1+ "-" + p.Id2); 

那么你可以在你的db上使用“Contains”

 using (dbEntities context = new dbEntities()) { var rec = await context.Table1.Where(entity => id1id2Strings .Contains(entity.Id1+ "-" + entity.Id2)); return rec.ToList(); } 

在没有一个通用的解决办法的情况下,我认为有两点需要考虑:

  1. 避免多列主键(也会使unit testing更容易)。
  2. 但是,如果必须的话,他们中的一个可能会将查询结果大小减小到O(n),其中n是理想查询结果的大小。 从这里,它的解决scheme5从Gerd Arnold上面。

例如,导致我这个问题的问题是查询订单行,其中关键是订单ID +订单行号+订单types,而且订单types是隐含的。 也就是说,订单types是一个常量,订单ID会减less查询设置为相关订单的订单行数量,每个订单通常有5个或更less的订单。

换一个说法:如果你有一个复合键,改变是其中一个有很less的重复。 从上面应用解决scheme5。