HasData vs UseSeeding для заполнения данных в EF Core 11: что выбрать?
Используйте HasData только для фиксированных справочных данных, принадлежащих модели. Для всего остального в EF Core 11 используйте UseSeeding и UseAsyncSeeding. Сравнение бок о бок с правилами, которые предопределяют решение.
Для заполнения данных в EF Core 11 используйте HasData только для небольших, фиксированных, детерминированных справочных таблиц, которые вы хотите версионировать внутри своих миграций (коды стран, валюты, строки-перечисления для поиска). Для всего остального используйте UseSeeding и UseAsyncSeeding: всё, что связано с ключами, генерируемыми базой данных, вычисляемыми или недетерминированными значениями, условной логикой, свойствами навигации или строками, зависящими от того, что уже есть в базе данных. Команда EF переименовала HasData в “model-managed data” именно для того, чтобы отговорить от его использования как универсального средства заполнения. В этой статье используются .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0) и C# 14.
Если запомнить только одну строку: HasData участвует в модели и в diff миграции, а UseSeeding выполняется как обычный код против живого DbContext. Почти все болезненные ошибки заполнения возникают из-за выбора первого, когда нужно было второе.
Матрица возможностей
Это та таблица, ради которой вы пришли. Каждая строка - реальное ограничение, а не ощущение.
| Возможность | HasData (model-managed data) | UseSeeding / UseAsyncSeeding |
|---|---|---|
| Настраивается в | OnModelCreating (модель) | DbContextOptionsBuilder |
| Когда выполняется | Внутри SQL миграции (InsertData) | При Migrate/EnsureCreated и dotnet ef database update |
| Первичные ключи | Должны задаваться вручную | Ключи, генерируемые базой данных, работают нормально |
| Недетерминированные значения | Запрещены (повторный diff при каждой сборке) | Разрешены (Guid.NewGuid(), DateTime.UtcNow) |
| Свойства навигации | Нет, только внешние ключи | Да, вставка полных графов |
| Условная логика | Нет | Да, вы пишете if |
| Внешние вызовы / преобразования | Нет | Да (хеширование, HTTP, чтение файлов) |
| Идемпотентность | Автоматическая (EF делает diff) | Вы пишете проверку существования |
| Фиксируется в системе контроля версий | Да, в снимке миграции | Нет, это код запуска |
| Обновляет существующие строки | Да, через diff миграции | Только если вы напишете обновление |
| Доступно с | EF Core 1.0 (было HasData) | EF Core 9, актуально в EF Core 11 |
Самая важная строка из всех - “недетерминированные значения”. HasData сравнивается со снимком модели при каждой сборке, поэтому DateTime.UtcNow или Guid.NewGuid() в вашем заполнении заставляет модель выглядеть изменившейся каждый раз, и EF выбрасывает PendingModelChangesWarning или генерирует бесконечную струйку бессмысленных миграций. Это та стена, в которую упирается большинство, обычно во время миграции с EF Core 6 на EF Core 11.
Когда выбирать HasData
HasData - правильный инструмент, когда данные действительно являются частью смысла вашей схемы. Мысленная проверка: было бы вам комфортно жёстко прописать эти строки в константе, и будут ли они почти никогда не меняться?
- Фиксированные таблицы поиска. Коды стран ISO, коды валют, штаты США, перечисление статусов заказа, опирающееся на таблицу. Ключи стабильны, вы назначаете их сами, и вы хотите, чтобы значения поставлялись и версионировались вместе со схемой. В EF Core 11 вы настраиваете это в
OnModelCreating:
// .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), C# 14
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OrderStatus>().HasData(
new OrderStatus { Id = 1, Name = "Pending" },
new OrderStatus { Id = 2, Name = "Shipped" },
new OrderStatus { Id = 3, Name = "Delivered" });
}
-
Данные, которые вы хотите автоматически сравнивать и мигрировать. Измените
"Shipped"на"Dispatched", и следующийdotnet ef migrations addвыдаст вызовUpdateData. Вы получаете версионированные, проверяемые, отслеживаемые изменения заполнения в том же месте, что и схема. Это реальное преимущество, котороеUseSeedingне даёт бесплатно. -
Данные, без которых схема бессмысленна. Если внешний ключ в другой таблице указывает на
OrderStatus.Id = 2, а строка отсутствует, ваше приложение сломано. Запекание в миграцию гарантирует, что она появится синхронно со схемой, внутри той же транзакции.
Ключи должны задаваться явно, а значения должны быть детерминированными. Никаких Guid.NewGuid(), никаких DateTime.Now, никаких свойств навигации (задавайте столбец внешнего ключа напрямую). Нарушьте любое из этих условий, и HasData будет бороться с вами при каждой сборке.
Когда выбирать UseSeeding и UseAsyncSeeding
UseSeeding - рекомендуемый универсальный механизм в EF Core 11. Это обычный код приложения с живым DbContext, поэтому ограничений HasData попросту не существует. Прибегайте к нему всякий раз, когда верно любое из следующего:
-
База данных генерирует ключ. Столбцы identity, последовательности,
newsequentialid(): сHasDataвам пришлось бы изобретать ключи вручную и молиться, чтобы они никогда не столкнулись с реальными вставками.UseSeedingпозволяет базе данных назначать их. -
Значение вычисляется или недетерминировано. Хеширование пароля администратора по умолчанию, проставление
CreatedAt = DateTime.UtcNow, генерация токенаGuid. Ничто из этого не переживает diff модели, поэтому должно выполняться во время выполнения:
// .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), C# 14
services.AddDbContext<AppDbContext>(options =>
options
.UseSqlServer(connectionString)
.UseAsyncSeeding(async (context, _, ct) =>
{
if (!await context.Set<User>().AnyAsync(u => u.Email == "admin@example.com", ct))
{
context.Set<User>().Add(new User
{
Email = "admin@example.com",
PasswordHash = PasswordHasher.Hash("change-me"), // runtime transform
CreatedAt = DateTime.UtcNow // non-deterministic
});
await context.SaveChangesAsync(ct);
}
})
.UseSeeding((context, _) =>
{
if (!context.Set<User>().Any(u => u.Email == "admin@example.com"))
{
context.Set<User>().Add(new User
{
Email = "admin@example.com",
PasswordHash = PasswordHasher.Hash("change-me"),
CreatedAt = DateTime.UtcNow
});
context.SaveChanges();
}
}));
-
Вам нужна условная или зависящая от данных логика. “Заполнить демо-тенант только в
Development” или “вставить эти строки только если таблица пуста”. Это обычныйif, которыйHasDataвообще не может выразить. -
Вы вставляете граф объектов через свойства навигации. Блог с тремя записями, заказ с позициями или связь многие-ко-многим. С
UseSeedingвы строите граф на C# и позволяетеSaveChangesразобраться с внешними ключами. СHasDataвам пришлось бы вручную прописать каждую строку соединения.
Подвох тот, который большинство упускает: вы должны реализовать обе перегрузки, и проверка существования лежит на вас. Инструменты EF Core (dotnet ef database update) вызывают только синхронный UseSeeding; ваш путь во время выполнения вызывает UseAsyncSeeding. Реализуйте одно и не реализуйте другое, и заполнение молча ничего не сделает с того пути, который вы забыли. Полные механизмы смотрите в как заполнять данные с UseSeeding и UseAsyncSeeding.
Что на самом деле происходит на диске
Самый ясный способ увидеть разницу - посмотреть, что производит каждый.
HasData материализуется в вашей миграции. После dotnet ef migrations add SeedStatuses сгенерированный метод Up содержит вызовы в форме SQL:
// .NET 11, EF Core 11 generated migration
migrationBuilder.InsertData(
table: "OrderStatuses",
columns: new[] { "Id", "Name" },
values: new object[,]
{
{ 1, "Pending" },
{ 2, "Shipped" },
{ 3, "Delivered" }
});
Данные теперь являются версионированным артефактом. Они выполняются один раз, внутри транзакции миграции, и EF отслеживает их в снимке модели, поэтому позднее редактирование производит UpdateData. Именно поэтому недетерминированное значение здесь яд: снимок никогда не совпал бы, поэтому EF считал бы, что модель менялась при каждой отдельной сборке.
UseSeeding не производит ничего на диске. Это callback, который срабатывает после того, как схема приведена в актуальное состояние, при каждом Migrate, EnsureCreated или dotnet ef database update, включая запуски, при которых не была применена ни одна миграция. Поскольку он выполняется безусловно, проверка существования - не опциональная уборка, а единственное, что стоит между вами и дублирующимися строками. EF Core 11 защищает callback тем же механизмом блокировки миграций, который он использует для миграций, поэтому два экземпляра приложения, запускающихся одновременно, не выполнят оба заполнение конкурентно, но блокировка не делает отсутствующий if идемпотентным.
Подвох, который решает за вас
Несколько ограничений полностью перевешивают предпочтения. Если применимо любое из них, решение принято за вас:
-
Ключи, генерируемые базой данных, вынуждают
UseSeeding. Если вы не можете или не хотите жёстко прописывать первичные ключи,HasDataотпадает. У него нет способа заполнить строку, ключ которой назначает база данных. -
Недетерминированные или вычисляемые значения вынуждают
UseSeeding. Хешированный пароль, временная метка, случайный токен: в момент появления одного из нихHasDataпревращает каждую сборку в фантомную миграцию. Это самая частая причина, по которой команды вырываютHasData. -
Фиксированная таблица-перечисление, на которую указывают другие строки, благоприятствует
HasData. Когда ссылочная целостность зависит от существования заполнения до любых реальных данных, вы хотите его внутри транзакции миграции, а не в callback запуска, который может выбросить исключение и оставить схему наполовину заполненной. -
Обязательный, генерируемый базой данных ключ со строкой
HasDataвызывает ошибку. Если вы попытаетесь заполнить сущность черезHasData, не предоставив её ключ, EF выброситThe seed entity for entity type 'X' cannot be added because a non-zero value is required for property 'Id'. Эта ошибка - то, как EF говорит вам, что вы выбрали не тот механизм: смотрите решение для этого конкретного сообщения.
Вы вполне можете использовать оба в одном приложении. Распространённое здоровое разделение: HasData для трёх таблиц поиска, без которых ваша схема не может функционировать, и UseSeeding для демо-данных, администратора по умолчанию и всего, что специфично для тенанта. Они не конфликтуют, потому что выполняются на разных этапах.
Рекомендация, повторно
По умолчанию используйте UseSeeding и UseAsyncSeeding в EF Core 11. Это рекомендуемый механизм, они обрабатывают динамические, с генерируемыми ключами, условные данные, которые реальные приложения действительно заполняют, и они падают громко, а не молча портят вашу историю миграций. Оставьте HasData для узкого случая, ради которого он был переименован: небольшие, фиксированные, детерминированные справочные данные с вручную назначенными ключами, которые вы действительно хотите версионировать и сравнивать вместе со своей схемой.
Ловушка, которой следует избегать, - исторический выбор по умолчанию. Годами HasData был единственным встроенным вариантом, поэтому кодовые базы рефлекторно тянулись к нему и затем тонули в фантомных миграциях и PendingModelChangesWarning. Если вы начинаете с нуля на EF Core 11, переверните этот инстинкт: сначала UseSeeding, HasData только когда вы можете назвать, почему данные принадлежат модели. Если вы сопровождаете сущности на основе records, то же разделение применяется чисто, поскольку records хорошо работают с EF Core 11 при обоих механизмах.
Связанное
- Как заполнять данные с UseSeeding и UseAsyncSeeding в EF Core 11
- Как заполнить связь многие-ко-многим в EF Core 11
- Решение: the seed entity cannot be added because a non-zero value is required for property Id
- Миграция с EF Core 6 на EF Core 11: breaking changes, которые действительно кусаются
- Как правильно использовать records с EF Core 11
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.