ローカルリストからLINQtoSQLを使用して数千のレコードを選択するのに役立つ汎用関数を作成しようとしています。 SQL Server(少なくとも2005)はクエリを2100パラメータに制限しており、それよりも多くのレコードを選択したいと思います。
これが良い使用例です:
var some_product_numbers = new int[] { 1,2,3 ... 9999 };
Products.SelectByParameterList(some_product_numbers, p => p.ProductNumber);
これが私の(機能しない)実装です:
public static IEnumerable<T> SelectByParameterList<T, PropertyType>(Table<T> items,
IEnumerable<PropertyType> parameterList, Expression<Func<T, PropertyType>> property) where T : class
{
var groups = parameterList
.Select((Parameter, index) =>
new
{
GroupID = index / 2000, //2000 parameters per request
Parameter
}
)
.GroupBy(x => x.GroupID)
.AsEnumerable();
var results = groups
.Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } )
.SelectMany(g =>
/* THIS PART FAILS MISERABLY */
items.Where(item => g.Parameters.Contains(property.Compile()(item)))
);
return results;
}
式を使用して述語を作成する例をたくさん見てきました。この場合、デリゲートを実行して現在のProductNumberの値を返すだけです。むしろ、これをSQLクエリに変換したいと思います(一般的でない形式でも正常に機能します)。
式をコンパイルすると、正方形に戻るだけです(デリゲートをFuncとして渡す)が、パラメーターを「コンパイルされていない」式に渡す方法がわかりません。
ご協力いただきありがとうございます!
****編集:**さらに明確にしましょう:
これが私が一般化したいものの実用的な例です:
var local_refill_ids = Refills.Select(r => r.Id).Take(20).ToArray();
var groups = local_refill_ids
.Select((Parameter, index) =>
new
{
GroupID = index / 5, //5 parameters per request
Parameter
}
)
.GroupBy(x => x.GroupID)
.AsEnumerable();
var results = groups
.Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } )
.SelectMany(g =>
Refills.Where(r => g.Parameters.Contains(r.Id))
)
.ToArray()
;
このSQLコードの結果:
SELECT [t0].[Id], ... [t0].[Version]
FROM [Refill] AS [t0]
WHERE [t0].[Id] IN (@p0, @p1, @p2, @p3, @p4)
... That query 4 more times (20 / 5 = 4)
これを行う最も簡単な方法:使用 LINQKit (無料の非制限ライセンス)
コードの作業バージョン:
public static IEnumerable<T> SelectByParameterList<T, PropertyType>(this Table<T> items, IEnumerable<PropertyType> parameterList, Expression<Func<T, PropertyType>> propertySelector, int blockSize) where T : class
{
var groups = parameterList
.Select((Parameter, index) =>
new
{
GroupID = index / blockSize, //# of parameters per request
Parameter
}
)
.GroupBy(x => x.GroupID)
.AsEnumerable();
var selector = LinqKit.Linq.Expr(propertySelector);
var results = groups
.Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } )
.SelectMany(g =>
/* AsExpandable() extension method requires LinqKit DLL */
items.AsExpandable().Where(item => g.Parameters.Contains(selector.Invoke(item)))
);
return results;
}
使用例:
Guid[] local_refill_ids = Refills.Select(r => r.Id).Take(20).ToArray();
IEnumerable<Refill> results = Refills.SelectByParameterList(local_refill_ids, r => r.Id, 10); //runs 2 SQL queries with 10 parameters each
あなたのご親切に感謝します!
クエリを細かく分割する方法を考え出しました。つまり、4000個の値を指定すると、それぞれ1000個のリクエストが4回実行される可能性があります。完全なノースウィンドの例で。 Expression.Invoke
のため、これはEntity Frameworkでは機能しない可能性があることに注意してください。ただし、LINQ toSQLでは問題ありません。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace ConsoleApplication5 {
/// SAMPLE USAGE
class Program {
static void Main(string[] args) {
// get some ids to play with...
string[] ids;
using(var ctx = new DataClasses1DataContext()) {
ids = ctx.Customers.Select(x => x.CustomerID)
.Take(100).ToArray();
}
// now do our fun select - using a deliberately small
// batch size to prove it...
using (var ctx = new DataClasses1DataContext()) {
ctx.Log = Console.Out;
foreach(var cust in ctx.Customers
.InRange(x => x.CustomerID, 5, ids)) {
Console.WriteLine(cust.CompanyName);
}
}
}
}
/// THIS IS THE INTERESTING BIT
public static class QueryableChunked {
public static IEnumerable<T> InRange<T, TValue>(
this IQueryable<T> source,
Expression<Func<T, TValue>> selector,
int blockSize,
IEnumerable<TValue> values) {
MethodInfo method = null;
foreach(MethodInfo tmp in typeof(Enumerable).GetMethods(
BindingFlags.Public | BindingFlags.Static)) {
if(tmp.Name == "Contains" && tmp.IsGenericMethodDefinition
&& tmp.GetParameters().Length == 2) {
method = tmp.MakeGenericMethod(typeof (TValue));
break;
}
}
if(method==null) throw new InvalidOperationException(
"Unable to locate Contains");
foreach(TValue[] block in values.GetBlocks(blockSize)) {
var row = Expression.Parameter(typeof (T), "row");
var member = Expression.Invoke(selector, row);
var keys = Expression.Constant(block, typeof (TValue[]));
var predicate = Expression.Call(method, keys, member);
var lambda = Expression.Lambda<Func<T,bool>>(
predicate, row);
foreach(T record in source.Where(lambda)) {
yield return record;
}
}
}
public static IEnumerable<T[]> GetBlocks<T>(
this IEnumerable<T> source, int blockSize) {
List<T> list = new List<T>(blockSize);
foreach(T item in source) {
list.Add(item);
if(list.Count == blockSize) {
yield return list.ToArray();
list.Clear();
}
}
if(list.Count > 0) {
yield return list.ToArray();
}
}
}
}
LINQ-to-SQLは引き続き標準のSQLパラメーターを介して機能するため、派手な式を記述しても役に立ちません。ここには3つの一般的なオプションがあります。
varchar(max)
として渡し、udfを使用して(サーバーで)テーブル変数に分割します。テーブル変数に結合します1つ目は最も単純です。 「splitcsvudf」を取得するのは簡単です(検索するだけです)。 udfをデータコンテキストにドラッグし、そこから消費します。
独自のQueryProviderを作成できます
public class QueryProvider : IQueryProvider
{
// Translates LINQ query to SQL.
private readonly Func<IQueryable, DbCommand> _translator;
// Executes the translated SQL and retrieves results.
private readonly Func<Type, string, object[], IEnumerable> _executor;
public QueryProvider(
Func<IQueryable, DbCommand> translator,
Func<Type, string, object[], IEnumerable> executor)
{
this._translator = translator;
this._executor = executor;
}
#region IQueryProvider Members
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return new Queryable<TElement>(this, expression);
}
public IQueryable CreateQuery(Expression expression)
{
throw new NotImplementedException();
}
public TResult Execute<TResult>(Expression expression)
{
bool isCollection = typeof(TResult).IsGenericType &&
typeof(TResult).GetGenericTypeDefinition() == typeof(IEnumerable<>);
var itemType = isCollection
// TResult is an IEnumerable`1 collection.
? typeof(TResult).GetGenericArguments().Single()
// TResult is not an IEnumerable`1 collection, but a single item.
: typeof(TResult);
var queryable = Activator.CreateInstance(
typeof(Queryable<>).MakeGenericType(itemType), this, expression) as IQueryable;
IEnumerable queryResult;
// Translates LINQ query to SQL.
using (var command = this._translator(queryable))
{
var parameters = command.Parameters.OfType<DbParameter>()
.Select(parameter => parameter)
.ToList();
var query = command.CommandText;
var newParameters = GetNewParameterList(ref query, parameters);
queryResult = _executor(itemType,query,newParameters);
}
return isCollection
? (TResult)queryResult // Returns an IEnumerable`1 collection.
: queryResult.OfType<TResult>()
.SingleOrDefault(); // Returns a single item.
}
public object Execute(Expression expression)
{
throw new NotImplementedException();
}
#endregion
private static object[] GetNewParameterList(ref string query, List<DbParameter> parameters)
{
var newParameters = new List<DbParameter>(parameters);
foreach (var dbParameter in parameters.Where(p => p.DbType == System.Data.DbType.Int32))
{
var name = dbParameter.ParameterName;
var value = dbParameter.Value != null ? dbParameter.Value.ToString() : "NULL";
var pattern = String.Format("{0}[^0-9]", dbParameter.ParameterName);
query = Regex.Replace(query, pattern, match => value + match.Value.Replace(name, ""));
newParameters.Remove(dbParameter);
}
for (var i = 0; i < newParameters.Count; i++)
{
var parameter = newParameters[i];
var oldName = parameter.ParameterName;
var pattern = String.Format("{0}[^0-9]", oldName);
var newName = "@p" + i;
query = Regex.Replace(query, pattern, match => newName + match.Value.Replace(oldName, ""));
}
return newParameters.Select(x => x.Value).ToArray();
}
}
static void Main(string[] args)
{
using (var dc=new DataContext())
{
var provider = new QueryProvider(dc.GetCommand, dc.ExecuteQuery);
var serviceIds = Enumerable.Range(1, 2200).ToArray();
var tasks = new Queryable<Task>(provider, dc.Tasks).Where(x => serviceIds.Contains(x.ServiceId) && x.CreatorId==37 && x.Creator.Name=="12312").ToArray();
}
}
リストや配列の代わりにIQuerable
をContains
関数に渡します。以下の例をご覧ください
var df_handsets = db.DataFeed_Handsets.Where(m => m.LaunchDate != null).
Select(m => m.Name);
var Make = (from m in db.MobilePhones
where (m.IsDeleted != true || m.IsDeleted == null)
&& df_handsets.Contains(m.Name)
orderby m.Make
select new { Value = m.Make, Text = m.Make }).Distinct();
リストまたは配列を渡すと、パラメーターの形式で渡され、リスト項目の数が2100を超えると、数を超えます。