实体框架核心2.2编译的查询结构参数在本地评估

时间:2019-02-27 16:09:40

标签: c# entity-framework ef-core-2.2 linq.compiledquery

我一直在研究使用Entity Framework Core的编译查询。我正在使用最新的稳定版本2.2.2。我正在阅读本文(https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/compiled-queries-linq-to-entities)以了解编译查询,并试图了解这是EF Core中的错误还是仅仅是他们尚未完成的事情。我知道该文章是针对EF6编写的,但我希望编译后的查询以相同的方式工作,并且找不到相反的地方。

这是我的DbContext设置和用于分页选项的简单结构:

public class BuggyDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
}

public struct PagingOptions
{
    public int Skip;
    public int Take;
}

[Table("User")]
public class User
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }
}

这是我编译的查询。第一个基于struct参数选择用户的“页面”(与文章中的示例非常相似)。第二个函数做同样的事情,但是接受“ skip”和“ take”作为基本的int32类型的单个参数。

var badQuery = EF.CompileQuery<BuggyDbContext, PagingOptions, IEnumerable<User>>((context, paging) =>
     context.Users
         .OrderBy(u => u.LastName)
         .Skip(paging.Skip)
         .Take(paging.Take));

var goodQuery = EF.CompileQuery<BuggyDbContext, int,int, IEnumerable<User>>((context, skip, take) =>
     context.Users
         .OrderBy(u => u.LastName)
         .Skip(skip)
         .Take(take));

这是演示问题的用法:

 using (var db = new BuggyDbContext())
 {

     var pagingOptions = new PagingOptions {
         Skip = 0,
         Take = 25
     };
     var firstPage = badQuery.Invoke(db, pagingOptions).ToList();
     var alternateFirstPage = goodQuery.Invoke(db, pagingOptions.Skip, pagingOptions.Take).ToList();
 }

goodQuery 运行时,一切正常。以下内容在日志中显示为期望的生成SQL:

SELECT [u].[UserId], [u].[FirstName], [u].[LastName]
FROM [User] AS [u]
ORDER BY [u].[LastName]
OFFSET @__skip ROWS FETCH NEXT @__take ROWS ONLY

但是,当 badQuery 运行时,它会选择所有记录,然后评估“跳过并接受”内存,这将导致糟糕的性能。

SELECT [u].[UserId], [u].[FirstName], [u].[LastName]
FROM [User] AS [u]
ORDER BY [u].[LastName]

warn: Microsoft.EntityFrameworkCore.Query[20500]
  => Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor
  The LINQ expression 'Skip(__paging.Skip)' could not be translated 
and will be evaluated locally.
warn: Microsoft.EntityFrameworkCore.Query[20500]
  => Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor
  The LINQ expression 'Take(__paging.Take)' could not be translated 
and will be evaluated locally.

出于两个非常重要的原因,我宁愿使用复杂类型(引用或值结构,无关紧要)作为已编译查询的参数:

  1. Lambda函数具有最大数量的输入参数。如果我的查询需要进行复杂的过滤,排序和分组,而这些查询需要多个输入,那么我将不得不采用其他方法。
  2. 对于正在调用查询的开发人员,输入参数更加清晰。即使在此示例中,调用查询的开发人员也将开始键入query.invoke,然后盯着intellisense中的2个未命名的整数参数。他们唯一了解他们的意思的方法是查看查询。如果输入参数更改顺序或含义,对查询的更改将非常危险。

EF Core 3.0路线图(https://docs.microsoft.com/en-us/ef/core/what-is-new/roadmap)确实表示,他们通常在使用LINQ查询策略(以避免此类可怕的运行查询,或者至少在运行时或在运行中捕获警告时提醒您)您的日志),但我希望struct参数可以正常工作。

如果我做错了什么,或者这是可行的,这里的任何人都有见识?您是否也认为这是一个错误?

2 个答案:

答案 0 :(得分:1)

我在https://github.com/aspnet/EntityFrameworkCore/issues/14857向EF团队提交了一个错误报告,该报告已关闭并标记为https://github.com/aspnet/EntityFrameworkCore/issues/13976的副本

已移至积压。响应如下:“基于正常分类,此功能有一个合理的解决方法,并且我们尚未看到明显的需求,因此我们现在将其移至积压中。” >

答案 1 :(得分:0)

我认为这是由于表达式的存储和评估方式所致。这绝对是一个错误,但是关于将来是否会解决此问题,我不确定。

我在项目上设置分页的方法是使用通用类,该类最终根据值类型构建表达式。 (请注意,有一些未使用的属性和字段,因为我遗漏了一些特定于域的代码)

public class Pagination<T>
{
    public IQueryable<T> Items;

    public int CurrentPageNumber { get; }

    public int PageSize { get; }

    public int StartPage { get; }

    public int TotalPages { get; set; }

    public Pagination(IQueryable<T> items, int pageNumber, int pageSize)
    {
        if (pageNumber <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(pageNumber));
        }

        if (pageSize <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(pageSize));
        }

        if (((decimal)DisplayPages % 2) == 0)
        {
            throw new ArgumentOutOfRangeException(nameof(DisplayPages), "Number of pages to render must be odd.");
        }

        Items = items;
        CurrentPageNumber = pageNumber;
        PageSize = pageSize;
        StartPage = 1;

        if (items.Any())
        {
            var rowCount = items.Count();
            TotalPages = (int)Math.Ceiling((decimal)rowCount / PageSize);
        }
        else
        {
            TotalPages = 1;
        }

    }

    public IQueryable<T> GetPageData()
    {
        return Items.Skip((CurrentPageNumber - 1) * PageSize).Take(PageSize) ?? new List<T>().AsQueryable();
    }

}

然后您可以像这样使用它:

var paginatedObjects = new Pagination<Type>(query, 1, 10)
{
    //Options if nessasary
};
paginatedObjects.GetPageData();
相关问题