Étant donné une liste d'identifiants, je peux interroger toutes les lignes pertinentes en:
context.Table.Where(q => listOfIds.Contains(q.Id));
Mais comment obtenir la même fonctionnalité lorsque la table a une clé composite?
C'est un vilain problème pour lequel je ne connais aucune solution élégante.
Supposons que vous ayez ces combinaisons de touches et que vous ne vouliez sélectionner que celles qui sont marquées (*).
Id1 Id2
--- ---
1 2 *
1 3
1 6
2 2 *
2 3 *
... (many more)
Comment faire cela est une façon dont Entity Framework est heureux? Examinons quelques solutions possibles et voyons si elles sont utiles.
Join
(ou Contains
) avec des pairesLa meilleure solution serait de créer une liste des paires souhaitées, par exemple Tuples, (List<Tuple<int,int>>
) et de joindre les données de la base de données avec cette liste:
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
Dans LINQ aux objets, ce serait parfait, mais, dommage, EF lèvera une exception comme
Impossible de créer une valeur constante de type 'System.Tuple`2 (...) Seuls les types primitifs ou les types énumération sont pris en charge dans ce contexte.
ce qui est une façon assez maladroite de vous dire qu'il ne peut pas traduire cette instruction en SQL, car Tuples
n'est pas une liste de valeurs primitives (comme int
ou string
).1. Pour la même raison, une instruction similaire utilisant Contains
(ou toute autre instruction LINQ) échouerait.
Bien sûr, nous pourrions transformer le problème en simple LINQ en objets tels que:
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
Inutile de dire que ce n'est pas une bonne solution. db.Table
pourrait contenir des millions d'enregistrements.
Contains
Offrons donc à EF deux listes de valeurs primitives, [1,2]
pour Id1
et [2,3]
pour Id2
. Nous ne voulons pas utiliser la jointure (voir note complémentaire), alors utilisons Contains
:
from entity in db.Table
where ids1.Contains(entity.Id1) && ids2.Contains(entity.Id2)
select entity
Mais maintenant, les résultats contiennent également l'entité {1,3}
! Bien sûr, cette entité correspond parfaitement aux deux prédicats. Mais gardons à l'esprit que nous nous rapprochons. Au lieu de mémoriser des millions d’entités, nous n’en avons plus que quatre.
Contains
avec les valeurs calculéesLa solution 3 a échoué car les deux instructions Contains
distinctes ne filtrent pas seulement les combinaisons de leurs valeurs. Et si nous créons d'abord une liste de combinaisons et essayons de les faire correspondre? Nous savons par la solution 1 que cette liste devrait contenir des valeurs primitives. Par exemple:
var computed = ids1.Zip(ids2, (i1,i2) => i1 * i2); // [2,6]
et la déclaration LINQ:
from entity in db.Table
where computed.Contains(entity.Id1 * entity.Id2)
select entity
Il y a quelques problèmes avec cette approche. Tout d'abord, vous verrez que cela retourne également l'entité {1,6}
. La fonction de combinaison (a * b) ne produit pas de valeurs qui identifient de manière unique une paire dans la base de données. Maintenant, nous pourrions créer une liste de chaînes comme ["Id1=1,Id2=2","Id1=2,Id2=3]"
et faire
from entity in db.Table
where computed.Contains("Id1=" + entity.Id1 + "," + "Id2=" + entity.Id2)
select entity
(Cela fonctionnerait dans EF6, pas dans les versions précédentes).
Cela devient assez compliqué. Mais un problème plus important est que cette solution n'est pas sargable , ce qui signifie: elle contourne tous les index de base de données sur Id1
et Id2
qui auraient pu être utilisés autrement. Cela fonctionnera très très mal.
Donc, la seule solution viable à laquelle je puisse penser est une combinaison de Contains
et d'un join
en mémoire: Commencez par faire la déclaration de conteneur comme dans la solution 3. N'oubliez pas, cela nous a rapprochés de ce que nous voulions. Ensuite, affinez le résultat de la requête en joignant le résultat en tant que liste en mémoire:
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;
Ce n'est pas élégant, c'est peut-être le même désordre, mais jusqu'à présent, c'est le seul modulable2 solution à ce problème, j'ai trouvé et appliqué dans mon propre code.
À l'aide d'un générateur de prédicat tel que Linqkit ou une alternative, vous pouvez créer une requête contenant une clause OR pour chaque élément de la liste de combinaisons. Cela pourrait être une option viable pour les très courtes listes. Avec quelques centaines d'éléments, la requête commencera à très mal fonctionner. Donc, je ne considère pas cela comme une bonne solution, sauf si vous pouvez être sûr à 100% qu'il y aura toujours un petit nombre d'éléments. Une élaboration de cette option peut être trouvée ici .
1Comme note amusante, EF fait crée une instruction SQL lorsque vous joignez une liste primitive, comme ceci
from entity in db.Table // db is a DbContext
join i in MyIntegers on entity.Id1 equals i
select entity
Mais le code généré est absurde. Un exemple réel où MyIntegers
ne contient que 5 entiers (!) Ressemble à ceci:
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]
Il y a n-1 UNION
s. Bien sûr, ce n'est pas du tout évolutif.
Ajout ultérieur:
Quelque part sur la route menant à EF version 6.1.3, cela a été grandement amélioré. Les UNION
s sont devenus plus simples et ils ne sont plus imbriqués. Auparavant, la requête abandonnait avec moins de 50 éléments dans la séquence locale (exception SQL: une partie de votre instruction SQL est trop imbriquée}.). La variable non imbriquée UNION
autorise les séquences locales jusqu'à deux des milliers (!) d'éléments. C'est toujours lent avec "beaucoup" d'éléments.
2Dans la mesure où l'instruction Contains
est évolutive: Scalable contient la méthode LINQ par rapport à un serveur SQL
Vous pouvez créer une collection de chaînes avec les deux clés comme ceci (je suppose que vos clés sont de type int):
var id1id2Strings = listOfIds.Select(p => p.Id1+ "-" + p.Id2);
Ensuite, vous pouvez simplement utiliser "Contains" sur votre base de données:
using (dbEntities context = new dbEntities())
{
var rec = await context.Table1.Where(entity => id1id2Strings .Contains(entity.Id1+ "-" + entity.Id2));
return rec.ToList();
}
Vous avez besoin d'un ensemble d'objets représentant les clés que vous souhaitez interroger.
class Key
{
int Id1 {get;set;}
int Id2 {get;set;}
Si vous avez deux listes et que vous vérifiez simplement que chaque valeur apparaît dans leur liste respective, vous obtenez le produit cartésien des listes - ce qui n'est probablement pas ce que vous voulez. Au lieu de cela, vous devez interroger les combinaisons spécifiques requises
List<Key> keys = // get keys;
context.Table.Where(q => keys.Any(k => k.Id1 == q.Id1 && k.Id2 == q.Id2));
Je ne suis pas tout à fait sûr qu'il s'agisse d'une utilisation valide d'Entity Framework; vous pouvez avoir des problèmes avec l'envoi du type Key
à la base de données. Si cela se produit, vous pouvez être créatif:
var composites = keys.Select(k => p1 * k.Id1 + p2 * k.Id2).ToList();
context.Table.Where(q => composites.Contains(p1 * q.Id1 + p2 * q.Id2));
Vous pouvez créer une fonction isomorphe (les nombres premiers sont bons pour cela), quelque chose comme un hashcode, que vous pouvez utiliser pour comparer la paire de valeurs. Tant que les facteurs multiplicatifs sont co-prime, ce modèle sera isomorphe (un à un) - c’est-à-dire que le résultat de p1*Id1 + p2*Id2
identifiera de manière unique les valeurs de Id1
et de Id2
tant que les nombres premiers seront correctement choisis.
Mais vous vous retrouvez alors dans une situation où vous implémentez des concepts complexes et que quelqu'un va devoir supporter cela. Mieux vaut probablement écrire une procédure stockée qui prend les objets clés valides.
J'ai essayé cette solution et cela a fonctionné avec moi et la requête de sortie était parfaite sans aucun paramètre
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();
en cas de clé composite, vous pouvez utiliser une autre liste idlist et ajouter une condition pour cela dans votre code
context.Table.Where(q => listOfIds.Contains(q.Id) && listOfIds2.Contains(q.Id2));
ou vous pouvez utiliser un autre tour créer une liste de vos clés en les ajoutant
listofid.add(id+id1+......)
context.Table.Where(q => listOfIds.Contains(q.Id+q.id1+.......));