Ответ 1
Это неприятная проблема, для которой я не знаю ничего элегантного решения.
Предположим, что у вас есть эти комбинации клавиш, и вы хотите выбрать только отмеченные (*).
Id1 Id2
--- ---
1 2 *
1 3
1 6
2 2 *
2 3 *
... (many more)
Как сделать так, чтобы Entity Framework была счастлива? Давайте посмотрим на некоторые возможные решения и посмотрим, хороши ли они.
Решение 1: Join
(или Contains
) с парами
Лучшим решением было бы создать список пар, который вы хотите, например Tuples, (List<Tuple<int,int>>
) и объединить данные базы данных с этим списком:
from entity in db.Table // db is a DbContext
join pair in Tuples on new { entity.Id1, entity.Id2 }
equals new { Id1 = pair.Item1, Id2 = pair.Item2 }
select entity
В LINQ для объектов это было бы прекрасно, но слишком плохо, EF будет генерировать исключение, например
Невозможно создать постоянное значение типа "System.Tuple`2 (...) В этом контексте поддерживаются только примитивные типы или типы перечислений.
который является довольно неуклюжим способом сказать вам, что он не может перевести этот оператор в SQL, потому что Tuples
не является списком примитивных значений (например, int
или string
). 1. По той же причине аналогичный оператор, использующий Contains
(или любой другой оператор LINQ), потерпит неудачу.
Решение 2: встроенная память
Конечно, мы могли бы превратить проблему в простой LINQ для таких объектов:
from entity in db.Table.AsEnumerable() // fetch db.Table into memory first
join pair Tuples on new { entity.Id1, entity.Id2 }
equals new { Id1 = pair.Item1, Id2 = pair.Item2 }
select entity
Излишне говорить, что это нехорошее решение. db.Table
может содержать миллионы записей.
Решение 3: два оператора Contains
Итак, предложите EF два списка примитивных значений, [1,2]
для Id1
и [2,3]
для Id2
. Мы не хотим использовать join (см. Примечание к стороне), поэтому используйте Contains
:
from entity in db.Table
where ids1.Contains(entity.Id1) && ids2.Contains(entity.Id2)
select entity
Но теперь результаты также содержат сущность {1,3}
! Ну, конечно, эта сущность идеально соответствует двум предикатам. Но имейте в виду, что мы приближаемся. Вместо того, чтобы вытаскивать миллионы объектов в память, теперь мы получаем только четыре из них.
Решение 4: Один Contains
с вычисленными значениями
Решение 3 не удалось, поскольку два отдельных оператора Contains
не только фильтруют комбинации своих значений. Что делать, если мы сначала создадим список комбинаций и попытаемся сопоставить эти комбинации? Из решения 1 известно, что этот список должен содержать примитивные значения. Например:
var computed = ids1.Zip(ids2, (i1,i2) => i1 * i2); // [2,6]
и оператор LINQ:
from entity in db.Table
where computed.Contains(entity.Id1 * entity.Id2)
select entity
Есть некоторые проблемы с этим подходом. Во-первых, вы увидите, что это также возвращает объект {1,6}
. Комбинационная функция (a * b) не дает значений, которые однозначно идентифицируют пару в базе данных. Теперь мы можем создать список строк типа ["Id1=1,Id2=2","Id1=2,Id2=3]"
и сделать
from entity in db.Table
where computed.Contains("Id1=" + entity.Id1 + "," + "Id2=" + entity.Id2)
select entity
(Это будет работать в EF6, а не в более ранних версиях).
Это становится довольно грязным. Но более важной проблемой является то, что это решение не sargable, что означает: он обходит любые индексы базы данных на Id1
и Id2
которые могли быть использованы иначе. Это будет очень плохо.
Решение 5: Лучшее из 2 и 3
Итак, единственное жизнеспособное решение, о котором я могу думать, это комбинация Contains
и a Join
в памяти: сначала сделайте оператор contains, как в решении 3. Помните, что он приблизился к тому, что мы хотели. Затем уточните результат запроса, присоединив результат как список в памяти:
var rawSelection = from entity in db.Table
where ids1.Contains(entity.Id1) && ids2.Contains(entity.Id2)
select entity;
var refined = from entity in rawSelection.AsEnumerable()
join pair in Tuples on new { entity.Id1, entity.Id2 }
equals new { Id1 = pair.Item1, Id2 = pair.Item2 }
select entity;
Это не изящно, беспорядочно все-таки возможно, но пока это единственное масштабируемое решение 2 для этой проблемы, которое я нашел и применил в моем собственном коде.
Решение 6: постройте запрос с предложениями OR
Используя построитель Predicate, такой как Linqkit или альтернативы, вы можете создать запрос, содержащий предложение OR для каждого элемента в списке комбинаций. Это может быть жизнеспособным вариантом для действительно коротких списков. С несколькими сотнями элементов запрос начнет работать очень плохо. Поэтому я не считаю это хорошим решением, если вы не можете быть на 100% уверены, что всегда будет небольшое количество элементов. Один вариант этой опции можно найти здесь.
1 Как забавная заметка, EF создает инструкцию SQL, когда вы присоединяетесь к примитивному списку, например
from entity in db.Table // db is a DbContext
join i in MyIntegers on entity.Id1 equals i
select entity
Но сгенерированный SQL, ну, абсурдно. Пример в реальной жизни, где MyIntegers
содержит только 5 (!) Целых чисел, выглядит следующим образом:
SELECT
[Extent1].[CmpId] AS [CmpId],
[Extent1].[Name] AS [Name],
FROM [dbo].[Company] AS [Extent1]
INNER JOIN (SELECT
[UnionAll3].[C1] AS [C1]
FROM (SELECT
[UnionAll2].[C1] AS [C1]
FROM (SELECT
[UnionAll1].[C1] AS [C1]
FROM (SELECT
1 AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
UNION ALL
SELECT
2 AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable2]) AS [UnionAll1]
UNION ALL
SELECT
3 AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable3]) AS [UnionAll2]
UNION ALL
SELECT
4 AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable4]) AS [UnionAll3]
UNION ALL
SELECT
5 AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable5]) AS [UnionAll4] ON [Extent1].[CmpId] = [UnionAll4].[C1]
Есть n-1 UNION
s. Конечно, это не масштабируемо вообще.
Позднее добавление:
Где-то по дороге в EF версии 6.1.3 это значительно улучшилось. UNION
стали проще, и они больше не вложены. Раньше запрос отказывался от менее 50 элементов в локальной последовательности (исключение SQL: некоторая часть вашего оператора SQL вложен слишком глубоко.) Не вложенные UNION
разрешают локальные последовательности до нескольких тысяч (!) элементов. Он все еще медленный, хотя и с "многими" элементами.
2 Поскольку оператор Contains
является масштабируемым: Масштабируемый Содержит метод LINQ для SQL-сервера