Ef Core
10.10. Raw SQL та Stored Procedures
10.10. Raw SQL та Stored Procedures
Вступ: Коли LINQ недостатньо
LINQ to Entities покриває більшість запитів, але є сценарії, де потрібний чистий SQL: складні аналітичні запити, виклик stored procedures, оптимізований SQL з hints, використання database-specific функцій. EF Core надає кілька способів виконання Raw SQL, зберігаючи переваги маппінгу та параметризації.
Передумови: 10.5. LINQ to Entities, знання SQL.
FromSqlRaw та FromSqlInterpolated
FromSqlInterpolated (рекомендований — безпечний)
using var context = new LibraryContext();
string author = "Мартін";
int minYear = 2000;
// Інтерполяція автоматично створює SqlParameter
var books = context.Books
.FromSqlInterpolated($@"
SELECT * FROM Books
WHERE Author LIKE {'%' + author + '%'}
AND Year >= {minYear}")
.ToList();
// SQL: SELECT * FROM Books WHERE Author LIKE @p0 AND Year >= @p1
// Параметри: @p0 = '%Мартін%', @p1 = 2000
FromSqlInterpolatedавтоматично перетворює {value} на SqlParameter. Це безпечно від SQL Injection, незважаючи на те, що виглядає як рядкова інтерполяція!FromSqlRaw (для динамічного SQL)
// Коли потрібен динамічний SQL
string tableName = "Books"; // УВАГА: не від користувача!
var books = context.Books
.FromSqlRaw($"SELECT * FROM {tableName} ORDER BY Title")
.ToList();
// З ручними параметрами
var books2 = context.Books
.FromSqlRaw(
"SELECT * FROM Books WHERE Year BETWEEN @start AND @end",
new SqlParameter("@start", 2000),
new SqlParameter("@end", 2024))
.ToList();
FromSqlRaw з рядковою інтерполяцією НЕ створює параметри — це пряма конкатенація і потенційна SQL Injection! Використовуйте FromSqlInterpolated або явні SqlParameter.Composable запити
Запити FromSql можна комбінувати з LINQ:
// FromSql + LINQ = комбінований запит
var result = context.Books
.FromSqlInterpolated($"SELECT * FROM Books WHERE Year > {2020}")
.Where(b => b.IsAvailable) // Додає AND IsAvailable = 1
.OrderBy(b => b.Title) // Додає ORDER BY
.Select(b => new { b.Title, b.Year }) // Тільки потрібні стовпці
.ToList();
// SQL: SELECT Title, Year FROM (SELECT * FROM Books WHERE Year > @p0) AS b
// WHERE b.IsAvailable = 1 ORDER BY b.Title
ExecuteSqlRaw / ExecuteSqlInterpolated
Для операцій, що не повертають сутності (INSERT, UPDATE, DELETE):
// Масове оновлення
int affected = context.Database
.ExecuteSqlInterpolated($@"
UPDATE Books SET IsAvailable = {false}
WHERE Year < {1990}");
Console.WriteLine($"Оновлено: {affected}");
// Виклик stored procedure
context.Database
.ExecuteSqlInterpolated($"EXEC sp_ArchiveOldBooks @CutoffYear = {2000}");
Stored Procedures
Виклик stored procedure для читання
// Stored procedure повертає дані → FromSql
var books = context.Books
.FromSqlInterpolated($"EXEC sp_GetBooksByAuthor @AuthorName = {"Мартін"}")
.ToList();
Виклик з OUTPUT параметрами
var outputParam = new SqlParameter
{
ParameterName = "@TotalCount",
SqlDbType = SqlDbType.Int,
Direction = ParameterDirection.Output
};
context.Database.ExecuteSqlRaw(
"EXEC sp_GetBookStats @TotalCount = @TotalCount OUTPUT",
outputParam);
int totalCount = (int)outputParam.Value!;
Keyless Entities (Views та Custom Queries)
Для SQL Views та запитів, що не мають Primary Key:
// Keyless entity
[Keyless]
public class BookStatistics
{
public string Author { get; set; } = "";
public int BookCount { get; set; }
public int AvgYear { get; set; }
}
// DbContext
public DbSet<BookStatistics> BookStatistics => Set<BookStatistics>();
// Конфігурація
modelBuilder.Entity<BookStatistics>(entity =>
{
entity.HasNoKey();
entity.ToView("vw_BookStatistics"); // Маппінг на View
});
// Використання
var stats = context.BookStatistics.ToList();
SqlQuery<T> (EF Core 8+)
Для запитів, що повертають прості типи:
// Скалярний запит
var years = context.Database
.SqlQuery<int>($"SELECT DISTINCT Year AS Value FROM Books ORDER BY Year")
.ToList();
// Composable
var recentYears = context.Database
.SqlQuery<int>($"SELECT DISTINCT Year AS Value FROM Books")
.Where(y => y > 2010)
.ToList();
Коли використовувати Raw SQL
| Сценарій | LINQ | Raw SQL |
|---|---|---|
| Простий CRUD | ✅ | ❌ |
| WHERE + ORDER BY | ✅ | ❌ |
| GROUP BY + агрегація | ✅ | ❌ |
| Складні JOIN (5+ таблиць) | ⚠️ | ✅ |
| Window Functions (ROW_NUMBER) | ❌ | ✅ |
| PIVOT/UNPIVOT | ❌ | ✅ |
| Full-Text Search | ❌ | ✅ |
| Stored Procedures | ❌ | ✅ |
| Views | ✅ (Keyless) | ✅ |
| Bulk UPDATE/DELETE | ✅ (EF7+) | ✅ |
Практичні завдання
Завдання 1: FromSql + LINQ
- Напишіть Raw SQL запит для пошуку книг за автором.
- Додайте LINQ
.Where()та.OrderBy()поверх Raw SQL. - Перевірте згенерований SQL (composable query).
Завдання 2: Stored Procedure
- Створіть SP
sp_GetBooksByYearз параметром@Year. - Викличте з EF Core через
FromSql. - Додайте OUTPUT параметр для загальної кількості.
Завдання 3: Keyless Entity + View
- Створіть SQL View
vw_AuthorStats(Author, BookCount, AvgYear, LatestBook). - Keyless entity
AuthorStatView. - Запит через
context.Set<AuthorStatView>().ToList().
Резюме
FromSqlInterpolated
Безпечний Raw SQL з автоматичною параметризацією. Composable з LINQ.
ExecuteSql
INSERT/UPDATE/DELETE без повернення сутностей. Для масових операцій та SP.
Keyless Entities
HasNoKey() для Views та custom queries. Тільки read-only, без Change Tracking.
SqlQuery<T>
EF Core 8+. Запити з простими типами (int, string). Composable з LINQ.