2014-12-02 10 views
12

Sto facendo un join su due funzioni SQL utilizzando Entity Framework come mio ORM. Quando la query viene eseguito ottengo questo messaggio di errore:LINQ left outer query error: OuterApply non aveva le chiavi appropriate

The query attempted to call 'Outer Apply' over a nested query, 
but 'OuterApply' did not have the appropriate keys 

Questa è la mia domanda:

var ingredientAllergenData = (from ings in db.fnListIngredientsFromItem(productId, (short)itemType, productId) 
          join ingAllergens in db.fnListAllergensFromItems(productId.ToString(CultureInfo.InvariantCulture), (short)itemType, currentLang) 
          on ings.id equals ingAllergens.ingredientId into ingAllergensData 
          from allergens in ingAllergensData.DefaultIfEmpty() 
          where ings.table == "tblIng" || ings.table == "" 
          select new {ings, allergens}).ToList(); 

ho scritto la stessa query in LINQPad e sono tornato risultati, quindi non sono sicuro di cosa il problema è:

var ingredientAllergenData = (from ings in fnListIngredientsFromItem(1232, 0, 1232) 
          join ingAllergens in fnListAllergensFromItems("1232", 0, 1) 
          on ings.Id equals ingAllergens.IngredientId into ingAllergensData 
          from allergens in ingAllergensData.DefaultIfEmpty() 
          where ings.Table == "tblIng" || ings.Table == "" 
          select new {ings, allergens}).ToList(); 

la risposta da parte LINQPad: enter image description here

EDIT Questa è la query SQL generato in LINQPad:

-- Region Parameters 
    DECLARE @p0 Int = 1232 
    DECLARE @p1 Int = 0 
    DECLARE @p2 Int = 1232 
    DECLARE @p3 VarChar(1000) = '1232' 
    DECLARE @p4 SmallInt = 0 
    DECLARE @p5 Int = 1 
    DECLARE @p6 VarChar(1000) = 'tblIng' 
    DECLARE @p7 VarChar(1000) = '' 
    -- EndRegion 
    SELECT [t0].[prodId] AS [ProdId], [t0].[id] AS [Id], [t0].[parent] AS [Parent], [t0].[name] AS [Name], [t0].[ing_gtin] AS [Ing_gtin], [t0].[ing_artsup] AS [Ing_artsup], [t0].[table] AS [Table], [t0].[quantity] AS [Quantity], [t2].[test], [t2].[prodId] AS [ProdId2], [t2].[ingredientId] AS [IngredientId], [t2].[allergenId] AS [AllergenId], [t2].[allergenName] AS [AllergenName], [t2].[level_of_containment] AS [Level_of_containment] 
    FROM [dbo].[fnListIngredientsFromItem](@p0, @p1, @p2) AS [t0] 
    LEFT OUTER JOIN (
     SELECT 1 AS [test], [t1].[prodId], [t1].[ingredientId], [t1].[allergenId], [t1].[allergenName], [t1].[level_of_containment] 
     FROM [dbo].[fnListAllergensFromItems](@p3, @p4, @p5) AS [t1] 
     ) AS [t2] ON [t0].[id] = ([t2].[ingredientId]) 
    WHERE ([t0].[table] = @p6) OR ([t0].[table] = @p7) 

Ho provato anche hardcoding gli stessi numeri in C# ed ho ottenuto di nuovo lo stesso errore.

+0

Vorrei iniziare provando a fare tutte le conversioni (casting a breve, .ToString chiamate, etc.) fuori dalla query e assegnandoli a variabili e utilizzando quelli nella query. Sembra che LINQ stia soffocando nel tentativo di rendere le conversioni parte della query. – Becuzz

+0

Dovresti provare esattamente la stessa query in Linqpad (usando le variabili). –

+0

Grazie per i suggerimenti ma quella è stata la prima cosa che ho fatto. – Aleks

risposta

10

Il problema è che Entity Framework deve sapere quali colonne della chiave primaria dei risultati TVF devono eseguire un join sinistro e il file EDMX generato predefinito non contiene tali informazioni. È possibile aggiungere le informazioni sul valore chiave associando i risultati TVF a un'entità (anziché il valore predefinito di associazione a un tipo complesso).

Il motivo per cui la stessa query funziona in LINQPad è che il driver Data Context predefinito per la connessione a un database in LINQPad utilizza LINQ to SQL (non Entity Framework). Ma sono stato in grado di ottenere la query per l'esecuzione in Entity Framework (eventualmente).

ho creato un locale di database di SQL Server simili valori di tabella funzioni:

CREATE FUNCTION fnListIngredientsFromItem(@prodId int, @itemType1 smallint, @parent int) 
RETURNS TABLE 
AS 
RETURN (
    select prodId = 1232, id = 1827, parent = 1232, name = 'Ossenhaaspunten', ing_gtin =089821, ing_artsup=141020, [table] = 'tblIng', quantity = '2 K' 
); 
go 
CREATE FUNCTION fnListAllergensFromItems(@prodIdString varchar(1000), @itemType2 smallint, @lang int) 
RETURNS TABLE 
AS 
RETURN (
    select prodId = '1232', ingredientId = 1827, allergenId = 11, allergenName = 'fish', level_of_containment = 2 
    union all 
    select prodId = '1232', ingredientId = 1827, allergenId = 16, allergenName = 'tree nuts', level_of_containment = 2 
    union all 
    select prodId = '1232', ingredientId = 1827, allergenId = 12, allergenName = 'crustacean and shellfish', level_of_containment = 2 
); 
go 

e ho creato un progetto di test utilizzando Entity Framework 6.1.2 e generato un file EDMX dal database utilizzando il modello Entity Data Designer in Visual Studio 2013. Con questa impostazione, sono stato in grado di ottenere lo stesso errore quando si tenta di eseguire la query:

System.NotSupportedException 
    HResult=-2146233067 
    Message=The query attempted to call 'OuterApply' over a nested query, but 'OuterApply' did not have the appropriate keys. 
    Source=EntityFramework 
    StackTrace: 
     at System.Data.Entity.Core.Query.PlanCompiler.NestPullup.ApplyOpJoinOp(Op op, Node n) 
     at System.Data.Entity.Core.Query.PlanCompiler.NestPullup.VisitApplyOp(ApplyBaseOp op, Node n) 
     at System.Data.Entity.Core.Query.InternalTrees.BasicOpVisitorOfT`1.Visit(OuterApplyOp op, Node n) 
     ... 

esecuzione di espressione alternativo per un LEFT jOIN provocato un errore di leggermente diverso:

var ingredientAllergenData = (db.fnListIngredientsFromItem(1323, (short)0, 1) 
    .GroupJoin(db.fnListAllergensFromItems("1232", 0, 1), 
     ing => ing.id, 
     allergen => allergen.ingredientId, 
     (ing, allergen) => new { ing, allergen } 
    ) 
).ToList(); 

Ecco uno stacktrace troncato dalla nuova eccezione:

System.NotSupportedException 
    HResult=-2146233067 
    Message=The nested query does not have the appropriate keys. 
    Source=EntityFramework 
    StackTrace: 
     at System.Data.Entity.Core.Query.PlanCompiler.NestPullup.ConvertToSingleStreamNest(Node nestNode, Dictionary`2 varRefReplacementMap, VarList flattenedOutputVarList, SimpleColumnMap[]& parentKeyColumnMaps) 
     at System.Data.Entity.Core.Query.PlanCompiler.NestPullup.Visit(PhysicalProjectOp op, Node n) 
     at System.Data.Entity.Core.Query.InternalTrees.PhysicalProjectOp.Accept[TResultType](BasicOpVisitorOfT`1 v, Node n) 
     ... 

Entity Framework è open source, in modo che possiamo effettivamente guardare il codice sorgente in cui viene lanciata questa eccezione. I commenti in questo frammento spiega qual è il problema (https://entityframework.codeplex.com/SourceControl/latest#src/EntityFramework/Core/Query/PlanCompiler/NestPullup.cs):

// Make sure that the driving node has keys defined. Otherwise we're in 
// trouble; we must be able to infer keys from the driving node. 
var drivingNode = nestNode.Child0; 
var drivingNodeKeys = Command.PullupKeys(drivingNode); 
if (drivingNodeKeys.NoKeys) 
{ 
    // ALMINEEV: In this case we used to wrap drivingNode into a projection that would also project Edm.NewGuid() thus giving us a synthetic key. 
    // This solution did not work however due to a bug in SQL Server that allowed pulling non-deterministic functions above joins and applies, thus 
    // producing incorrect results. SQL Server bug was filed in "sqlbuvsts01\Sql Server" database as #725272. 
    // The only known path how we can get a keyless drivingNode is if 
    // - drivingNode is over a TVF call 
    // - TVF is declared as Collection(Row) is SSDL (the only form of TVF definitions at the moment) 
    // - TVF is not mapped to entities 
    //  Note that if TVF is mapped to entities via function import mapping, and the user query is actually the call of the 
    //  function import, we infer keys for the TVF from the c-space entity keys and their mappings. 
    throw new NotSupportedException(Strings.ADP_KeysRequiredForNesting); 
} 

che spiega il percorso che conduce a tale errore, in modo da qualcosa che possiamo fare per scendere quel percorso dovrebbe risolvere il problema. Supponendo di dover fare quel left join sui risultati di una funzione valutata a livello di tabella, un'opzione (forse l'unica opzione?) È quella di mappare i risultati di TVF a un'entità che ha una chiave primaria. Quindi Entity Framework conoscerà i valori chiave dei risultati TVF in base alla mappatura a quell'entità e dovremmo evitare questi errori relativi alle chiavi mancanti.

Per impostazione predefinita, quando si genera un file EDMX dal database, un TVF viene mappato su un tipo complesso. Ci sono istruzioni su come cambiarlo a https://msdn.microsoft.com/en-us/library/vstudio/ee534438%28v=vs.100%29.aspx.

Nel mio progetto di test, ho aggiunto una tabella vuota con uno schema che corrispondeva all'output dei TVF per ottenere la progettazione del modello per generare entità, quindi sono passato al browser del modello e aggiornato la funzione di importazione per restituire una raccolta di queste entità (invece dei tipi complessi generati automaticamente). Dopo aver apportato queste modifiche, la stessa query LINQ è stata eseguita senza errori.

var ingredientAllergenData = (from ings in db.fnListIngredientsFromItem(productId, (short)itemType, productId) 
          join ingAllergens in db.fnListAllergensFromItems(productId.ToString(CultureInfo.InvariantCulture), (short)itemType, currentLang) 
          on ings.id equals ingAllergens.ingredientId into ingAllergensData 
          from allergens in ingAllergensData.DefaultIfEmpty() 
          where ings.table == "tblIng" || ings.table == "" 
          select new {ings, allergens}).ToList(); 

Ecco lo SQL traccia che la query mi ha dato:

SELECT 
    1 AS [C1], 
    [Extent1].[prodId] AS [prodId], 
    [Extent1].[id] AS [id], 
    [Extent1].[parent] AS [parent], 
    [Extent1].[name] AS [name], 
    [Extent1].[ing_gtin] AS [ing_gtin], 
    [Extent1].[ing_artsup] AS [ing_artsup], 
    [Extent1].[table] AS [table], 
    [Extent1].[quantity] AS [quantity], 
    [Extent2].[prodId] AS [prodId1], 
    [Extent2].[ingredientId] AS [ingredientId], 
    [Extent2].[allergenId] AS [allergenId], 
    [Extent2].[allergenName] AS [allergenName], 
    [Extent2].[level_of_containment] AS [level_of_containment] 
    FROM [dbo].[fnListIngredientsFromItem](@prodId, @itemType1, @parent) AS [Extent1] 
    LEFT OUTER JOIN [dbo].[fnListAllergensFromItems](@prodIdString, @itemType2, @lang) AS [Extent2] ON ([Extent1].[id] = [Extent2].[ingredientId]) OR (([Extent1].[id] IS NULL) AND ([Extent2].[ingredientId] IS NULL)) 
    WHERE [Extent1].[table] IN ('tblIng','') 
+0

"Ben fatto, mio ​​caro Watson!" Mi chiedo se un altro modo per sbarazzarsi dell'eccezione sia aggiungere un 'OrderBy':' db.fnListIngredientsFromItem (productId, (short) itemType, productId) .OrderBy (x => x.ProdId) '. Vedi [questo] (http://stackoverflow.com/a/27206566/861716). –

+0

Ottima risposta mi ha dato l'idea di risolvere un altro problema. – rodpl

+0

Ottima spiegazione! Nel mio caso stavo raggruppando i risultati del TVF e poi facendo 'where g.Count() == numSearchTerms || g.FirstOrDefault(). SearchTerm == null'. Leggendo questo, ho avuto l'idea di provare 'dove g.Count() == numSearchTerms || g.Any (x => x.SearchTerm == null) 'invece ha funzionato! – adam0101