EntityFramework - 包含复合键的查询

时间:2014-10-05 01:48:37

标签: c# .net entity-framework

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

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

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

8 个答案:

答案 0 :(得分:37)

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

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

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

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

解决方案1:Join(或Contains)成对

最好的解决方案是创建所需对的列表,例如元组(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 to对象中,这将是完美的,但是,太糟糕了,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

毋庸置疑,这不是一个好的解决方案。 db.Table可能包含数百万条记录。

解决方案3:两个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}!当然,这个实体完全匹配两个谓词。但是请记住,我们越来越近了。我们现在只获得其中的四个实体,而不是将数百万个实体吸引到内存中。

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

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

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)不会生成唯一标识数据库中对的值。现在我们可以创建一个字符串列表,如["Id1=1,Id2=2","Id1=2,Id2=3]"

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

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

这变得非常混乱。但更重要的一个问题是此解决方案不是sargable,这意味着:它绕过Id1Id2上可能已经使用过的任何数据库索引。这将表现得非常糟糕。

解决方案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;

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

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

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


1 作为一个有趣的附注,当您加入基元列表时,EF 创建一个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变得更简单,它们不再嵌套。以前,查询将放弃本地序列中少于50个元素(SQL异常: SQL语句的某些部分嵌套太深。)非嵌套UNION允许本地序列高达数千(!)的元素。虽然有很多元素,但它仍然很慢。

2 至于Contains语句是可扩展的:Scalable Contains method for LINQ against a SQL backend

答案 1 :(得分:2)

您可以使用这两个键创建字符串集合(我假设您的键是int类型):

self.imageView.backgroundColor = UIColor.black

然后你可以使用&#34;包含&#34;在您的数据库:

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

答案 2 :(得分:0)

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

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

或者您可以使用另一个技巧通过添加键来创建键列表

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

答案 3 :(得分:0)

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

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的有效使用;将Key类型发送到数据库时可能会出现问题。如果发生这种情况,那么您可以发挥创意:

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

您可以创建一个同构函数(素数对此有用),类似于哈希码,您可以使用它来比较这对值。只要乘法因子是共同素数,这个模式将是同构的(一对一) - 即p1*Id1 + p2*Id2的结果将唯一地标识Id1Id2的值为只要正确选择了素数。

但是你最终会遇到一种情况,即你正在实施复杂的概念而某人将不得不支持这一点。可能更好地编写一个存储过程来获取有效的密钥对象。

答案 4 :(得分:0)

我尝试了此解决方案,它与我合作,输出查询非常完美,没有任何参数

import UIKit
import PDFKit

class ViewController: UIViewController {

    @IBOutlet weak var pdfView: PDFView!

    lazy var pdfDoc:PDFDocument? = {
        guard let path = Bundle.main.path(forResource: "6368", ofType: "pdf") else {return nil}
        let url = URL(fileURLWithPath: path)
        return PDFDocument(url: url)
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setupPDFView()
        self.save()
    }

    func setupPDFView() {
        //Setup and put pdf on view
        pdfView.autoScales = true
        pdfView.displayMode = .singlePageContinuous
        pdfView.displayDirection = .horizontal
        pdfView.document = pdfDoc

        self.add(annotation: self.circleAnnotation(), to: 0)
    }

    func add(annotation: PDFAnnotation, to page:Int){
        self.pdfDoc?.page(at: page)?.addAnnotation(annotation)
    }

    func circleAnnotation()->PDFAnnotation {
        let bounds = CGRect(x: 20.0, y: 20.0, width:200.0, height: 200.0)
        let annotation = PDFAnnotation(bounds: bounds, forType: .circle, withProperties: nil)
        annotation.interiorColor = UIColor.black
        return annotation
    }

    func save() {
        //Save to file
        guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {return}
        let data = pdfView.document?.dataRepresentation()
        do {
            if data != nil{try data!.write(to: url)}
        } 
        catch {
            print(error.localizedDescription)
        }
    }
}

答案 5 :(得分:0)

您可以为每个复合主键使用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();

答案 6 :(得分:0)

我最终为这个问题编写了一个依赖于 System.Linq.Dynamic.Core;

它有很多代码,目前没有时间重构,但感谢输入/建议。

        public static IQueryable<TEntity> WhereIsOneOf<TEntity, TSource>(this IQueryable<TEntity> dbSet, 
            IEnumerable<TSource> source, 
            Expression<Func<TEntity, TSource,bool>> predicate) where TEntity : class
        {
            var (where, pDict) = GetEntityPredicate(predicate, source);
            return dbSet.Where(where, pDict);

            (string WhereStr, IDictionary<string, object> paramDict) GetEntityPredicate(Expression<Func<TEntity, TSource, bool>> func, IEnumerable<TSource> source)
            {
                var firstP = func.Parameters[0];
                var binaryExpressions = RecurseBinaryExpressions((BinaryExpression)func.Body);

                var i = 0;
                var paramDict = new Dictionary<string, object>();
                var res = new List<string>();
                foreach (var sourceItem in source)
                {
                    var innerRes = new List<string>();
                    foreach (var bExp in binaryExpressions)
                    {
                        var emp = ToEMemberPredicate(firstP, bExp);
                        var val = emp.GetKeyValue(sourceItem);
                        var pName = $"@{i++}";
                        paramDict.Add(pName, val);
                        var str = $"{emp.EntityMemberName} {emp.SQLOperator} {pName}";
                        innerRes.Add(str);
                    }

                    res.Add( "(" + string.Join(" and ", innerRes) + ")");
                }

                var sRes = string.Join(" || ", res);

                return (sRes, paramDict);
            }
            
            EMemberPredicate ToEMemberPredicate(ParameterExpression firstP, BinaryExpression bExp)
            {
                var lMember = (MemberExpression)bExp.Left;
                var rMember = (MemberExpression)bExp.Right;

                var entityMember = lMember.Expression == firstP ? lMember : rMember;
                var keyMember = entityMember == lMember ? rMember : lMember;

                return new EMemberPredicate(entityMember, keyMember, bExp.NodeType);
            }
            List<BinaryExpression> RecurseBinaryExpressions(BinaryExpression e, List<BinaryExpression> runningList = null)
            {
                if (runningList == null) runningList = new List<BinaryExpression>();

                if (e.Left is BinaryExpression lbe)
                {
                    var additions = RecurseBinaryExpressions(lbe);
                    runningList.AddRange(additions);
                }
                
                if (e.Right is BinaryExpression rbe)
                {
                    var additions = RecurseBinaryExpressions(rbe);
                    runningList.AddRange(additions);
                }

                if (e.Left is MemberExpression && e.Right is MemberExpression)
                {
                    runningList.Add(e);
                }

                return runningList;
            }
        }

助手类:

    public class EMemberPredicate
    {
        public readonly MemberExpression EntityMember;
        public readonly MemberExpression KeyMember;
        public readonly PropertyInfo KeyMemberPropInfo;
        public readonly string EntityMemberName;
        public readonly string SQLOperator;

        public EMemberPredicate(MemberExpression entityMember, MemberExpression keyMember, ExpressionType eType)
        {
            EntityMember = entityMember;
            KeyMember = keyMember;
            KeyMemberPropInfo = (PropertyInfo)keyMember.Member;
            EntityMemberName = entityMember.Member.Name;
            SQLOperator = BinaryExpressionToMSSQLOperator(eType);
        }

        public object GetKeyValue(object o)
        {
            return KeyMemberPropInfo.GetValue(o, null);
        }

        private string BinaryExpressionToMSSQLOperator(ExpressionType eType)
        {
            switch (eType)
            {
                case ExpressionType.Equal:
                    return "==";
                case ExpressionType.GreaterThan:
                    return ">";
                case ExpressionType.GreaterThanOrEqual:
                    return ">=";
                case ExpressionType.LessThan:
                    return "<";
                case ExpressionType.LessThanOrEqual:
                    return "<=";
                case ExpressionType.NotEqual:
                    return "<>";
                default:
                    throw new ArgumentException($"{eType} is not a handled Expression Type.");
            }
        }
    }

像这样使用:

// This can be a Tuple or whatever..  If Tuple, then y below would be .Item1, etc.
// This data structure is up to you but is what I use.
[FromBody] List<CustomerAddressPk> cKeys
            var res = await dbCtx.CustomerAddress
                .WhereIsOneOf(cKeys, (x, y) => y.CustomerId == x.CustomerId 
                   && x.AddressId == y.AddressId)
                .ToListAsync();

希望这对其他人有帮助。

答案 7 :(得分:-1)

在缺乏一般解决方案的情况下,我认为有两件事需要考虑:

  1. 避免使用多列主键(也会使单元测试更容易)。
  2. 但是,如果你必须,那么其中一个人可能会减少 查询结果大小为O(n),其中n是理想查询的大小 结果。从这里开始,上面是Gerd Arnold的解决方案5.
  3. 例如,引出我这个问题的问题是查询订单行,其中键是订单ID +订单行号+订单类型,而源订单类型是隐式的。也就是说,订单类型是常量,订单ID会将查询集减少到相关订单的订单行,每个订单通常会有5个或更少。

    要改写:如果您有复合键,则更改是其中一个复制键非常少。从上面应用解决方案5。