Запрос EF не выполняется без Convert.ToString()

У меня есть запрос, похожий на следующий.

var adjustments = DbContext.StorageAdjustments
    .Where(a => a.StorageId == id && a.TimeStamp >= start)
    .Select(a => new StorageUpdateModel
    {
        // Maps properties from the StorageAdjustments table to the StorageUpdateModel.
        TimeStamp = a.TimeStamp,
        Change = a.Quantity,
        Product = a.Product.Name,
        UpdateType = StorageUpdateType.Adjustment,
        UpdateData = Convert.ToString(a.UserName),
        UpdateData2 =Convert.ToString(a.Comments),
        Id = 0,
    });

var trucks = DbContext.StorageTrucks
    .Where(t => t.StorageId == id && t.TimeStamp >= start)
    .Select(t => new StorageUpdateModel
    {
        // Maps properties from the StorageTrucks table to the StorageUpdateModel.
        TimeStamp = t.TimeStamp,
        Change = -t.Quantity,
        Product = "",
        UpdateType = StorageUpdateType.Truck,
        UpdateData = (t.Truck.BillOfLading.HasValue && t.Truck.BillOfLading.Value>0 ? Convert.ToString(t.Truck.BillOfLading.Value) :""),
        UpdateData2 = (t.Truck.ManualEntry ? "True" : "False"),
        Id = t.TruckId,
    });

var railcars = DbContext.StorageRailcars
    .Where(r => r.StorageId == id && r.TimeStamp >= start)
    .Select(r => new StorageUpdateModel
    {
        // Maps properties from the StorageRailcars table to the StorageUpdateModel.
        TimeStamp = r.TimeStamp,
        Change = -r.Quantity,
        Product = "",
        UpdateType = StorageUpdateType.Railcar,
        UpdateData = ((r.Railcar!=null && r.Railcar.RailcarNumber!=null)? Convert.ToString(r.Railcar.RailcarNumber) : ""),
        UpdateData2 = "",
        Id = r.RailcarId,
    });

// Union all three queries into a single query.
var query = adjustments
    .Union(trucks)
    .Union(railcars)
    .OrderByDescending(u => u.TimeStamp)
    .AsQueryable();

var results = await query.ToListAsync();

При его выполнении генерируется исключение.

System.InvalidOperationException
  HResult=0x80131509
  Message=Unable to translate set operation when matching columns on both sides have different store types.
  Source=Microsoft.EntityFrameworkCore.Relational
  StackTrace:
   at Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression.ApplySetOperation(SetOperationType setOperationType, SelectExpression select2, Boolean distinct)
   at Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression.ApplyUnion(SelectExpression source2, Boolean distinct)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateUnion(ShapedQueryExpression source1, ShapedQueryExpression source2)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.CountAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at RailtraxCore.Pager.<ApplyFilterAsync>d__23`1.MoveNext() in D:\Users\jwood\source\repos\Railtrax\RailtraxCore\Pager.cs:line 117

Но если я изменю строку:

Product = a.Product.Name,

К:

Product = Convert.ToString(a.Product.Name),

Тогда исключение не выдается!

a.Product.Name уже является строкой. Может ли кто-нибудь предложить какие-либо идеи относительно того, как Convert.ToString() имеет значение?

Примечание. Типы ссылок, допускающие значение NULL, на этой странице отключены. И StorageUpdateModel.Product, и a.Product.Name относятся к типу string (не string?). a.Product.Name — обязательный столбец. И я использую EntityFrameworkCore 7.0.3.

Почему вы используете его для a.UserName и a.Comments или это одна и та же проблема с разными столбцами? И каков точный тип этих столбцов в БД?

juharr 29.03.2023 22:15

@juharr: Да, та же проблема. Они обе струнные. Стараюсь не усложнять, просто сосредоточившись на a.Product.Name.

Jonathan Wood 29.03.2023 22:24

C# — это язык типов. Вы не предоставляете определение класса для StorageUpdateModel.Product или a.Product.Name. Они одинаковы? Вы используете обнуляемость? Является ли a.Product.Name обнуляемым? Какой LINQ вы на самом деле используете - LINQ to Entity Framework и LINQ to Entity Framework Core очень разные, и им также нужна версия.

NetMage 29.03.2023 22:35

@NetMage: здесь отключены ссылочные типы, допускающие значение NULL. И StorageUpdateModel.Product, и a.Product.Name относятся к типу string (не string?). a.Product.Name — обязательный столбец. Использование EntityFrameworkCore 7.0.3.

Jonathan Wood 29.03.2023 22:39

Похоже, вы столкнулись с недостатком дизайна в EF Core: он будет исправлен в EF Core 8 (см. #19129 ), но я не уверен, что он будет исправлен в EF Core 7. Было принято окончательное решение. чтобы не проверять на стороне EF Core и позволить SQL беспокоиться об этом.

NetMage 29.03.2023 22:55

@NetMage: Да, но я до сих пор не совсем понимаю, что делает Convert.ToString(). Я погуглил и нашел проблему на GitHub. Но я не могу объяснить, как Convert.ToString() решает эту проблему.

Jonathan Wood 29.03.2023 22:57

Поскольку вы выполняете Union, EF Core не заботится о StorageUpdateModel и его типах — он будет работать с типами, назначенными каждому свойству. Текущая реализация операций над наборами в EF Core проверяет соответствие типов SQL, поэтому тип a.Product.Name в базе данных предположительно похож на nvarchar(30), а тип "" — на nvarchar(max), поэтому они не совпадают, и возникает (неверное) исключение. Convert.ToString преобразует подразумеваемый тип SQL в nvarchar(max), и теперь они совпадают.

NetMage 29.03.2023 23:04

@NetMage: Да, это звучит правильно. Погуглив, я смог найти SqlFunctions.StringConvert(), но ни слова о том, как Convert.ToString() обрабатывается EF. Но кажется довольно очевидным, что EF понимает и переводит Convert.ToString().

Jonathan Wood 29.03.2023 23:07

Примечание для следующего раза ... Если вы погуглите и найдете материал, который кажется близким и, возможно, актуальным, включите его в свой вопрос (резюме и ссылка, вероятно, будут достаточно хорошими). Нет смысла, когда куча людей выдает одну и ту же информацию.

Flydog57 29.03.2023 23:15
stackoverflow.com/q/66789248/861716
Gert Arnold 29.03.2023 23:17

+1 к нулевой способности, хотя из памяти EF обычно справляется с этим лучше, но бывают ли случаи, когда продукт для данной строки равен #null ?? Вы можете попробовать что-то вроде этого, чтобы определить, есть ли странная проблема с обработкой нулей: Product = a.Product?.Name ?? "". EF 6 до того, как ссылочные типы, допускающие значение null, просто возвращали бы #null, если продукт не был найден или не имел имени. Сейчас /w EF Core 7?? Честно говоря, ссылочные типы, допускающие значение Null, обязательно будут проблемой в будущем, поскольку, хотя ваш код может простить их, детали реализации в EF и других библиотеках могут быть немного более педантичными.

Steve Py 29.03.2023 23:23

К сожалению, в EF Core есть сопоставления функций для конкретного поставщика без обязательного минимального набора переведенных методов, но если вы используете SQL Server, вот задокументированные сопоставления функций для поставщика SQL Server. Вы можете видеть, что Convert.ToString приводит к nvarchar(max), что совместимо со строковыми литералами. Если бы был способ привести строковый литерал к определенному nvarchar(n), чтобы соответствовать пониманию EF Core определения столбца базы данных, это сработало бы, но я не уверен, как это сделать.

NetMage 29.03.2023 23:52

@NetMage: Это отличная находка. Спасибо! Если вам захочется написать это как ответ, я приму это.

Jonathan Wood 30.03.2023 17:43
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
13
121
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема в том, что Entity Framework гарантирует, что все столбцы совпадают при выполнении Union()s. Это включает даже максимальную длину строк (например, NVARCHAR(50)).

Это причина исключения Unable to translate set operation when matching columns on both sides have different store types. здесь.

Однако Convert.ToString() преобразует строку в NVARCHAR(MAX) (как описано здесь). В результате Entity Framework считает столбцы совместимыми.

Что менее ясно, так это то, почему Entity Framework блокирует первый случай, когда очевидно, что это можно заставить работать.

Вывод неверный. Сам SQL требует, чтобы совпадающие столбцы в UNION имели один и тот же тип и либо преобразовывали их неявно, либо через ошибку. Такие запросы обычно являются признаком ошибок дизайна (почему для одного и того же объекта использовались разные типы?) или вызывают ошибки, поскольку числа или даты могут быть преобразованы в текст с использованием неправильного формата.

Panagiotis Kanavos 04.04.2023 08:56

В этом конкретном случае, похоже, есть несколько проблем с дизайном. Неоднозначные свойства UpdateData и UpdateData2 содержат совершенно разные вещи. Это проблема ООП. Затем запрос LINQ пытается преобразовать два разных использования в запрос SQL. Первый запрос пытается загрузить коносамент, второй — номер вагона, когда поля таблицы имеют разные типы. Было бы лучше иметь отдельные запросы, загружающие только то, что необходимо, и сопоставлять результаты с StorageUpdateModel, используя, например, Automapper или Mapperly.

Panagiotis Kanavos 04.04.2023 09:01

@PanagiotisKanavos: кто-то в Microsoft сказал мне, что ограничением является Entity Framework. Это известная проблема, на которую многие жаловались. UpdateData и UpdateData2 — обе строки. И мы знаем, что их можно сделать совместимыми, потому что я сделал их совместимыми с Convert.ToString().

Jonathan Wood 04.04.2023 19:35

Другие вопросы по теме