avr. 21 2010

[LINQ To SQL] : Exception “All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.”

Category: Linq To SqlNicolas Esprit @ 14:03

Dernièrement, j’ai utilisé LINQ To SQL pour réaliser un UNION sur deux requêtes. Certains champs d’une des deux requêtes avaient volontairement pour valeur string.Empty. Seulement, lors de l’exécution cette exception est apparue :

“All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.”.

Vous me direz la résolution du problème est simple, il suffit de vérifier qu’il y a bien le même nombre de champs sélectionnés dans les deux SELECT de la requête. Seulement c’est déjà le cas ! Comment expliquer cela alors ? C’est en fait simple : LINQ To SQL génère une requête SQL à partir de la Query déclarée dans le code. Lors de la génération, LINQ procède à certaines optimisations dont le retrait des valeurs utilisées plusieurs fois dans les SELECT (ou bien des champs présents plusieurs fois). Un petit exemple sera plus parlant :

var a = (from t in dc.Table 
select new {
    Element1 = t.Champ1, 
    Element2 = t.Champ2, 
}
).Union(from t in dc.Table 
select new { 
    Element1 = t.Champ1, 
    Element2 = t.Champ1, // notez la répétition 
}); 

 

Génèrera le SQL suivant :

SELECT [t2].[Champ1] AS [Element1], [t2].[Champ2] AS [Element2]
FROM (
    SELECT [t0].[Champ1], [t0].[Champ2]
    FROM [dbo].[Table] AS [t0]
    UNION
    SELECT [t1].[Champ1] – Notez l’absence du deuxième Champ1
    FROM [dbo].[Table] AS [t1]
    ) AS [t2]
 

Ici, la répétition du champ Champ1 a disparu de la seconde requête. Cette optimisation peut en fait être considérée comme un bug. Celui-ci a été reporté à Microsoft, mais ne sera apparemment pas corrigé. Il intervient également lorsque dans un SELECT on utile par exemple string.Empty plusieurs fois. Exemple :

 
var a = (from t in dc.Table
 select new {    
    Element1 = t.Champ1,     
    Element2 = t.Champ2, 
    Element3 = t.Champ3, 
}
).Union(from t in dc.Table 
select new {     
    Element1 = t.Champ1,     
    Element2 = string.Empty, 
    Element3 = string.Empty,
}); 

 

Génèrera le SQL suivant :

SELECT [t2].[Champ1] AS [Element1], [t2].[Champ2] AS [Element2], [t2].[Champ3] AS [Element3]
FROM (    
    SELECT [t0].[Champ1], [t0].[Champ2], [t0].[Champ3]       
    FROM [dbo].[Table] AS [t0]   
    UNION    
    SELECT [t1].[Champ1], @p0 AS [value]
    FROM [dbo].[Table] AS [t1]    
    ) AS [t2]
 

Comme vous pouvez le voir, le deuxième string.Empty a disparu. Au final le second SELECT de l’UNION ne comporte que 2 champs au lieu de 3 pour le premier SELECT. Ainsi l’exception est encore levée. Une solution, utiliser le mot-clé let comme ci-dessous :

 
var a = (from t in dc.Table
select new { 
    Element1 = t.Champ1, 
    Element2 = t.Champ2, 
    Element3 = t.Champ3, 
}
).Union(from t in dc.Table
let defaultString = default(string)
select new { 
    Element1 = t.Champ1, 
    Element2 = string.Empty, 
    Element3 = defaultString,
});
 

De cette façon, il n’y a pas de répétition, et donc lors de l’optimisation l’Element3 du deuxième SELECT n’est pas retiré de la requête SQL.

Cette solution fonctionne, seulement elle a un gros inconvénient : il faut un let pour chaque utilisation. Autrement dit si j’ajoute un Element4 dans mes requêtes, je ne pourrais utiliser la variable defaultString deux fois car elle sera elle aussi supprimée lors de l’optimisation. Il faut donc déclarer une seconde variable defaultString2 pour l’Element4.

Si quelqu’un a une meilleure solution, je suis preneur !

PS : Au passage, je vous invite à consulter ce billet pour savoir comment visualiser dans la fenêtre Output de Visual Studio le code SQL généré par LINQ lors de l’exécution : http://www.u2u.info/Blogs/Kris/Lists/Posts/Post.aspx?ID=11

Tags: , ,

Les commentaires sont clos