给定一个id列表,我可以通过以下方式查询所有相关行:
context.Table.Where(q => listOfIds.Contains(q.Id));
但是当Table有一个复合键时,你如何实现相同的功能呢?
答案 0 :(得分:37)
这是一个令人讨厌的问题,我不知道任何优雅的解决方案。
假设您有这些组合键,并且您只想选择标记的组合(*)。
Id1 Id2
--- ---
1 2 *
1 3
1 6
2 2 *
2 3 *
... (many more)
如何做到这一点是实体框架快乐的一种方式?让我们看看一些可能的解决方案,看看它们是否有用。
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
不是原始值列表(如int
或string
) 1 。出于同样的原因,使用Contains
(或任何其他LINQ语句)的类似语句将失败。
当然,我们可以将问题转化为简单的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
可能包含数百万条记录。
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}
!当然,这个实体完全匹配两个谓词。但是请记住,我们越来越近了。我们现在只获得其中的四个实体,而不是将数百万个实体吸引到内存中。
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,这意味着:它绕过Id1
和Id2
上可能已经使用过的任何数据库索引。这将表现得非常糟糕。
所以我能想到的唯一可行的解决方案是内存中Contains
和join
的组合:首先在解决方案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 解决方案,并应用于我自己的代码中。
使用类似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
的结果将唯一地标识Id1
和Id2
的值为只要正确选择了素数。
但是你最终会遇到一种情况,即你正在实施复杂的概念而某人将不得不支持这一点。可能更好地编写一个存储过程来获取有效的密钥对象。
答案 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)
在缺乏一般解决方案的情况下,我认为有两件事需要考虑:
例如,引出我这个问题的问题是查询订单行,其中键是订单ID +订单行号+订单类型,而源订单类型是隐式的。也就是说,订单类型是常量,订单ID会将查询集减少到相关订单的订单行,每个订单通常会有5个或更少。
要改写:如果您有复合键,则更改是其中一个复制键非常少。从上面应用解决方案5。