Entity FrameworkJuniorExperience

Как сравнить 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, а не только определение
  • Связывает инструмент с классом задач и ограничениями
  • Умеет объяснить технологию простым языком

Related topics