c# - una - Expresión de LINQ para devolver el valor de la propiedad?
expresiones lambda java (5)
He encontrado una manera de dividir la consulta en partes, es decir, le das 4000 valores, por lo que podría hacer 4 solicitudes de 1000 cada una; con el ejemplo completo de Northwind. Tenga en cuenta que esto podría no funcionar en Entity Framework, debido a Expression.Invoke
, pero está bien en 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();
}
}
}
}
Intento crear una función genérica que me ayude a seleccionar miles de registros usando LINQ to SQL desde una lista local. SQL Server (al menos en 2005) limita las consultas a 2100 parámetros y me gustaría seleccionar más registros que eso.
Aquí hay un buen ejemplo de uso:
var some_product_numbers = new int[] { 1,2,3 ... 9999 };
Products.SelectByParameterList(some_product_numbers, p => p.ProductNumber);
Aquí está mi implementación (no funcional):
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;
}
He visto muchos ejemplos de creación de predicados utilizando expresiones. En este caso, solo quiero ejecutar el delegado para devolver el valor del ProductNumber actual. O más bien, quiero traducir esto en la consulta SQL (funciona bien en forma no genérica).
Sé que compilar Expression solo me lleva al punto de partida (pasar el delegado como Func), pero no estoy seguro de cómo pasar un parámetro a una expresión "sin compilar".
¡Gracias por tu ayuda!
**** EDIT: ** Permítanme aclarar más:
Aquí hay un ejemplo de trabajo de lo que quiero generalizar:
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()
;
Resultados en este código 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)
LINQ-to-SQL todavía funciona a través de parámetros SQL estándar, por lo que escribir una expresión elegante no va a ayudar. Hay 3 opciones comunes aquí:
- empacar los identificadores en (por ejemplo) csv / tsv; pasar como
varchar(max)
y usar un udf para dividirlo (en el servidor) en una variable de tabla; unirse a la variable de tabla - utilice un parámetro con valores de tabla en SQL Server 2008
- tener una tabla en el servidor en la que pueda insertar los identificadores (quizás a través de SqlBulkCopy) (quizás con una "guía de sesión" o similar); únete a esta mesa
El primero es el más simple; obtener un "split csv udf" es trivial (solo búscalo). Arrastre el udf al contexto de datos y consuma desde allí.
La forma más sencilla de hacerlo: utilice LINQKit ( LINQKit gratuita y no restrictiva)
Versión de trabajo del código:
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;
}
Ejemplo de uso:
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
¡Gracias, de nuevo, por toda su ayuda!
Pase IQuerable
a la función Contains
lugar de lista o matriz. por favor mira el ejemplo de abajo
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();
cuando pasa la lista o matriz, se pasa en forma de parámetros y excede los recuentos cuando los elementos de la lista cuentan más que 2100.
Puede crear su propio 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();
}
}