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','')
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
Dovresti provare esattamente la stessa query in Linqpad (usando le variabili). –
Grazie per i suggerimenti ma quella è stata la prima cosa che ho fatto. – Aleks