简体   繁体   English

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

[英]Invalid column name when using EF Core filtered includes

I came across this error when modifying a DB first project (using fluent migrator) and scaffolding the EF context to generate models.我在修改 DB 第一个项目(使用 fluent migrator)并搭建 EF 上下文以生成模型时遇到了这个错误。 I have reproduced it by making a code-first simplification.我通过代码优先的简化复制了它。 This means that I can't accept answers that suggest modifying the annotations or fluent configuration , because this will be deleted and recreated on the next migration and scaffold.这意味着我不能接受建议修改注释或 fluent configuration 的答案,因为这将在下一次迁移和脚手架中被删除和重新创建。

The simplified idea is that a device has:简化的想法是设备具有:

  • many attributes许多属性
  • many histories representing changes to the device over time代表设备随时间变化的许多历史记录
    • each history entry has an optional location每个历史条目都有一个可选的位置

IOW you can move a device around to locations (or no location) and keep track of that over time. IOW 您可以将设备四处移动到某个位置(或没有位置),并随着时间的推移进行跟踪。 在此处输入图片说明

The code-first model I came up with to simulate this is as follows:我想出的用于模拟这个的代码优先模型如下:

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>();
}

Running the following query to select all devices works fine.运行以下查询以选择所有设备工作正常。 I'm using a filtered include to only select the most recent history for this "view":我正在使用过滤的包含来仅为此“视图”选择最近的历史记录:

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();

that works fine, however when I try and select only one device by ID using the same includes:效果很好,但是当我尝试使用相同的 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();

I get the following error:我收到以下错误:

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

I can't figure out why this is working for a number of rows but not working for a single row.我无法弄清楚为什么这适用于多行但不适用于单行。

For completeness, ToModel() is just an extension method to return a POCO.为了完整ToModel()ToModel()只是一个返回 POCO 的扩展方法。

I'm not even sure where to start looking, ideas welcome!我什至不知道从哪里开始寻找,欢迎提出想法!

Looks like you have hit EF Core query translation bug, so I would suggest to seek/report it to EF Core GitHub issue tracker.看起来您遇到了 EF Core 查询翻译错误,因此我建议向 EF Core GitHub 问题跟踪器寻求/报告它。

From what I can tell, it's caused by "pushing down" the root query as subquery because of the Take operator (which is basically what First method is using in the second case).据我所知,这是由于Take运算符(基本上是First方法在第二种情况下使用的)而将根查询作为子查询“下推”引起的。 This somehow messes up the generated subquery aliases and leads to invalid SQL.这以某种方式弄乱了生成的子查询别名并导致无效的 SQL。

It can be seen by comparing the generated SQL for the first query通过比较第一次查询生成的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]

and for the second (or just inserting .Where(d => d.DeviceId == deviceId).Take(1) before Select in the first):对于第二个(或者只是在第一个Select之前插入.Where(d => 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]

Note the usage of [t] in the first SELECT [t].[HistoryId]... inside the OUTER APPLY , which in the fist query is alias to the inner Histories subquery in FROM clause, while in second it is alias to the outer Devices subquery, which of couse have no columns mentioned in the error message.请注意[t]OUTER APPLY中的第一个SELECT [t].[HistoryId]...的用法,它在第一个查询中是FROM子句中内部Histories子查询的别名,而在第二个查询中它是外部Devices子查询,其中当然没有错误消息中提到的列。 Apparently in the second case [t0] should have been used.显然在第二种情况下应该使用[t0]

Since it is a bug, you have to wait it to be fixed.由于它是一个错误,您必须等待它被修复。 Until then, the workaround I could suggest is to explicitly execute row limiting operator ( First ) outside of the EF Core query context, eg在那之前,我建议的解决方法是在 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();

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM