我正在寻找一种以可扩展方式执行Contains()
语句的优雅方式。在我提出实际问题之前,请允许我给出一些背景知识。
IN
声明在Entity Framework和LINQ to SQL中,Contains
语句被翻译为SQL IN
语句。例如,从这个声明:
var ids = Enumerable.Range(1,10);
var courses = Courses.Where(c => ids.Contains(c.CourseID)).ToList();
实体框架将生成
SELECT
[Extent1].[CourseID] AS [CourseID],
[Extent1].[Title] AS [Title],
[Extent1].[Credits] AS [Credits],
[Extent1].[DepartmentID] AS [DepartmentID]
FROM [dbo].[Course] AS [Extent1]
WHERE [Extent1].[CourseID] IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
不幸的是,In
语句不可扩展。根据{{3}}:
在IN子句中包含极大数量的值(数千个)会消耗资源并返回错误8623或8632
与耗尽资源或超出表达式限制有关。
但之前发生这些错误时,随着项目数量的增加,IN
语句变得越来越慢。我无法找到有关其增长率的文档,但它可以很好地执行数千个项目,但除此之外它变得非常缓慢。 (基于SQL Server经验)。
我们不能总是避免这种说法。具有源数据的JOIN
通常会表现得更好,但这只有在源数据位于同一上下文中时才有可能。在这里,我正在处理来自断开连接的场景中的客户端的数据。所以我一直在寻找可扩展的解决方案。一个令人满意的方法是将操作切成块:
var courses = ids.ToChunks(1000)
.Select(chunk => Courses.Where(c => chunk.Contains(c.CourseID)))
.SelectMany(x => x).ToList();
(其中ToChunks
是MSDN小扩展方法)。
这将以1000块的形式执行查询,这些查询都表现得非常好。用例如将运行5000个项目,5个查询,这些查询可能比具有5000个项目的一个查询更快。
但是我当然不希望在我的代码中分散这个构造。我正在寻找一种扩展方法,通过该方法可以将任何IQueryable<T>
转换为厚实的执行语句。理想情况下是这样的:
var courses = Courses.Where(c => ids.Contains(c.CourseID))
.AsChunky(1000)
.ToList();
但也许这个
var courses = Courses.ChunkyContains(c => c.CourseID, ids, 1000)
.ToList();
我第一次给出了后一种解决方案:
public static IEnumerable<TEntity> ChunkyContains<TEntity, TContains>(
this IQueryable<TEntity> query,
Expression<Func<TEntity,TContains>> match,
IEnumerable<TContains> containList,
int chunkSize = 500)
{
return containList.ToChunks(chunkSize)
.Select (chunk => query.Where(x => chunk.Contains(match)))
.SelectMany(x => x);
}
显然,部分x => chunk.Contains(match)
无法编译。但我不知道如何将match
表达式操纵为Contains
表达式。
也许有人可以帮我解决这个问题。当然,我愿意接受其他方法来使这个语句可扩展。
答案 0 :(得分:8)
我在一个月之前用一种不同的方法解决了这个问题。也许这对你来说也是一个很好的解决方案。
我不希望我的解决方案更改查询本身。所以ids.ChunkContains(p.Id)或特殊的WhereContains方法是不可行的。该解决方案还应该能够将Contains与另一个过滤器结合使用,以及多次使用相同的集合。
db.TestEntities.Where(p => (ids.Contains(p.Id) || ids.Contains(p.ParentId)) && p.Name.StartsWith("Test"))
所以我尝试将逻辑封装在一个特殊的ToList方法中,该方法可以重写表达式,以便以块的形式查询指定的集合。
var ids = Enumerable.Range(1, 11);
var result = db.TestEntities.Where(p => Ids.Contains(p.Id) && p.Name.StartsWith ("Test"))
.ToChunkedList(ids,4);
为了重写表达式树,我在查询中发现了来自本地集合的所有Contains方法调用,并带有帮助类的视图。
private class ContainsExpression
{
public ContainsExpression(MethodCallExpression methodCall)
{
this.MethodCall = methodCall;
}
public MethodCallExpression MethodCall { get; private set; }
public object GetValue()
{
var parent = MethodCall.Object ?? MethodCall.Arguments.FirstOrDefault();
return Expression.Lambda<Func<object>>(parent).Compile()();
}
public bool IsLocalList()
{
Expression parent = MethodCall.Object ?? MethodCall.Arguments.FirstOrDefault();
while (parent != null) {
if (parent is ConstantExpression)
return true;
var member = parent as MemberExpression;
if (member != null) {
parent = member.Expression;
} else {
parent = null;
}
}
return false;
}
}
private class FindExpressionVisitor<T> : ExpressionVisitor where T : Expression
{
public List<T> FoundItems { get; private set; }
public FindExpressionVisitor()
{
this.FoundItems = new List<T>();
}
public override Expression Visit(Expression node)
{
var found = node as T;
if (found != null) {
this.FoundItems.Add(found);
}
return base.Visit(node);
}
}
public static List<T> ToChunkedList<T, TValue>(this IQueryable<T> query, IEnumerable<TValue> list, int chunkSize)
{
var finder = new FindExpressionVisitor<MethodCallExpression>();
finder.Visit(query.Expression);
var methodCalls = finder.FoundItems.Where(p => p.Method.Name == "Contains").Select(p => new ContainsExpression(p)).Where(p => p.IsLocalList()).ToList();
var localLists = methodCalls.Where(p => p.GetValue() == list).ToList();
如果在查询表达式中找到了在ToChunkedList方法中传递的本地集合,我将对原始列表的Contains调用替换为包含一个批处理的id的临时列表的新调用。
if (localLists.Any()) {
var result = new List<T>();
var valueList = new List<TValue>();
var containsMethod = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)
.Single(p => p.Name == "Contains" && p.GetParameters().Count() == 2)
.MakeGenericMethod(typeof(TValue));
var queryExpression = query.Expression;
foreach (var item in localLists) {
var parameter = new List<Expression>();
parameter.Add(Expression.Constant(valueList));
if (item.MethodCall.Object == null) {
parameter.AddRange(item.MethodCall.Arguments.Skip(1));
} else {
parameter.AddRange(item.MethodCall.Arguments);
}
var call = Expression.Call(containsMethod, parameter.ToArray());
var replacer = new ExpressionReplacer(item.MethodCall,call);
queryExpression = replacer.Visit(queryExpression);
}
var chunkQuery = query.Provider.CreateQuery<T>(queryExpression);
for (int i = 0; i < Math.Ceiling((decimal)list.Count() / chunkSize); i++) {
valueList.Clear();
valueList.AddRange(list.Skip(i * chunkSize).Take(chunkSize));
result.AddRange(chunkQuery.ToList());
}
return result;
}
// if the collection was not found return query.ToList()
return query.ToList();
Expression Replacer:
private class ExpressionReplacer : ExpressionVisitor {
private Expression find, replace;
public ExpressionReplacer(Expression find, Expression replace)
{
this.find = find;
this.replace = replace;
}
public override Expression Visit(Expression node)
{
if (node == this.find)
return this.replace;
return base.Visit(node);
}
}
答案 1 :(得分:1)
Linqkit救援!可能是一种直接做到这一点的更好的方式,但这似乎工作得很好,并且很清楚正在做什么。添加的内容为AsExpandable()
,可让您使用Invoke
扩展名。
using LinqKit;
public static IEnumerable<TEntity> ChunkyContains<TEntity, TContains>(
this IQueryable<TEntity> query,
Expression<Func<TEntity,TContains>> match,
IEnumerable<TContains> containList,
int chunkSize = 500)
{
return containList
.ToChunks(chunkSize)
.Select (chunk => query.AsExpandable()
.Where(x => chunk.Contains(match.Invoke(x))))
.SelectMany(x => x);
}
您可能还想这样做:
containsList.Distinct()
.ToChunks(chunkSize)
...或类似的东西,如果出现这种情况,你不会得到重复的结果:
query.ChunkyContains(x => x.Id, new List<int> { 1, 1 }, 1);
答案 2 :(得分:1)
另一种方法是以这种方式构建谓词(当然,应该改进一些部分,只是提出想法)。
public static Expression<Func<TEntity, bool>> ContainsPredicate<TEntity, TContains>(this IEnumerable<TContains> chunk, Expression<Func<TEntity, TContains>> match)
{
return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(
typeof (Enumerable),
"Contains",
new[]
{
typeof (TContains)
},
Expression.Constant(chunk, typeof(IEnumerable<TContains>)), match.Body),
match.Parameters);
}
你可以在你的ChunkContains方法中调用
return containList.ToChunks(chunkSize)
.Select(chunk => query.Where(ContainsPredicate(chunk, match)))
.SelectMany(x => x);
答案 3 :(得分:0)
有没有办法让你用两个数据上下文来解决这个问题?
两个底层数据库是否有任何方式可以相互通信。例如,在SQL Server中,您可以创建链接服务器,以允许您对两个表运行查询,就好像它们位于同一个数据库中一样。
根据数据更改的频率,您可以设置ETL过程以将ID(和相关数据)导入数据库。
答案 4 :(得分:0)
使用带有表值参数的存储过程也可以正常工作。实际上你在表/视图和表值参数之间的存储过程中编写了一个联合。
https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters