EntityFramework - 包含复合键的查询

ste*_*rnr 26 .net c# entity-framework

给定一个id列表,我可以通过以下方式查询所有相关行:

context.Table.Where(q => listOfIds.Contains(q.Id));
Run Code Online (Sandbox Code Playgroud)

但是当Table具有复合键时,如何实现相同的功能?

Ger*_*old 39

这是一个令人讨厌的问题,我不知道任何优雅的解决方案.

假设您有这些组合键,并且您只想选择标记的组合(*).

Id1  Id2
---  ---
1    2 *
1    3
1    6
2    2 *
2    3 *
... (many more)
Run Code Online (Sandbox Code Playgroud)

如何做到这一点是实体框架快乐的一种方式?让我们看看一些可能的解决方案,看看它们是否有用.

解决方案1 ​​:( JoinContains)配对

最好的解决方案是创建所需对的列表,例如Tuples,(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
Run Code Online (Sandbox Code Playgroud)

在LINQ to objects中,这将是完美的,但是,太糟糕了,EF会抛出一个例外

无法创建类型'System.Tuple`2(...)的常量值在此上下文中仅支持基元类型或枚举类型.

这是一种相当笨拙的方式告诉您它无法将此语句转换为SQL,因为Tuples它不是原始值列表(如intstring).1.出于同样的原因,使用Contains(或任何其他LINQ语句)的类似语句将失败.

解决方案2:内存中

当然,我们可以将问题转化为简单的LINQ对象,如下所示:

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
Run Code Online (Sandbox Code Playgroud)

不用说,这不是一个好的解决方案.db.Table可能包含数百万条记录.

解决方案3:两个Contains陈述

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

from entity in db.Table
where ids1.Contains(entity.Id1) && ids2.Contains(entity.Id2)
select entity
Run Code Online (Sandbox Code Playgroud)

但现在结果还包含实体{1,3}!当然,这个实体完全匹配两个谓词.但是请记住,我们越来越近了.我们现在只获得其中的四个实体,而不是将数百万个实体吸引到内存中.

解决方案4:一个Contains具有计算值

解决方案3失败,因为两个单独的Contains语句不仅过滤其值的组合.如果我们首先创建组合列表并尝试匹配这些组合,该怎么办?我们从解决方案1中知道该列表应包含原始值.例如:

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

和LINQ声明:

from entity in db.Table
where computed.Contains(entity.Id1 * entity.Id2)
select entity
Run Code Online (Sandbox Code Playgroud)

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

from entity in db.Table
where computed.Contains("Id1=" + entity.Id1 + "," + "Id2=" + entity.Id2)
select entity
Run Code Online (Sandbox Code Playgroud)

(这适用于EF6,而不是早期版本).

这变得相当混乱.但是一个更重要的问题是这个解决方案不是可以解决的,这意味着:它绕过任何数据库索引Id1并且Id2可能已经使用过.这将表现得非常糟糕.

解决方案5:最好的2和3

因此,我能想到的唯一可行的解​​决方案是内存Containsjoin内存的组合:首先在解决方案3中执行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;
Run Code Online (Sandbox Code Playgroud)

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

解决方案6:使用OR子句构建查询

使用类似Linqkit的Predicate构建器或替代方法,您可以构建一个查询,该查询包含组合列表中每个元素的OR子句.对于真正的短名单,这可能是一个可行的选择.使用几百个元素,查询将开始执行非常糟糕.所以我不认为这是一个很好的解决方案,除非你能100%确定总会有少量的元素.可在此处找到此选项的一个详细说明.


1作为一个有趣的附注,EF 在您加入基元列表时创建一个SQL语句,就像这样

from entity in db.Table // db is a DbContext
join i in MyIntegers on entity.Id1 equals i
select entity
Run Code Online (Sandbox Code Playgroud)

但是生成的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]
Run Code Online (Sandbox Code Playgroud)

有n-1 UNIONs.当然,根本不可扩展.

后来补充:
在EF版本6.1.3的某个地方,这已经有了很大的改进.该UNION■找变得简单,他们不再嵌套.以前,查询将放弃本地序列中少于50个元素(SQL异常:SQL语句的某些部分嵌套得太深.)非嵌套UNION允许本地序列最多为数千(!)个元素.虽然有很多元素,但它仍然很慢.

2只要Contains语句是可伸缩的:Scalable包含针对SQL后端的LINQ方法

  • 为了清楚起见,如果您运行的集合超过2100个元素,那么.Contains将抛出异常([here](http://knowledgebase.progress.com/articles/Article/6160)). (2认同)
  • [解决方案7:编写一个存储过程,在多列上进行连接.](http://stackoverflow.com/questions/35688830/entity-framework-unable-to-create-a-constant-value-of-type-叔仅原语?noredirect = 1#comment59055806_35688830) (2认同)

yv9*_*89c 10

使用 SQL Server 的 Entity Framework Core 解决方案

新的! QueryableValuesEF6 Edition已经到来!

以下解决方案利用QueryableValues。这是我编写的一个库,主要是为了解决 SQL Server 中由于使用 LINQ 方法组成本地值的查询而导致的查询计划缓存污染问题Contains。它还允许您以高性能的方式在查询中组合复杂类型的值,这将实现这个问题中所要求的。

首先,您需要安装并设置库,之后您可以使用以下任何模式,允许您使用组合键查询实体:

// Required to make the AsQueryableValues method available on the DbContext.
using BlazarTech.QueryableValues;

// Local data that will be used to query by the composite key
// of the fictitious OrderProduct table.
var values = new[]
{
    new { OrderId = 1, ProductId = 10 },
    new { OrderId = 2, ProductId = 20 },
    new { OrderId = 3, ProductId = 30 }
};

// Optional helper variable (needed by the second example due to CS0854)
var queryableValues = dbContext.AsQueryableValues(values);

// Example 1 - Using a Join (preferred).
var example1Results = dbContext
    .OrderProduct
    .Join(
        queryableValues,
        e => new { e.OrderId, e.ProductId },
        v => new { v.OrderId, v.ProductId },
        (e, v) => e
    )
    .ToList();

// Example 2 - Using Any (similar behavior as Contains).
var example2Results = dbContext
    .OrderProduct
    .Where(e => queryableValues
        .Where(v =>
            v.OrderId == e.OrderId &&
            v.ProductId == e.ProductId
        )
        .Any()
    )
    .ToList();
Run Code Online (Sandbox Code Playgroud)

有用的链接

QueryableValues在 MIT 许可证下分发。


Sla*_*nov 6

您可以Union为每个复合主键使用:

var compositeKeys = new List<CK> 
{
    new CK { id1 = 1, id2 = 2 },
    new CK { id1 = 1, id2 = 3 },
    new CK { id1 = 2, id2 = 4 }
};

IQuerable<CK> query = null;
foreach(var ck in compositeKeys)
{
    var temp = context.Table.Where(x => x.id1 == ck.id1 && x.id2 == ck.id2);
    query = query == null ? temp : query.Union(temp);
}
var result = query.ToList();
Run Code Online (Sandbox Code Playgroud)


小智 -1

如果是复合键,您可以使用另一个 idlist 并在代码中添加一个条件

context.Table.Where(q => listOfIds.Contains(q.Id) && listOfIds2.Contains(q.Id2));
Run Code Online (Sandbox Code Playgroud)

或者您可以使用另一种技巧,通过添加密钥来创建密钥列表

listofid.add(id+id1+......)
context.Table.Where(q => listOfIds.Contains(q.Id+q.id1+.......));
Run Code Online (Sandbox Code Playgroud)