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