Как сравнить Entity Framework с raw SQL, query builder и другими ORM по контролю, типобезопасности и производительности?
EF Core даёт типобезопасность и миграции за счёт меньшего контроля над SQL; Dapper — минималистичный маппинг без overhead; ADO.NET — максимальный контроль и скорость. Гибрид EF + Dapper — практический стандарт.
Четыре подхода к работе с БД в .NET
В экосистеме .NET существуют четыре принципиально разных подхода к доступу к данным. Выбор между ними определяется требованиями к контролю, типобезопасности и производительности.
Raw SQL (ADO.NET / Npgsql)
Полный контроль над каждым байтом запроса. Разработчик пишет SQL вручную и маппит результат на объекты самостоятельно.
await using var conn = new NpgsqlConnection(connectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand(
"SELECT id, name FROM products WHERE category_id = $1 AND price < $2",
conn);
cmd.Parameters.AddWithValue(1, categoryId);
cmd.Parameters.AddWithValue(2, maxPrice);
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
yield return new Product(reader.GetInt32(0), reader.GetString(1));
Плюсы: нет накладных расходов ORM, любой SQL (COPY, оконные функции, CTE). Минусы: нет типобезопасности на этапе компиляции, большой бойлерплейт, нет автоматических миграций.
Query Builder (SqlKata, Dapper)
Dapper — micro-ORM: добавляет только маппинг результата, SQL пишется вручную.
// Dapper
using var conn = new NpgsqlConnection(connectionString);
var products = await conn.QueryAsync<Product>(
"SELECT id, name, price FROM products WHERE category_id = @CategoryId",
new { CategoryId = 5 });
// SqlKata — query builder с флюентным API
var query = new Query("products")
.Where("category_id", categoryId)
.Where("price", "<", maxPrice)
.Select("id", "name");
var sql = compiler.Compile(query).Sql; // проверяемо в тестах
Плюсы: нет ORM overhead, простой маппинг, лёгкость отладки. Минусы: нет change tracking, нет миграций, строковые имена колонок не проверяются компилятором.
Entity Framework Core
Полноценный ORM: LINQ → SQL, change tracking, миграции, навигационные свойства.
var products = await db.Products
.Where(p => p.CategoryId == categoryId && p.Price < maxPrice)
.Include(p => p.Category)
.Select(p => new ProductDto(p.Id, p.Name, p.Category.Name))
.ToListAsync();
Плюсы: типобезопасные запросы, автоматический маппинг, миграции, интеграция с DI. Минусы: сложно предсказать SQL для нетривиальных запросов, overhead change tracker для read-only, bulk-операции требуют расширений.
Другие ORM: NHibernate, Marten
NHibernate — более зрелый ORM с XML/fluent-конфигурацией, поддерживает сложные сценарии кеширования второго уровня. Marten использует PostgreSQL как документ-ориентированную БД (JSONB) с LINQ-поиском поверх.
Сравнительная таблица
- Контроль над SQL: ADO.NET = 100%, Dapper = 95%, EF Core = 60–80%, NHibernate = 50–70%
- Типобезопасность: EF Core = высокая (Expression Tree), Dapper = низкая (строки), ADO.NET = нет
- Производительность: ADO.NET ≈ Dapper > EF Core (AsNoTracking) > EF Core (tracking) > NHibernate (с кешем L2 — зависит)
- Скорость разработки: EF Core > Dapper > ADO.NET для CRUD; ADO.NET > все для аналитики
- Миграции: только EF Core и NHibernate из коробки
Практический совет
В зрелых .NET-проектах часто используют гибрид: EF Core для транзакционных операций (создание/обновление сущностей) и Dapper или SqlQueryRaw для аналитических отчётов и batch-запросов. Это лучше, чем пытаться сделать всё через один инструмент.
Подводные камни
- EF Core не всегда генерирует оптимальный SQL для сложных LINQ — нужно проверять через
ToQueryString()или логирование. - Dapper без параметризации уязвим к SQL-инъекциям — интерполяция строк в запросах запрещена.
- NHibernate session не равна EF DbContext — разные модели жизненного цикла, нельзя смешивать концепции.
- EF Core проекция через
Selectс анонимным типом не поддерживает обновление — только чтение. - Производительность EF Core на INSERT 10 000 строк в 10–20 раз хуже, чем Npgsql COPY — для bulk нужны расширения.
- Dapper QueryMultiple требует правильного порядка чтения результатов — ошибка порядка даёт неочевидный баг.
- EF Core migrations в multi-tenant БД требуют кастомного MigrationHistory-провайдера — стандартный не работает.
What hurts your answer
- Объяснять Entity Framework только через определение, без задачи и границ применимости
- Перечислять функции Entity Framework, но не показывать, какую проблему они решают
- Называть Entity Framework универсальным выбором для любых проектов
What they're listening for
- Понимает назначение Entity Framework, а не только определение
- Связывает инструмент с классом задач и ограничениями
- Умеет объяснить технологию простым языком