SQL-frågor
Med Entity Framework Core kan du gå ner till SQL-frågor när du arbetar med en relationsdatabas. SQL-frågor är användbara om den fråga du vill ha inte kan uttryckas med LINQ, eller om en LINQ-fråga gör att EF genererar ineffektiv SQL. SQL-frågor kan returnera vanliga entitetstyper eller nyckellösa entitetstyper som ingår i din modell.
Grundläggande SQL-frågor
Du kan använda FromSql för att starta en LINQ-fråga baserat på en SQL-fråga:
var blogs = await context.Blogs
.FromSql($"SELECT * FROM dbo.Blogs")
.ToListAsync();
Not
FromSql introducerades i EF Core 7.0. När du använder äldre versioner använder du FromSqlInterpolated i stället.
SQL-frågor kan användas för att köra en lagrad procedur som returnerar entitetsdata:
var blogs = await context.Blogs
.FromSql($"EXECUTE dbo.GetMostPopularBlogs")
.ToListAsync();
Not
FromSql kan bara användas direkt på en DbSet
. Den kan inte bestå över en godtycklig LINQ-fråga.
Överföra parametrar
Varning
Var uppmärksam på parameterisering när du använder SQL-frågor
När du introducerar några användaringivna värden i en SQL-fråga måste du vara noga med att undvika SQL-inmatningsattacker. SQL-inmatning inträffar när ett program integrerar ett strängvärde som tillhandahålls av användaren i en SQL-fråga, och det användardefinierade värdet skapas för att avsluta strängen och utföra en annan skadlig SQL-åtgärd. Om du vill veta mer om SQL-inmatning kan du se den här sidan.
Metoderna FromSql och FromSqlInterpolated är säkra mot SQL-inmatning och integrerar alltid parameterdata som en separat SQL-parameter. Men metoden FromSqlRaw kan vara sårbar för SQL-inmatningsattacker om den används felaktigt. Mer information finns nedan.
I följande exempel skickas en enskild parameter till en lagrad procedur genom att inkludera en parameterplatshållare i SQL-frågesträngen och ange ytterligare ett argument:
var user = "johndoe";
var blogs = await context.Blogs
.FromSql($"EXECUTE dbo.GetMostPopularBlogsForUser {user}")
.ToListAsync();
Även om den här syntaxen kan se ut som vanlig C# stränginterpolation, omsluts det angivna värdet i en DbParameter
och det genererade parameternamnet infogas där platshållaren {0}
angavs. Detta gör FromSql säkra från SQL-inmatningsattacker och skickar värdet effektivt och korrekt till databasen.
När du kör lagrade procedurer kan det vara användbart att använda namngivna parametrar i SQL-frågesträngen, särskilt när den lagrade proceduren har valfria parametrar:
var user = new SqlParameter("user", "johndoe");
var blogs = await context.Blogs
.FromSql($"EXECUTE dbo.GetMostPopularBlogsForUser @filterByUser={user}")
.ToListAsync();
Om du behöver mer kontroll över databasparametern som skickas kan du även skapa en DbParameter
och ange den som ett parametervärde. På så sätt kan du ange den exakta databastypen för parametern, eller fasetter, till exempel dess storlek, precision eller längd:
var user = new SqlParameter("user", "johndoe");
var blogs = await context.Blogs
.FromSql($"EXECUTE dbo.GetMostPopularBlogsForUser {user}")
.ToListAsync();
Notera
Parametrarna som du skickar måste exakt matcha definitionen för lagrad procedur. Var särskilt uppmärksam på ordningen på parametrarna, var noga med att inte missa eller felplaceera någon av dem - eller överväg att använda namngiven parameter notation. Kontrollera också att parametertyperna motsvarar och att deras fasetter (storlek, precision, skala) anges efter behov.
Dynamisk SQL och parametrar
FromSql och dess parameterisering bör användas när det är möjligt. Det finns dock vissa scenarier där SQL måste delas dynamiskt och databasparametrar inte kan användas. Anta till exempel att en C#-variabel innehåller namnet på en egenskap som ska filtreras efter. Det kan vara frestande att använda en SQL-fråga, till exempel följande:
var propertyName = "User";
var propertyValue = "johndoe";
var blogs = await context.Blogs
.FromSql($"SELECT * FROM [Blogs] WHERE {propertyName} = {propertyValue}")
.ToListAsync();
Den här koden fungerar inte eftersom databaser inte tillåter parameterisering av kolumnnamn (eller någon annan del av schemat).
För det första är det viktigt att överväga konsekvenserna av att dynamiskt konstruera en fråga – via SQL eller på annat sätt. Om du accepterar ett kolumnnamn från en användare kan de välja en kolumn som inte är indexerad, vilket gör att frågan körs extremt långsamt och överbelastar databasen. eller så kan de välja en kolumn som innehåller data som du inte vill ska exponeras. Förutom verkligt dynamiska scenarier är det vanligtvis bättre att ha två frågor för två kolumnnamn, i stället för att använda parameterisering för att komprimera dem till en enda fråga.
Om du har bestämt dig för att skapa SQL dynamiskt måste du använda FromSqlRaw, som gör det möjligt att interpolera variabeldata direkt i SQL-strängen i stället för att använda en databasparameter:
var columnName = "Url";
var columnValue = new SqlParameter("columnValue", "http://SomeURL");
var blogs = await context.Blogs
.FromSqlRaw($"SELECT * FROM [Blogs] WHERE {columnName} = @columnValue", columnValue)
.ToListAsync();
I koden ovan infogas kolumnnamnet direkt i SQL med hjälp av C#-stränginterpolation. Det är ditt ansvar att se till att strängvärdet är säkert och att rensa det om det kommer från ett osäkert ursprung. Det innebär att du identifierar specialtecken som semikolon, kommentarer och andra SQL-konstruktioner och antingen tar bort dem på rätt sätt eller avvisar sådana indata.
Å andra sidan skickas kolumnvärdet via en DbParameter
och är därför säker inför SQL-inmatningen.
Varning
Var mycket försiktig när du använder FromSqlRawoch se alltid till att värdena antingen kommer från ett säkert ursprung eller är korrekt sanerade. SQL-inmatningsattacker kan få katastrofala konsekvenser för ditt program.
Skapa med LINQ
Du kan skriva ovanpå den första SQL-frågan med linq-operatorer. EF Core behandlar din SQL som en underfråga och skriver över den i databasen. I följande exempel används en SQL-fråga som väljer från en Table-Valued Funktion (TVF). Och sedan bygger vidare på det med LINQ för filtrering och sortering.
var searchTerm = "Lorem ipsum";
var blogs = await context.Blogs
.FromSql($"SELECT * FROM dbo.SearchBlogs({searchTerm})")
.Where(b => b.Rating > 3)
.OrderByDescending(b => b.Rating)
.ToListAsync();
Ovanstående fråga genererar följande SQL:
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM (
SELECT * FROM dbo.SearchBlogs(@p0)
) AS [b]
WHERE [b].[Rating] > 3
ORDER BY [b].[Rating] DESC
Inklusive relaterade data
Operatorn Include
kan användas för att läsa in relaterade data, precis som med andra LINQ-frågor:
var searchTerm = "Lorem ipsum";
var blogs = await context.Blogs
.FromSql($"SELECT * FROM dbo.SearchBlogs({searchTerm})")
.Include(b => b.Posts)
.ToListAsync();
För att skapa med LINQ måste SQL-frågan vara skrivbar, eftersom EF Core behandlar den angivna SQL som en underfråga. Sammansättningsbara SQL-frågor börjar vanligtvis med nyckelordet SELECT
och får inte innehålla SQL-funktioner som inte är giltiga i en underfråga, till exempel:
- Ett avslutande semikolon
- På SQL Server, ett avslutande tips på frågenivå (till exempel
OPTION (HASH JOIN)
) - På SQL Server används en
ORDER BY
-sats som inte används medOFFSET 0
ELLERTOP 100 PERCENT
iSELECT
-satsen
SQL Server tillåter inte sammansättning över lagrade proceduranrop, så alla försök att tillämpa ytterligare frågeoperatorer på ett sådant anrop resulterar i ogiltig SQL. Använd AsEnumerable eller AsAsyncEnumerable direkt efter FromSql eller FromSqlRaw för att se till att EF Core inte försöker skriva över en lagrad procedur.
Ändringsspårning
Frågor som använder FromSql eller FromSqlRaw följa exakt samma regler för ändringsspårning som andra LINQ-frågor i EF Core. Om frågan handlar om entitetstyper, spåras resultaten som standard.
I följande exempel används en SQL-fråga som väljer från en Table-Valued Function (TVF) och inaktiverar sedan ändringsspårning med anropet till AsNoTracking
:
var searchTerm = "Lorem ipsum";
var blogs = await context.Blogs
.FromSql($"SELECT * FROM dbo.SearchBlogs({searchTerm})")
.AsNoTracking()
.ToListAsync();
Fråga efter skalära typer (icke-entiteter)
Not
Den här funktionen introducerades i EF Core 7.0.
Även om FromSql är användbart för att köra frågor mot entiteter som definierats i din modell kan du SqlQuery enkelt fråga efter skalära typer som inte är entitetstyper via SQL, utan att behöva gå ned till API:er för dataåtkomst på lägre nivå. Följande fråga hämtar till exempel alla ID:t från tabellen Blogs
:
var ids = await context.Database
.SqlQuery<int>($"SELECT [BlogId] FROM [Blogs]")
.ToListAsync();
Du kan också skapa LINQ-operatorer i SQL-frågan. Men eftersom din SQL blir en underfråga vars utdatakolumn måste refereras av den SQL som EF lägger till, måste du namnge utdatakolumnen Value
. Följande fråga returnerar till exempel de ID:ar som ligger över ID-genomsnittet:
var overAverageIds = await context.Database
.SqlQuery<int>($"SELECT [BlogId] AS [Value] FROM [Blogs]")
.Where(id => id > context.Blogs.Average(b => b.BlogId))
.ToListAsync();
SqlQuery kan användas med alla skalära typer som stöds av databasprovidern. Om du vill använda en typ som inte stöds av databasprovidern kan du använda konfiguration före konventionen för att definiera en värdekonvertering för den.
SqlQueryRaw möjliggör dynamisk konstruktion av SQL-frågor, precis som FromSqlRaw gör för entitetstyper.
Kör icke-frågande SQL-kommandon
I vissa scenarier kan det vara nödvändigt att köra SQL som inte returnerar några data, vanligtvis för att ändra data i databasen eller anropa en lagrad procedur som inte returnerar några resultatuppsättningar. Detta kan göras via ExecuteSql:
using (var context = new BloggingContext())
{
var rowsModified = context.Database.ExecuteSql($"UPDATE [Blogs] SET [Url] = NULL");
}
Detta kör den angivna SQL-filen och returnerar antalet rader som ändrats. ExecuteSql skyddar mot SQL-inmatning med hjälp av säker parameterisering, precis som FromSql, och ExecuteSqlRaw möjliggör dynamisk konstruktion av SQL-frågor, precis som FromSqlRaw gör för frågor.
Not
Före EF Core 7.0 var det ibland nödvändigt att använda ExecuteSql
API:er för att utföra en "massuppdatering" på databasen, enligt ovan. Detta är betydligt effektivare än att fråga efter alla matchande rader och sedan använda SaveChanges
för att ändra dem. EF Core 7.0 introducerade ExecuteUpdate och ExecuteDelete, vilket gjorde det möjligt att uttrycka effektiva massuppdateringsåtgärder via LINQ. Vi rekommenderar att du använder dessa API:er när det är möjligt i stället för ExecuteSql
.
Begränsningar
Det finns några begränsningar att känna till när du returnerar entitetstyper från SQL-frågor:
- SQL-frågan måste returnera data för alla egenskaper av entitetstypen.
- Kolumnnamnen i resultatuppsättningen måste matcha kolumnnamnen som egenskaperna mappas till. Observera att det här beteendet skiljer sig från EF6. EF6 ignorerade egenskaps-till-kolumn-mappning för SQL-frågor, och kolumnnamn för resultatuppsättningar måste matcha dessa egenskapsnamn.
- SQL-frågan får inte innehålla relaterade data. I många fall kan du dock skriva ovanpå frågan med operatorn
Include
för att returnera relaterade data (se Inklusive relaterade data).