Start Debugging

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 - правильный инструмент, когда данные действительно являются частью смысла вашей схемы. Мысленная проверка: было бы вам комфортно жёстко прописать эти строки в константе, и будут ли они почти никогда не меняться?

// .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" });
}

Ключи должны задаваться явно, а значения должны быть детерминированными. Никаких Guid.NewGuid(), никаких DateTime.Now, никаких свойств навигации (задавайте столбец внешнего ключа напрямую). Нарушьте любое из этих условий, и HasData будет бороться с вами при каждой сборке.

Когда выбирать UseSeeding и UseAsyncSeeding

UseSeeding - рекомендуемый универсальный механизм в EF Core 11. Это обычный код приложения с живым DbContext, поэтому ограничений HasData попросту не существует. Прибегайте к нему всякий раз, когда верно любое из следующего:

// .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();
            }
        }));

Подвох тот, который большинство упускает: вы должны реализовать обе перегрузки, и проверка существования лежит на вас. Инструменты 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 идемпотентным.

Подвох, который решает за вас

Несколько ограничений полностью перевешивают предпочтения. Если применимо любое из них, решение принято за вас:

Вы вполне можете использовать оба в одном приложении. Распространённое здоровое разделение: HasData для трёх таблиц поиска, без которых ваша схема не может функционировать, и UseSeeding для демо-данных, администратора по умолчанию и всего, что специфично для тенанта. Они не конфликтуют, потому что выполняются на разных этапах.

Рекомендация, повторно

По умолчанию используйте UseSeeding и UseAsyncSeeding в EF Core 11. Это рекомендуемый механизм, они обрабатывают динамические, с генерируемыми ключами, условные данные, которые реальные приложения действительно заполняют, и они падают громко, а не молча портят вашу историю миграций. Оставьте HasData для узкого случая, ради которого он был переименован: небольшие, фиксированные, детерминированные справочные данные с вручную назначенными ключами, которые вы действительно хотите версионировать и сравнивать вместе со своей схемой.

Ловушка, которой следует избегать, - исторический выбор по умолчанию. Годами HasData был единственным встроенным вариантом, поэтому кодовые базы рефлекторно тянулись к нему и затем тонули в фантомных миграциях и PendingModelChangesWarning. Если вы начинаете с нуля на EF Core 11, переверните этот инстинкт: сначала UseSeeding, HasData только когда вы можете назвать, почему данные принадлежат модели. Если вы сопровождаете сущности на основе records, то же разделение применяется чисто, поскольку records хорошо работают с EF Core 11 при обоих механизмах.

Связанное

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад