2009-02-20 9 views
23

J'essaie de créer une fonction générique pour m'aider à sélectionner des milliers d'enregistrements en utilisant LINQ to SQL à partir d'une liste locale. SQL Server (2005 au moins) limite les requêtes à 2100 paramètres et j'aimerais sélectionner plus d'enregistrements que cela.LINQ Expression à renvoyer Valeur de la propriété?

ici serait un bon exemple d'utilisation:

var some_product_numbers = new int[] { 1,2,3 ... 9999 }; 

Products.SelectByParameterList(some_product_numbers, p => p.ProductNumber); 

Voici mon (non travail) mise en œuvre:

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; 
} 

J'ai vu beaucoup d'exemples de prédicats de construction en utilisant des expressions. Dans ce cas, je veux seulement exécuter le délégué pour retourner la valeur du ProductNumber actuel. Ou plutôt, je veux traduire cela dans la requête SQL (ça marche bien sous une forme non générique).

Je sais que la compilation de l'expression me ramène juste à la case départ (passant dans le délégué comme Func) mais je ne suis pas sûr de savoir comment passer un paramètre à une expression "non compilée".

Merci pour votre aide!

**** EDIT: ** Permettez-moi de préciser encore:

Voici un exemple concret de ce que je veux généraliser:

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() 
; 

résultats dans ce code 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) 

Répondre

7

meilleure façon de faire: Utiliser LINQKit (Free, licence non restrictive)

travail version du code:

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; 
} 

Exemple d'utilisation:

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 

Merci encore pour votre aide!

+1

Je serais intéressé par ce que le TSQL pour cela, par rapport à ma réponse InRange ... –

+0

SELECT [t0] . [Id], ... [t0]. [Version] DE [Recharge] AS [t0] OERE [t0]. [Id] IN (@ p0, @ p1, @ p2, @ p3, @ p4, @ p5, @ p6, @ p7, @ p8, @ p9) ... Cette requête 2times (20/10 = 2) – kwcto

+0

Quelle est votre suggestion pour ** blockSize ** afin d'optimiser les requêtes en utilisant LinqToSql? Pour le dire autrement, est-il préférable d'avoir moins de requêtes avec des blocs plus grands, ou plus de requêtes avec des blocs plus petits? – ni5ni6

3

LINQ-to-SQL fonctionne toujours via des paramètres SQL standard, l'écriture d'une expression sophistiquée ne va donc pas aider. Il y a 3 options communes ici:

  • emballez les identifiants dans (par exemple) csv/tsv; transmettre en tant que varchar(max) et utiliser un udf pour le diviser (sur le serveur) en une variable de table; joindre à la variable de table
  • utiliser un paramètre table-évalué dans SQL Server 2008
  • avoir une table sur le serveur que vous pourriez pousser les identifiants dans (peut-être via SqlBulkCopy) (peut-être avec un "session guid" ou similaire) joindre à cette table

La première est la plus simple; obtenir un "split csv udf" est trivial (il suffit de le chercher). Faites glisser l'udf sur le contexte de données et consommez à partir de là.

+2

Ceci n'est pas nécessaire. Voir ma réponse ci-dessous. – kwcto

41

J'ai trouvé un moyen de découper la requête en morceaux - c'est-à-dire que vous lui donnez 4000 valeurs, donc elle peut faire 4 requêtes de 1000 chacune; avec un exemple complet de Northwind. Notez que cela ne fonctionne pas sur Entity Framework, en raison de Expression.Invoke - mais bien sur LINQ to SQL:

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(); 
      } 
     } 
    } 
} 
+2

Ceci gère le cas de 'queryable.Where (o => values.Contains (o.propertyToTest))' en le remplaçant par 'queryable.InRange (o => o.propertyToTest, blockSize, values)' (si je comprends bien correctement), mais je regarde un débordement similaire sur la limite des paramètres 2100, avec par exemple 'queryable.Where (o =>! values.Contains (o.propertyToTest))'. J'essaye de modifier InRange() pour obtenir un équivalent de NotInRange(), et je ne suis pas sûr comment faire la négation booléenne. Ma pensée était à la ligne 'foreach (T record in source.Where (lambda))'? –

+2

En fait, après beaucoup de chasse, je pense que j'ai trouvé ce qui est nécessaire, de manière appropriée d'une réponse que vous avez donné un mois auparavant à celui-ci: http://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct -bool # 457328, ref "Cela fonctionne aussi bien pour annuler une seule opération:" –

+0

@Marc - Comment cela serait-il géré dans VB? «rendement rendement» n'existe évidemment pas pour nous. –

0

Passez la IQuerable à la fonction Contains au lieu de la liste ou du tableau. S'il vous plaît voir l'exemple ci-dessous

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(); 

lorsque vous passez la liste ou un tableau, il est passé sous forme de paramètres et de son dépasser les comptes lorsque les éléments de la liste compte est supérieur à 2100.

+2

Vous assumez la collection à comparer provient de la base de données elle-même. Ce n'est pas toujours le cas. – kwcto

0

Vous pouvez créer votre propre 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(); 

     } 

    } 
Questions connexes