Entity FrameworkMiddleExperience

Какие ошибки делают команды, когда прячут сложность базы данных за Entity Framework?

Команды прячут за EF N+1 запросы, загружают таблицы в память вместо SQL-фильтра, применяют миграции без проверки DDL, забывают про индексы на FK и делают DbContext синглтоном — каждая из этих ошибок обнаруживается только в production.

Типичные ошибки команд при абстрагировании БД через EF

Entity Framework создаёт иллюзию, что разработчик может не знать SQL. Это работает до первой production-проблемы. Вот конкретные ошибки, которые происходят в реальных проектах.

Ошибка 1: N+1 запросы, спрятанные за навигационными свойствами

// Плохо: каждый вызов .Items генерирует отдельный SELECT
var orders = await db.Orders.Where(o => o.Status == "pending").ToListAsync();
foreach (var order in orders)
    Console.WriteLine($"{order.Id}: {order.Items.Count} items"); // N+1!

// Хорошо: один JOIN запрос
var orders = await db.Orders
    .Where(o => o.Status == "pending")
    .Include(o => o.Items)
    .ToListAsync();

Команды обнаруживают это только когда в таблице 10 000 заказов и страница начинает грузиться 30 секунд.

Ошибка 2: Загрузка всей таблицы с фильтрацией в памяти

// Плохо: EF загружает всех пользователей в память, потом фильтрует
var admins = (await db.Users.ToListAsync())
    .Where(u => u.Role == "admin");

// Хорошо: WHERE переходит в SQL
var admins = await db.Users
    .Where(u => u.Role == "admin")
    .ToListAsync();

Это происходит, когда ToListAsync() вызывается до Where(), или когда в LINQ используются методы, которые EF не умеет транслировать в SQL.

Ошибка 3: Миграции без проверки сгенерированного SQL

# Команда создаёт миграцию и сразу применяет в production
dotnet ef migrations add RenameColumn
dotnet ef database update  # ОПАСНО!

# Правильно: сначала проверить SQL
dotnet ef migrations script --idempotent
# Просмотреть ALTER TABLE, проверить что нет DROP COLUMN вместо RENAME
# Только потом применять

EF может сгенерировать DROP + ADD вместо RENAME для колонки с данными — все данные теряются.

Ошибка 4: Отсутствие индексов на FK-колонках

EF создаёт внешние ключи, но не всегда создаёт индексы на них. По умолчанию EF Core добавляет индексы на FK, но при ручной конфигурации через Fluent API это легко пропустить:

// В DbContext.OnModelCreating нужно явно добавить индекс
modelBuilder.Entity<Order>()
    .HasIndex(o => o.CustomerId)   // индекс на FK
    .HasIndex(o => o.CreatedAt);   // индекс для сортировки

Ошибка 5: DbContext как синглтон

// Плохо: DbContext не потокобезопасен!
services.AddSingleton<AppDbContext>();

// Правильно: Scoped — новый контекст на каждый HTTP-запрос
services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString)); // AddDbContext автоматически делает Scoped

Синглтон DbContext в многопоточном ASP.NET Core приложении приводит к race condition на ChangeTracker.

Ошибка 6: Слепое доверие к generated SQL для сложной аналитики

EF плохо справляется с оконными функциями, GROUP BY с HAVING, рекурсивными CTE. Команды пишут сложный LINQ, получают неоптимальный или неправильный SQL и не проверяют его через ToQueryString().

// Для сложных запросов — сразу сырой SQL
var report = await db.Database
    .SqlQueryRaw<SalesReport>(@"
        SELECT
            DATE_TRUNC('month', created_at) AS month,
            SUM(total) AS revenue,
            COUNT(*) AS order_count
        FROM orders
        WHERE created_at >= @start
        GROUP BY 1
        ORDER BY 1",
        new NpgsqlParameter("start", startDate))
    .ToListAsync();

Как предотвращать

  • Code review с обязательной проверкой сгенерированного SQL для новых запросов
  • MiniProfiler или EF Core logging в dev-окружении — видно все запросы в браузере
  • Интеграционные тесты с реальной PostgreSQL (через Testcontainers) — ловят N+1 и медленные запросы
  • Проверка миграций через --idempotent скрипт перед применением

Подводные камни

  • Lazy loading через прокси (UseLazyLoadingProxies) скрывает N+1 — кажется, что всё работает, пока БД маленькая.
  • Soft-delete через глобальный HasQueryFilter обходится через IgnoreQueryFilters() — разработчики делают это случайно.
  • Конкурентные migrations при горизонтальном масштабировании (несколько инстансов стартуют одновременно) — нужна блокировка или проверка в CI/CD.
  • Change tracker накапливает данные в long-lived DbContext — в background jobs нужно вызывать db.ChangeTracker.Clear() или пересоздавать контекст.
  • EF Core 6+ interceptors позволяют перехватывать и модифицировать SQL — команды используют их неаккуратно и ломают транзакции.
  • Обновление EF Core минорной версии может изменить генерируемый SQL — нужно тестировать производительность после обновления.
  • Отсутствие составных индексов при фильтрации по нескольким колонкам — EF не подсказывает, какие индексы нужны.

What hurts your answer

  • Перечислять ошибки без объяснения причин
  • Не отличать beginner mistakes от production failure modes
  • Не предлагать процесс, который предотвращает повторение ошибок

What they're listening for

  • Знает типичные ошибки при работе с Entity Framework
  • Понимает причины ошибок
  • Предлагает практики prevention и early detection

Related topics