对编译时不知道的类型使用Skip/Take

本文关键字:Skip Take 类型 编译 不知道 | 更新日期: 2023-09-27 18:05:44

我想写一个函数,从数据库表逐页检索数据。这里的目标是节省内存。这是验证程序的一部分,我们将偶尔在我们的数据库上运行,以确保我们有一致的数据。表可能非常大,所以我不想为了进行验证而将整个表加载到内存中。

考虑到这一点,我写了这个函数:
static IEnumerable<T> RetreivePages<T>(IQueryable<T> query, int count, int pageSize)
{
    int pages = count / pageSize;
    if (count % pageSize > 0)
    {
        pages++;
    }
    for (int i = 0; i < pages; i++)
    {
        foreach (T item in query.Skip(i * pageSize).Take(pageSize))
        {
            yield return item;
        }
    }
} 

这里的想法是,我们一次只检索pageSize行,所以我们不会用表中的所有行填充内存。

不幸的是,这不起作用。query.Skip行抛出以下异常:

方法'Skip'仅支持LINQ to Entities中的排序输入。方法' order '必须在方法'Skip'之前调用。

还有其他方法可以完成我想要的吗?

链接为重复的问题的答案建议按列排序。.OrderBy不会在这里工作,因为T的属性在函数内部是未知的。

对编译时不知道的类型使用Skip/Take

你可以在你的方法中传递一个已经排序的查询,并将输入类型更改为IOrderedEnumerable<T>,或者在你的方法中传递选择器来排序,就像这样:

static IEnumerable<T> RetreivePages<T, U>(
    IQueryable<T> query, 
    Func<T, U> orderBy, //<--- Additional parameter
    int count, int pageSize)
{
    //Apply the ordering
    var orderedQuery = query.OrderBy(orderBy);
    int pages = count / pageSize;
    if (count % pageSize > 0)
    {
        pages++;
    }
    for (int i = 0; i < pages; i++)
    {
        //Use the new ordered version
        foreach (T item in orderedQuery.Skip(i * pageSize).Take(pageSize))
        {
            yield return item;
        }
    }
} 

然后这样命名:

var query = ...;
//Assuming your query object have a property called "ID":
var pagedQuery = RetrievePages(query, x => x.ID, 10, 100;

DavidG是对的,你必须以某种方式排序,所以让我们看看可以做些什么。

这个答案提供了一个很好的通用函数,可以按字符串名称排序:

public static class QueryHelper
{
    public static IQueryable<T> OrderByField<T>(this IQueryable<T> q, string sortField, bool ascending = true)
    {
        var param = Expression.Parameter(typeof(T), "p");
        var prop = Expression.Property(param, sortField);
        var exp = Expression.Lambda(prop, param);
        string method = ascending ? "OrderBy" : "OrderByDescending";
        Type[] types = { q.ElementType, exp.Body.Type };
        var mce = Expression.Call(typeof(Queryable), method, types, q.Expression, exp);
        return q.Provider.CreateQuery<T>(mce);
    }
}

现在排序在哪个字段上?通常情况下,表上有一个主键。我想你知道。在这种情况下,您可以检索键并对其进行排序。你的代码将变成:

public class DbHelper<T> where T : class
{
    private readonly string[] _keyNames;
    public DbHelper(DbContext context)
    {
        ObjectSet<T> objectSet = ((IObjectContextAdapter)context).ObjectContext.CreateObjectSet<T>();
        _keyNames = objectSet.EntitySet.ElementType.KeyMembers.Select(k => k.Name).ToArray();
    }
    public IEnumerable<T> RetreivePages(IQueryable<T> query, int count, int pageSize)
    {
        int pages = count / pageSize;
        if (count % pageSize > 0)
        {
            pages++;
        }
        for (int i = 0; i < pages; i++)
        {
            IQueryable<T> queryToRun = _keyNames.Aggregate(query, (current, keyName) => current.OrderByField(keyName));
            foreach (T item in queryToRun.Skip(i * pageSize).Take(pageSize))
            {
                yield return item;
            }
        }
    }
}

使用这种方法有很多注意事项。获取键的成本相当高,因此您肯定不希望为相同的类型参数值创建多个DbHelper实例。此外,动态构造这样的查询比手动排序要慢。

因此,我建议使用David的解决方案,而不是我的解决方案(坦率地说,这很简单,应该是显而易见的),但我仍然想在这里记录它,以防替代方案在不同的场景中有用。