Как проанализировать int в запросе EF Core 3?

После обновления до EF Core 3 я получаю следующую ошибку в следующем коде:

System.InvalidOperationException: 'Выражение LINQ' DbSet .Max(c => Convert.ToInt32(c.ClaimNumber.Substring(c.ClaimNumber.Length - 6)))' не может быть переведено. Либо перепишите запрос в форме, которую можно перевести, либо явно переключитесь на оценку клиента, вставив вызов AsEnumerable(), AsAsyncEnumerable(), ToList() или ToListAsync(). См. https://go.microsoft.com/fwlink/?linkid=2101038. Чтобы получить больше информации.'

var maxId = Db.Claims
    .Select(c => c.ClaimNumber.Substring(c.ClaimNumber.Length - 6))
    .Max(x => Convert.ToInt32(x));

Я также пытался использовать int.Parse вместо Convert.ToInt32, и он выдает ту же ошибку. Я понимаю сообщение об ошибке. Тем не менее, заставить SQL Server анализировать строку в int в T-SQL с помощью CAST или CONVERT очень просто, я надеюсь, что есть простой способ написать запрос, чтобы он транслировался в операцию на стороне сервера, верно?

ОБНОВЛЕНИЕ После отличного ответа Клаудио я подумал, что должен добавить некоторую информацию для следующего человека, который придет. Причина, по которой я считал, что синтаксический анализ был проблемой с приведенным выше кодом, заключается в том, что следующее выполняется без ошибок и дает правильный результат:

var maxId = Db.Claims
    .Select(c => c.ClaimNumber.Substring(c.ClaimNumber.Length - 6))
    .AsEnumerable()
    .Max(x => int.Parse(x));

Однако я копнул глубже и обнаружил, что это SQL-запрос, который EF выполняет из этого кода:

SELECT [c].[ClaimNumber], CAST(LEN([c].[ClaimNumber]) AS int) - 6
FROM [Claims] AS [c]
WHERE [c].[ClaimNumber] IS NOT NULL

Это явно не делает ничего подобного тому, что я хотел, и поэтому Клаудио прав в том, что вызов Substring на самом деле является проблемой.


person pbarranis    schedule 02.03.2020    source источник


Ответы (1)


Отказ от ответственности: несмотря на то, что это возможно, я настоятельно рекомендую вам не использовать преобразование типов в вашем запросе, так как это приводит к значительному снижению производительности запроса.

Дело в том, что Convert.ToInt(x) часть здесь не проблема. Это c.ClaimsNumber.Substring(c.ClaimNumber.Length - 6), что транслятор EF Core не может переводить в T-SQL.

Несмотря на то, что функция RIGHT существует в Sql Server, вы также не сможете использовать ее с текущими версиями EF Core (последняя версия — 3.1.2 на момент написания). Единственное решение для получения желаемого — создать пользовательскую функцию Sql Server, сопоставить ее с EF Core и использовать в своем запросе.

1) Создать функцию с помощью миграции

> dotnet ef migrations add CreateRightFunction

Во вновь созданный файл миграции поместите этот код:

public partial class CreateRightFunctions : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@"
CREATE FUNCTION fn_Right(@input nvarchar(4000), @howMany int)
RETURNS nvarchar(4000)
BEGIN
RETURN RIGHT(@input, @howMany)
END
");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@"
DROP FUNCTION fn_Right
");
    }
}

Затем запустите обновление БД:

dotnet ef database update

2) Сопоставьте функцию с контекстом EF Core

В вашем классе контекста [DbFunction ("fn_Right")]

public static string Right(string input, int howMany)
{
    throw new NotImplementedException(); // this code doesn't get executed; the call is passed through to the database function
}

3) Используйте функцию в своем запросе

var maxId = Db.Claims.Select(c => MyContext.Right(c.ClaimNumber, 6)).Max(x => Convert.ToInt32(x));

Сгенерированный запрос:

SELECT MAX(CONVERT(int, [dbo].[fn_Right]([c].[ClaimNumber], 6)))
FROM [Claims] AS [c]

Опять же, это далеко не лучшая практика, я думаю, вам следует подумать о добавлении столбца int в вашу таблицу для хранения этого «числа», что бы оно ни представляло в вашем домене.

Кроме того, в первый раз последние 6 символов ClaimNumber содержат нецифровой символ, это больше не будет работать. Если ClaimNumber вводится человеком, рано или поздно это произойдет.

Вы должны кодировать и проектировать свою базу данных и приложение для обеспечения надежности, даже если вы абсолютно уверены, что эти 6 символов всегда будут представлять число. Они не могли делать это вечно :)

person Claudio Valerio    schedule 03.03.2020
comment
Благодарю вас! Да, я полностью согласен, что это было бы трагическим количеством кода для написания того, чего я пытаюсь достичь. Мне интересно - есть ли у вас ссылка на документацию EF, в которой говорится, какие функции .NET поддерживаются для перевода в SQL? Страницу видел, но не нашел. Было бы неплохо вернуться позже, чтобы узнать, станет ли / когда .Substring поддерживаться. - person pbarranis; 03.03.2020
comment
И да, если бы это были данные, введенные человеком, этот код, который у меня есть, был бы ужасной идеей. Это данные, сгенерированные кодом, и приведенный выше код довольно редко когда-либо запускается, поэтому чем проще и легче его понять и поддерживать, тем лучше. - person pbarranis; 03.03.2020
comment
Ознакомьтесь с EF.Functions официальной документацией ядра ef. Я не исключаю, что есть какой-то пакет nuget, который расширяет EF Core и позволяет использовать некоторые другие функции, но я не знаю ни одного. - person Claudio Valerio; 03.03.2020