使用 EF Core 过滤包含时的列名无效

Posted

技术标签:

【中文标题】使用 EF Core 过滤包含时的列名无效【英文标题】:Invalid column name when using EF Core filtered includes 【发布时间】:2021-12-21 02:18:21 【问题描述】:

我在修改 DB first 项目(使用 fluent 迁移器)和搭建 EF 上下文以生成模型时遇到了这个错误。我通过进行代码优先的简化来复制它。这意味着我不能接受建议修改注释或流畅配置的答案,因为这将在下一次迁移和脚手架时被删除并重新创建。

简化的想法是设备具有:

许多属性 表示设备随时间变化的许多历史记录 每个历史记录条目都有一个可选位置

IOW,您可以将设备移动到某个位置(或没有位置)并随着时间的推移对其进行跟踪。

我想出的代码优先模型如下:

public class ApiContext : DbContext

    public ApiContext(DbContextOptions<ApiContext> options) : base(options)  

    public DbSet<Device> Devices  get; set; 
    public DbSet<History> Histories  get; set; 
    public DbSet<Location> Locations  get; set; 


public class Device

    public int DeviceId  get; set; 
    public string DeviceName  get; set; 

    public List<History> Histories  get;  = new List<History>();
    public List<Attribute> Attributes  get;  = new List<Attribute>();


public class History

    public int HistoryId  get; set; 
    public DateTime DateFrom  get; set; 
    public string State  get; set; 

    public int DeviceId  get; set; 
    public Device Device  get; set; 

    public int? LocationId  get; set; 
    public Location Location  get; set; 


public class Attribute

    public int AttributeId  get; set; 
    public string Name  get; set; 

    public int DeviceId  get; set; 
    public Device Device  get; set; 


public class Location

    public int LocationId  get; set; 
    public string LocationName  get; set; 

    public List<History> Histories  get;  = new List<History>();

运行以下查询以选择所有设备可以正常工作。我使用filtered include 仅选择此“视图”的最新历史记录:

var devices = _apiContext.Devices.AsNoTracking()
    .Include(d => d.Histories.OrderByDescending(h => h.DateFrom).Take(1))
    .ThenInclude(h => h.Location)
    .Include(d => d.Attributes)
    .Select(d => d.ToModel()).ToList();

效果很好,但是当我尝试使用相同的 ID 仅选择一个设备时:

var device = _apiContext.Devices.AsNoTracking()
    .Include(d => d.Histories.OrderByDescending(h => h.DateFrom).Take(1))
    .ThenInclude(h => h.Location)
    .Include(d => d.Attributes)
    .First(d => d.DeviceId == deviceId)
    .ToModel();

我收到以下错误:

Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): Invalid column name 'LocationId'.
Invalid column name 'HistoryId'.
Invalid column name 'DateFrom'.
Invalid column name 'LocationId'.
Invalid column name 'State'.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
   at Microsoft.Data.SqlClient.SqlDataReader.get_MetaData()
   at Microsoft.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString, Boolean isInternal, Boolean forDescribeParameterEncryption, Boolean shouldCacheForAlwaysEncrypted)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean isAsync, Int32 timeout, Task& task, Boolean asyncWrite, Boolean inRetry, SqlDataReader ds, Boolean describeParameterEncryptionRequest)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String method)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at System.Data.Common.DbCommand.ExecuteReader()
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.InitializeReader(DbContext _, Boolean result)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.First[TSource](IQueryable`1 source, Expression`1 predicate)
   at efcore_test.App.PrintSingleDevice(Int32 deviceId) in C:\Users\Iain\projects\efcore-5-bug\efcore-test\App.cs:line 44
   at efcore_test.Program.<>c__DisplayClass1_0.<Main>b__4(App app) in C:\Users\Iain\projects\efcore-5-bug\efcore-test\Program.cs:line 28
   at efcore_test.Program.RunInScope(IServiceProvider serviceProvider, Action`1 method) in C:\Users\Iain\projects\efcore-5-bug\efcore-test\Program.cs:line 35
   at efcore_test.Program.Main(String[] args) in C:\Users\Iain\projects\efcore-5-bug\efcore-test\Program.cs:line 28
ClientConnectionId:1418edb2-0889-4f4d-9554-85344c9a35a9
Error Number:207,State:1,Class:16

我不明白为什么这适用于多行但不适用于单行。

为了完整起见,ToModel() 只是返回 POCO 的扩展方法。

我什至不知道从哪里开始寻找,欢迎提出想法!

编辑

错误报告:https://github.com/dotnet/efcore/issues/26585 repro:https://github.com/thinkOfaNumber/efcore-5-test

【问题讨论】:

不同之处在于在第一个查询中忽略了包含,因为ToModel() 是直接在忽略包含的IQueryable 上的投影。第二个查询执行IQueryable,然后在内存中进行投影。也就是说,这可能是一个错误。在 EF-core 5 中,我可以毫无问题地执行包含 IncludeOrderByDescendingTake(1) 的更简单查询。如果您尝试使用每个查询中唯一的一个 Includes 进行查询,该怎么办?可能是映射错误。 您首先要检查的是第一个查询是否真的可以在没有 .Select(d =&gt; d.ToModel()) 的情况下工作。 哦,我所说的忽略Includes 不是真的。 ToModel 可能是 AutoMapper 的 ProjectTo 之类的扩展方法,但它是表达式内部的方法 (.Select(d =&gt; d.ToModel())),因此 EF 确实执行了整个查询,然后执行投影客户端,因为它在最后选择。事实上,这回答了 Ivan 的建议:它不起作用。看看哪些包含起作用和不起作用会很有趣。 【参考方案1】:

更新:该错误已在 EF Core 6.0 中修复,因此下一个仅适用于 EF Core 5.0。

看起来您遇到了 EF Core 5.0 查询翻译错误,所以我建议您寻找/报告它到 EF Core GitHub 问题跟踪器。

据我所知,这是由于Take 运算符(基本上是First 方法在第二种情况下使用的)将根查询“下推”为子查询引起的。这会以某种方式混淆生成的子查询别名并导致无效的 SQL。

对比第一次查询生成的SQL可以看出

SELECT [d].[DeviceId], [d].[DeviceName], [t0].[HistoryId], [t0].[DateFrom], [t0].[DeviceId], [t0].[LocationId], [t0].[State], [t0].[LocationId0], [t0].[LocationName], [a].[AttributeId], [a].[DeviceId], [a].[Name]
FROM [Devices] AS [d]
OUTER APPLY (
    SELECT [t].[HistoryId], [t].[DateFrom], [t].[DeviceId], [t].[LocationId], [t].[State], [l].[LocationId] AS [LocationId0], [l].[LocationName]
    FROM (
        SELECT TOP(1) [h].[HistoryId], [h].[DateFrom], [h].[DeviceId], [h].[LocationId], [h].[State]
        FROM [Histories] AS [h]
        WHERE [d].[DeviceId] = [h].[DeviceId]
        ORDER BY [h].[DateFrom] DESC
    ) AS [t]
    LEFT JOIN [Locations] AS [l] ON [t].[LocationId] = [l].[LocationId]
) AS [t0]
LEFT JOIN [Attribute] AS [a] ON [d].[DeviceId] = [a].[DeviceId]
ORDER BY [d].[DeviceId], [t0].[DateFrom] DESC, [t0].[HistoryId], [t0].[LocationId0], [a].[AttributeId]

对于第二个(或者只是在第一个 Select 之前插入 .Where(d =&gt; d.DeviceId == deviceId).Take(1)):

SELECT [t].[DeviceId], [t].[DeviceName], [t1].[HistoryId], [t1].[DateFrom], [t1].[DeviceId], [t1].[LocationId], [t1].[State], [t1].[LocationId0], [t1].[LocationName], [a].[AttributeId], [a].[DeviceId], [a].[Name]
FROM (
    SELECT TOP(1) [d].[DeviceId], [d].[DeviceName]
    FROM [Devices] AS [d]
    WHERE [d].[DeviceId] = @__deviceId_0
) AS [t]
OUTER APPLY (
    SELECT [t].[HistoryId], [t].[DateFrom], [t].[DeviceId], [t].[LocationId], [t].[State], [l].[LocationId] AS [LocationId0], [l].[LocationName]
    FROM (
        SELECT TOP(1) [h].[HistoryId], [h].[DateFrom], [h].[DeviceId], [h].[LocationId], [h].[State]
        FROM [Histories] AS [h]
        WHERE [t].[DeviceId] = [h].[DeviceId]
        ORDER BY [h].[DateFrom] DESC
    ) AS [t0]
    LEFT JOIN [Locations] AS [l] ON [t].[LocationId] = [l].[LocationId]
) AS [t1]
LEFT JOIN [Attribute] AS [a] ON [t].[DeviceId] = [a].[DeviceId]
ORDER BY [t].[DeviceId], [t1].[DateFrom] DESC, [t1].[HistoryId], [t1].[LocationId0], [a].[AttributeId]

注意[t]OUTER APPLY 内的第一个SELECT [t].[HistoryId]... 中的用法,在第一个查询中是FROM 子句中的内部Histories 子查询的别名,而在第二个查询中它是外部Devices 子查询,其中当然没有错误消息中提到的列。显然在第二种情况下应该使用[t0]

由于这是一个错误,您必须等待它被修复。在那之前,我可以建议的解决方法是在 EF Core 查询上下文之外显式执行行限制运算符 (First),例如

var device = _apiContext.Devices.AsNoTracking()
    .Include(d => d.Histories.OrderByDescending(h => h.DateFrom).Take(1))
    .ThenInclude(h => h.Location)
    .Include(d => d.Attributes)
    .Where(d => d.DeviceId == deviceId) // instead of .First(d => d.DeviceId == deviceId)
    .AsEnumerable() // switch to client evaluation (LINQ to Objects context)
    .First() // and execute `First` here
    .ToModel();

【讨论】:

感谢您的详细回答,但该解决方法不会先将所有内容加载到内存中吗?我不能这样做,因为表太大,所以我可能需要尝试几个版本,看看是否所有 v5 都存在这个错误(我刚刚从 ef-core 3 升级到使用过滤包含!)。现在已经很晚了,所以我明天会去问题跟踪器。谢谢! 是的,它会加载,但不是全部。请注意,Where 过滤器仍应用于服务器端(在使用 AsEnumerable() 切换到客户端评估之前),因此只会加载与过滤器匹配的项目。对于问题中的 PK 值过滤器,哪个是 0 或 1 项 - 与直接执行 First 完全相同。如果您想将分页应用于第一个查询(在ToList() 之前),这将是一个问题,因为Take 必须在AsEnumerable 之后才能解决当前的 EF Core 错误。 到什么时候推出的,我也说不准,但我在最新的官方EF Core 5.0.11中复制了它,所以即使在即将到来的EFC 6.0中也有可能出现 幸运的是,我今天遇到了类似的问题,但我无法确定究竟是什么原因造成的:它在实时 API 中正常工作,在单元测试中正常工作API,但在仅针对查询的隔离单元测试中失败。奇怪的是,如果我查看查询的“调试”视图,它生成的 SQL 是正确的,但是在您执行查询的那一刻(例如 First()),它最终会变成别的东西 @John FirstFirstOrDefaultSingle 和类似的 final 查询运算符 - 它们在执行之前修改生成的查询。基本上在末尾附加Take(1)Take 是在这种情况下导致无效 SQL 生成的运算符。

以上是关于使用 EF Core 过滤包含时的列名无效的主要内容,如果未能解决你的问题,请参考以下文章

EF Core 数据过滤

SS OrmLite:加入和过滤列名时的列名不明确

EF Core SQL 过滤器翻译

EF Core 如何向 .ThenIncludes 添加过滤器

如何过滤 EF Core 中的多对多联接

EF Core 中实现 动态数据过滤器