2009-02-20 19 views
23

Sto cercando di creare una funzione generica che mi aiuti a selezionare migliaia di record utilizzando LINQ to SQL da un elenco locale. SQL Server (almeno per il 2005) limita le query ai parametri 2100 e vorrei selezionare più record.LINQ Espressione per restituire Valore di proprietà?

qui sarebbe un buon esempio di utilizzo:

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

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

Ecco il mio (non funzionante) implementazione:

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

Ho visto un sacco di esempi di predicati costruzione utilizzando espressioni. In questo caso voglio solo eseguire il delegato per restituire il valore dell'attuale numero prodotto. O meglio, voglio tradurre questo nella query SQL (funziona bene in forma non generica).

So che la compilazione dell'espressione mi riporta indietro al punto uno (passando al delegato come Func) ma non sono sicuro di come passare un parametro a un'espressione "non compilata".

Grazie per il vostro aiuto!

**** EDIT: ** Vorrei chiarire ulteriormente:

Ecco un esempio di lavoro di ciò che voglio generalizzare:

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

I risultati in questo codice 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) 

risposta

7

modo più semplice per farlo: Utilizzare LINQKit (libero, licenza non restrittiva)

di lavoro versione di codice: utilizzo

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

Esempio:

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 

Grazie ancora per tutto il tuo aiuto!

+1

Sarei interessato a ciò che TSQL fa, rispetto alla mia risposta InRange ... –

+0

SELECT [t0] . [Id], ... [t0]. [Version] FROM [Refill] AS [t0] DOVE [t0]. [Id] IN (@ p0, @ p1, @ p2, @ p3, @ p4, @ p5, @ p6, @ p7, @ p8, @ p9) ... Quella query 2 volte (20/10 = 2) – kwcto

+0

Qual è il tuo suggerimento per ** blockSize ** per ottimizzare le query utilizzando LinqToSql? O , per dirla in modo diverso, è meglio avere meno query con blocchi più grandi o più query con blocchi più piccoli? – ni5ni6

3

LINQ-to-SQL funziona ancora tramite parametri SQL standard, quindi la scrittura di un'espressione di fantasia non aiuterà. Esistono 3 opzioni comuni:

  • inserire gli ID in (ad esempio) csv/tsv; passare come varchar(max) e usare un udf per dividerlo (sul server) in una variabile di tabella; unire alla variabile di tabella
  • utilizzare un parametro con valori di tabella in SQL Server 2008
  • avere una tabella sul server che è possibile inserire gli ID in (forse tramite SqlBulkCopy) (forse con un "guid guid" o simile); iscriviti a questo tavolo

Il primo è il più semplice; ottenere un "split csv udf" è banale (basta cercarlo). Trascina l'udf sul contesto dati e consumala da lì.

+2

Questo non è necessario. Vedi la mia risposta qui sotto. – kwcto

41

Ho trovato un modo per dividere la query in pezzi, cioè gli dai 4000 valori, quindi potrebbe fare 4 richieste di 1000 ciascuna; con l'esempio di Northwind completo. Si noti che questo potrebbe non funzionare su Entity Framework, a causa di Expression.Invoke - ma è bene su 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

Questo gestisce il caso di 'queryable.Where (o => values.Contains (o.propertyToTest))' sostituendolo con 'queryable.InRange (o => o.propertyToTest, blockSize, values)' (se lo capisco correttamente), ma sto osservando un overflow simile sul limite dei parametri 2100, ad es 'queryable.Where (o =>! values.Contains (o.propertyToTest))'. Sto cercando di modificare InRange() per ottenere un equivalente NotInRange() e non sono sicuro di come eseguire la negazione booleana. Il mio pensiero era sulla linea 'foreach (T record in source.Where (lambda))'? –

+2

In realtà, dopo una lunga caccia, penso di aver trovato ciò che è necessario, in modo appropriato da una risposta che avevi dato un mese prima a questo: http://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct -bool # 457328, ref "Anche questo funziona bene per annullare una singola operazione:" –

+0

@Marc - Come sarebbe gestito in VB? 'yield return 'ovviamente non esiste per noi. –

0

Passaggio IQuerable alla funzione Contains anziché elenco o matrice. vedere l'esempio di seguito

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

quando si passa lista o un array viene passato sotto forma di parametri e la sua superare i conteggi quando gli elementi dell'elenco conteggio è maggiore di 2100.

+2

Stai assumendo la collezione da abbinare proviene dal database stesso. Questo non è sempre il caso. – kwcto

0

è possibile creare il proprio 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(); 

     } 

    } 
Problemi correlati