web-dev-qa-db-fra.com

EntityFramework - contient une requête sur la clé composite

É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?

21
sternr

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.

Solution 1: Join (ou Contains) avec des paires

La 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.

Solution 2: en mémoire

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.

Solution 3: Deux déclarations 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.

Solution 4: Une Contains avec les valeurs calculées

La 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.

Solution 5: Meilleur des 2 et 3

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.

Solution 6: créez une requête avec les clauses OR

À 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 UNIONs. 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 UNIONs 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

34
Gert Arnold

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();
            }
1
juanora

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.

0
Kirk Broadhurst

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();
0
Amro Samy

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+.......));
0
user4093832