учитывая список идентификаторов, я могу запросить все соответствующие строки:
context.Table.Where(q => listOfIds.Contains(q.Id));
Но как вы достигаете той же функциональности, когда таблица имеет составной ключ?
учитывая список идентификаторов, я могу запросить все соответствующие строки:
context.Table.Where(q => listOfIds.Contains(q.Id));
Но как вы достигаете той же функциональности, когда таблица имеет составной ключ?
Это неприятная проблема, для которой я не знаю ничего элегантного решения.
Предположим, что у вас есть эти комбинации клавиш, и вы хотите выбрать только отмеченные (*).
Id1 Id2
--- ---
1 2 *
1 3
1 6
2 2 *
2 3 *
... (many more)
Как сделать так, чтобы Entity Framework была счастлива? Давайте посмотрим на некоторые возможные решения и посмотрим, хороши ли они.
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), потерпит неудачу.
Конечно, мы могли бы превратить проблему в простой 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
может содержать миллионы записей.
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}
! Ну, конечно, эта сущность идеально соответствует двум предикатам. Но имейте в виду, что мы приближаемся. Вместо того, чтобы вытаскивать миллионы объектов в память, теперь мы получаем только четыре из них.
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
которые могли быть использованы иначе. Это будет очень плохо.
Итак, единственное жизнеспособное решение, о котором я могу думать, это комбинация 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 для этой проблемы, которое я нашел и применил в моем собственном коде.
Используя построитель 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-сервера
Вы можете создать коллекцию строк с этими двумя ключами (я предполагаю, что ваши ключи - это тип int):
var id1id2Strings = listOfIds.Select(p => p.Id1+ "-" + p.Id2);
Затем вы можете просто использовать "Содержит" на вашем db:
using (dbEntities context = new dbEntities())
{
var rec = await context.Table1.Where(entity => id1id2Strings .Contains(entity.Id1+ "-" + entity.Id2));
return rec.ToList();
}
В случае составного ключа вы можете использовать другой список идентификаторов и добавить условие для этого в свой код
context.Table.Where(q => listOfIds.Contains(q.Id) && listOfIds2.Contains(q.Id2));
или вы можете использовать трюк друг друга, создавая список своих ключей, добавляя их
listofid.add(id+id1+......)
context.Table.Where(q => listOfIds.Contains(q.Id+q.id1+.......));
Вам нужен набор объектов, представляющих ключи, которые вы хотите запросить.
class Key
{
int Id1 {get;set;}
int Id2 {get;set;}
Если у вас есть два списка, и вы просто проверяете, что каждое значение отображается в их соответствующем списке, тогда вы получаете декартовое произведение из списков, что, скорее всего, не то, что вы хотите. Вместо этого вам нужно запросить требуемые комбинации
List<Key> keys = // get keys;
context.Table.Where(q => keys.Any(k => k.Id1 == q.Id1 && k.Id2 == q.Id2));
Я не совсем уверен, что это действительное использование Entity Framework; у вас могут возникнуть проблемы с отправкой типа Key
в базу данных. Если это произойдет, вы можете быть творческими:
var composites = keys.Select(k => p1 * k.Id1 + p2 * k.Id2).ToList();
context.Table.Where(q => composites.Contains(p1 * q.Id1 + p2 * q.Id2));
Вы можете создать изоморфную функцию (простые числа хороши для этого), что-то вроде хэш-кода, который вы можете использовать для сравнения пары значений. Пока мультипликативные факторы являются совместными, этот шаблон будет изоморфным (взаимно однозначным), т.е. Результат p1*Id1 + p2*Id2
однозначно идентифицирует значения Id1
и Id2
, пока простые числа правильно выбран.
Но тогда вы оказываетесь в ситуации, когда вы реализуете сложные концепции, и кто-то должен будет поддержать это. Вероятно, лучше написать хранимую процедуру, которая принимает действительные ключевые объекты.
Я попробовал это решение, и оно работало со мной, и выходной запрос был идеальным без каких-либо параметров
using LinqKit; // nuget
var customField_Ids = customFields?.Select(t => new CustomFieldKey { Id = t.Id, TicketId = t.TicketId }).ToList();
var uniqueIds1 = customField_Ids.Select(cf => cf.Id).Distinct().ToList();
var uniqueIds2 = customField_Ids.Select(cf => cf.TicketId).Distinct().ToList();
var predicate = PredicateBuilder.New<CustomFieldKey>(false); //LinqKit
var lambdas = new List<Expression<Func<CustomFieldKey, bool>>>();
foreach (var cfKey in customField_Ids)
{
var id = uniqueIds1.Where(uid => uid == cfKey.Id).Take(1).ToList();
var ticketId = uniqueIds2.Where(uid => uid == cfKey.TicketId).Take(1).ToList();
lambdas.Add(t => id.Contains(t.Id) && ticketId.Contains(t.TicketId));
}
predicate = AggregateExtensions.AggregateBalanced(lambdas.ToArray(), (expr1, expr2) =>
{
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<CustomFieldKey, bool>>
(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
});
var modifiedCustomField_Ids = repository.GetTable<CustomFieldLocal>()
.Select(cf => new CustomFieldKey() { Id = cf.Id, TicketId = cf.TicketId }).Where(predicate).ToArray();
Вы можете использовать Union
для каждого составного первичного ключа:
var compositeKeys = new List<CK>
{
new CK { id1 = 1, id2 = 2 },
new CK { id1 = 1, id2 = 3 },
new CK { id1 = 2, id2 = 4 }
};
IQuerable<CK> query = null;
foreach(var ck in compositeKeys)
{
var temp = context.Table.Where(x => x.id1 == ck.id1 && x.id2 == ck.id2);
query = query == null ? temp : query.Union(temp);
}
var result = query.ToList();
В отсутствие общего решения, я думаю, есть две вещи, которые следует учитывать:
Например, проблема, связанная с этим вопросом, заключалась в том, чтобы запросить строки порядка, где ключ - это идентификатор заказа + номер строки заказа + тип заказа, а источник имел тип заказа неявный. То есть тип заказа был константой, идентификатор заказа уменьшал бы запрос, заданный для порядка строк соответствующих заказов, и обычно их было бы 5 или меньше.
Перефразировать: если у вас есть составной ключ, изменения состоят в том, что у одного из них очень мало дубликатов. Примените решение 5 сверху.