Не удалось перевести LINQ в SQL — выражение, проверяющее значение null

#linq #linq-to-sql #entity-framework-core #automapper #ef-core-3.1

#linq #linq-to-sql #entity-framework-core #automapper #ef-core-3.1

Вопрос:

Прежде всего, полный сценарий можно найти здесь:https://dotnetfiddle.net/a2aIkJ

Я использую AutoMapper, чтобы включить фильтрацию (выражение LINQ) по атрибутам DTO и сопоставить эти выражения обратно с объектом базы данных. В конфигурации AutoMapper у меня есть следующий сценарий:

 cfg.CreateMap<Car, CarDto>()
    .ForMember(dest => dest.BrandFks, opt => opt.MapFrom(source => source.Wheels.Select(x => x.FkBrand)));
  

Когда я использую следующий код, преобразование LINQ в SQL работает должным образом:

 // This code throws NO error
var working = context.Cars
    .UseAsDataSource(mapper.ConfigurationProvider).For<CarDto>()
    .Where(x => x.BrandFks.Any(y => y == 1))
    .ToList();
  

Однако, когда я проверяю то же свойство на null:

 // This code throws the error
var notWorking = context.Cars
    .UseAsDataSource(mapper.ConfigurationProvider).For<CarDto>()
    .Where(x => x.BrandFks == null)
    .ToList();
  

возникает следующая ошибка:

 Unhandled exception. System.InvalidOperationException: The LINQ expression 'DbSet<Car>
    .Where(c => DbSet<Wheel>
        .Where(w => EF.Property<Nullable<int>>(c, "Id") != null amp;amp; EF.Property<Nullable<int>>(c, "Id") == EF.Property<Nullable<int>>(w, "Id"))
        .Select(w => w.FkBrand) == null)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().
  

Кто-нибудь знает, почему это не работает и как я мог бы решить эту проблему?

Редактировать — Уточнение: выражение фильтра определяется другим компонентом, который знает только о DTO и ничего о базе данных.

Комментарии:

1. Вместо проверки на null , не следует ли вам использовать !x.BrandFks.Any() ? Похоже, что BrandFks это коллекция, которая не может иметь членов, но не может быть нулевой …?

2. @NetMage Но проверка на null имеет смысл только при просмотре DTO, и в моем случае выражение фильтра устанавливается компонентом, который знает только о DTO и ничего о том, как оно сохраняется в базе данных

3. Перевод показывает, что запрос все еще переводится в SQL, поэтому DTO не (точно) актуален. Вы могли бы поставить .AsEnumerable() перед Where и извлечь все данные, связанные с клиентом, для тестирования?

Ответ №1:

Правильный подход

Вы должны использовать следующий запрос:

 var correct = context.Cars
    .UseAsDataSource(mapper.ConfigurationProvider).For<CarDto>()
    .Where(x => !x.BrandFks.Any())
    .ToList();
  

Если вы регистрируете сгенерированные запросы, становится очевидным, почему запрос необходимо переписать, как указано выше:

 SELECT [c].[Id], [c].[Name], [w].[FkBrand], [w].[Id]
FROM [Cars] AS [c]
LEFT JOIN [Wheels] AS [w] ON [c].[Id] = [w].[Id]
WHERE NOT (EXISTS (
    SELECT 1
    FROM [Wheels] AS [w0]
    WHERE [c].[Id] = [w0].[Id]))
ORDER BY [c].[Id], [w].[Id]
  

Automapper переводит ваше opt.MapFrom(source => source.Wheels.Select(x => x.FkBrand)) утверждение в LEFT JOIN on Wheels , а затем использует CarDto.BrandFks как синоним (своего рода) для Wheels таблицы.

Короче говоря, вы не можете проверить, является ли таблица NULL , но вы можете проверить, пуста ли она.

Неоптимальная альтернатива

В качестве неоптимальной альтернативы вы могли бы просто переключиться на оценку клиента, но это приводит к потенциально большому набору результатов из базы данных, который затем фильтруется в памяти:

 var suboptimal = context.Cars
    .UseAsDataSource(mapper.ConfigurationProvider).For<CarDto>()
    .AsEnumerable() // <-- switch to client evaluation
    .Where(x => x.BrandFks == null)
    .ToList();
  

Сгенерированный SQL выглядит следующим образом (он не содержит никакого фильтра, поскольку он фильтруется позже в памяти через LINQ to Objects):

 SELECT [c].[Id], [c].[Name], [w].[FkBrand], [w].[Id]
FROM [Cars] AS [c]
LEFT JOIN [Wheels] AS [w] ON [c].[Id] = [w].[Id]
ORDER BY [c].[Id], [w].[Id]
  

Смотрите .NET Fiddle для получения кода в действии.

Комментарии:

1. Таким образом, это означало бы, что мне нужно перехватить null проверки в выражении DTO и изменить его на !Any() ? Возможно ли это?

2. Если source или Wheels в opt.MapFrom(source => source.Wheels.Select(x => x.FkBrand)) может быть null , то вам обычно нужно проверить их там и защититься от этого. В противном случае вы этого не сделаете, потому что .Select(x => x.FkBrand) всегда будет возвращать перечислимое значение (даже если оно пустое) и никогда не вернется null . Однако это предполагает, что выражение более или менее переведено AutoMapper как есть. Теоретически AutoMapper может просто переписать его совершенно другим способом (например, AutoMapper может вообще не нуждаться в каких-либо null проверках, потому что он все равно переводит выражение в SQL).

3. Поведение по умолчанию в отношении нулевых коллекций описано в разделе Обработка нулевых коллекций .