Entity Framework 核心 2.2 编译查询结构参数在本地求值
Entity Framework Core 2.2 Compiled Query Struct Parameter Evaluating Locally
我一直在研究使用 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; }
}
这是我编译的查询。第一个 select 是基于结构参数的 "page" 用户(与文章中的示例非常相似)。第二个做同样的事情,但接受 "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 所有记录然后评估内存中的 Skip 和 Take,这将导致可怕的性能。
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.
出于几个非常重要的原因,我更愿意使用复杂类型(引用或值结构,不关心)作为编译查询的参数:
- Lambda 函数具有最大数量的输入参数。如果我有一个需要多个输入的复杂过滤、排序和分组的查询,我将被迫走其他路线。
- 输入参数对于调用查询的开发人员来说更加清晰。即使在此示例中,调用查询的开发人员也会开始键入 query.Invoke,然后在智能感知中盯着 2 个未命名的整数参数。他们知道他们的意思的唯一方法是查看查询。如果输入参数改变了顺序或含义,对查询的更改将是极其危险的。
EF Core 3.0 路线图 (https://docs.microsoft.com/en-us/ef/core/what-is-new/roadmap) 确实表示他们正在研究一般的 LINQ 查询策略(以避免这种可怕的 运行 查询,或者至少让您在运行时或碰巧在你的日志中发现了一个警告),但我希望结构参数能够工作。
如果我做错了什么或者这是正在进行中的事情,有人在这里有任何见解吗?您是否也认为这是一个错误?
我认为这是由于表达式的存储和计算方式所致。这肯定是个bug,至于以后会不会修复我就不知道了。
我在我的项目中设置分页的方式是使用一个通用的 class,它最终从值类型构建一个表达式。 (请注意,有一些未使用的属性和字段,因为我在逻辑之外留下了一些特定于域的代码)
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();
我在 https://github.com/aspnet/EntityFrameworkCore/issues/14857 It was closed and marked as duplicate of https://github.com/aspnet/EntityFrameworkCore/issues/13976
向 EF 团队提交了错误报告
已移至积压。这是回复:"Based on normal triage, this is a feature for which there is a reasonable workaround and for which we have not, as yet, seen significant demand, so we're moving it to the backlog for now."
我一直在研究使用 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; }
}
这是我编译的查询。第一个 select 是基于结构参数的 "page" 用户(与文章中的示例非常相似)。第二个做同样的事情,但接受 "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 所有记录然后评估内存中的 Skip 和 Take,这将导致可怕的性能。
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.
出于几个非常重要的原因,我更愿意使用复杂类型(引用或值结构,不关心)作为编译查询的参数:
- Lambda 函数具有最大数量的输入参数。如果我有一个需要多个输入的复杂过滤、排序和分组的查询,我将被迫走其他路线。
- 输入参数对于调用查询的开发人员来说更加清晰。即使在此示例中,调用查询的开发人员也会开始键入 query.Invoke,然后在智能感知中盯着 2 个未命名的整数参数。他们知道他们的意思的唯一方法是查看查询。如果输入参数改变了顺序或含义,对查询的更改将是极其危险的。
EF Core 3.0 路线图 (https://docs.microsoft.com/en-us/ef/core/what-is-new/roadmap) 确实表示他们正在研究一般的 LINQ 查询策略(以避免这种可怕的 运行 查询,或者至少让您在运行时或碰巧在你的日志中发现了一个警告),但我希望结构参数能够工作。
如果我做错了什么或者这是正在进行中的事情,有人在这里有任何见解吗?您是否也认为这是一个错误?
我认为这是由于表达式的存储和计算方式所致。这肯定是个bug,至于以后会不会修复我就不知道了。
我在我的项目中设置分页的方式是使用一个通用的 class,它最终从值类型构建一个表达式。 (请注意,有一些未使用的属性和字段,因为我在逻辑之外留下了一些特定于域的代码)
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();
我在 https://github.com/aspnet/EntityFrameworkCore/issues/14857 It was closed and marked as duplicate of https://github.com/aspnet/EntityFrameworkCore/issues/13976
向 EF 团队提交了错误报告已移至积压。这是回复:"Based on normal triage, this is a feature for which there is a reasonable workaround and for which we have not, as yet, seen significant demand, so we're moving it to the backlog for now."