Поделиться через


Присвоение начальных значений данных

Заполнение данных — это процесс заполнения базы данных исходным набором данных.

Это можно сделать несколькими способами в EF Core:

Параметры конфигурации UseSeeding и UseAsyncSeeding методы

В EF 9 появились UseSeeding и UseAsyncSeeding методы, которые обеспечивают удобный способ заполнения базы данных начальными данными. Эти методы направлены на улучшение опыта использования пользовательской логики инициализации (описано ниже). Они предоставляют единое четкое местоположение, где можно разместить весь код начальной загрузки данных. Кроме того, код внутри методов UseSeeding и UseAsyncSeeding защищен механизмом блокировки миграции, чтобы предотвратить проблемы с параллелизмом.

Новые методы заполнения вызываются как часть операции EnsureCreated, команды Migrate и dotnet ef database update, даже если изменения модели отсутствуют и миграции не были применены.

Совет

Использование UseSeeding и UseAsyncSeeding рекомендуется как способ инициализации базы данных начальными данными в работе с EF Core.

Эти методы можно настроить на шаге конфигурации параметров. Рассмотрим пример:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
        .UseSeeding((context, _) =>
        {
            var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });

Примечание.

UseSeeding вызывается из метода EnsureCreated, и UseAsyncSeeding вызывается из метода EnsureCreatedAsync. При использовании этой функции рекомендуется реализовать оба UseSeedingUseAsyncSeeding метода с использованием аналогичной логики, даже если код с помощью EF является асинхронным. В настоящее время средства EF Core используют синхронную версию метода и не будут правильно заполнять базу данных, если UseSeeding метод не реализован.

Настраиваемая логика инициализации

Простой и эффективный способ выполнения заполнения данных — использовать SaveChangesAsync до начала выполнения основной логики приложения. Рекомендуется использовать UseSeeding и UseAsyncSeeding для этой цели, однако иногда использование этих методов не является хорошим решением. Пример сценария заключается в том, что для инициализации необходимо использовать два разных контекста в одной транзакции. Ниже приведен пример кода, выполняющий настраиваемую инициализацию в приложении напрямую:

using (var context = new DataSeedingContext())
{
    await context.Database.EnsureCreatedAsync();

    var testBlog = await context.Blogs.FirstOrDefaultAsync(b => b.Url == "http://test.com");
    if (testBlog == null)
    {
        context.Blogs.Add(new Blog { Url = "http://test.com" });
        await context.SaveChangesAsync();
    }
}

Предупреждение

Код инициализации не должен быть частью обычного выполнения приложения, так как это может привести к проблемам параллелизма при выполнении нескольких экземпляров, а также приложение должно иметь разрешение на изменение схемы базы данных.

В зависимости от ограничений развертывания код инициализации можно выполнять разными способами:

  • Локальное выполнение приложения инициализации
  • Развертывание приложения инициализации с помощью основного приложения, вызов подпрограммы инициализации и отключение или удаление приложения инициализации.

Обычно это можно автоматизировать с помощью профилей публикации.

Данные для управления моделью

Данные также могут быть связаны с типом сущности в рамках конфигурации модели. Затем миграции EF Core могут автоматически определять, какие операции вставки, обновления или удаления необходимо применить при обновлении базы данных до новой версии модели.

Предупреждение

Миграции учитывают изменения модели только при определении того, какая операция должна выполняться для получения управляемых данных в требуемое состояние. Таким образом, любые изменения данных, выполненные вне миграции, могут быть потеряны или вызваны ошибкой.

Например, это приведет к настройке управляемых данных для CountryOnModelCreating:

modelBuilder.Entity<Country>(b =>
{
    b.Property(x => x.Name).IsRequired();
    b.HasData(
        new Country { CountryId = 1, Name = "USA" },
        new Country { CountryId = 2, Name = "Canada" },
        new Country { CountryId = 3, Name = "Mexico" });
});

Чтобы добавить сущности, имеющие связь, необходимо указать значения внешнего ключа:

modelBuilder.Entity<City>().HasData(
    new City { Id = 1, Name = "Seattle", LocatedInId = 1 },
    new City { Id = 2, Name = "Vancouver", LocatedInId = 2 },
    new City { Id = 3, Name = "Mexico City", LocatedInId = 3 },
    new City { Id = 4, Name = "Puebla", LocatedInId = 3 });

При управлении данными для навигации "многие ко многим" необходимо явно настроить сущность соединения. Если тип сущности имеет какие-либо свойства в теневом состоянии (например LanguageCountry , сущность соединения ниже), анонимный класс можно использовать для предоставления значений:

modelBuilder.Entity<Language>(b =>
{
    b.HasData(
        new Language { Id = 1, Name = "English" },
        new Language { Id = 2, Name = "French" },
        new Language { Id = 3, Name = "Spanish" });

    b.HasMany(x => x.UsedIn)
        .WithMany(x => x.OfficialLanguages)
        .UsingEntity(
            "LanguageCountry",
            r => r.HasOne(typeof(Country)).WithMany().HasForeignKey("CountryId").HasPrincipalKey(nameof(Country.CountryId)),
            l => l.HasOne(typeof(Language)).WithMany().HasForeignKey("LanguageId").HasPrincipalKey(nameof(Language.Id)),
            je =>
            {
                je.HasKey("LanguageId", "CountryId");
                je.HasData(
                    new { LanguageId = 1, CountryId = 2 },
                    new { LanguageId = 2, CountryId = 2 },
                    new { LanguageId = 3, CountryId = 3 });
            });
});

Собственные типы сущностей можно настроить аналогичным образом:

modelBuilder.Entity<Language>().OwnsOne(p => p.Details).HasData(
    new { LanguageId = 1, Phonetic = false, Tonal = false, PhonemesCount = 44 },
    new { LanguageId = 2, Phonetic = false, Tonal = false, PhonemesCount = 36 },
    new { LanguageId = 3, Phonetic = true, Tonal = false, PhonemesCount = 24 });

Дополнительные сведения см. в полном примере проекта .

После добавления данных в модель следует использовать миграции для применения этих изменений.

Совет

Если необходимо применить миграции в рамках автоматического развертывания, можно создать скрипт SQL, который можно предварительно просмотреть перед выполнением.

Кроме того, можно использовать EnsureCreatedAsync для создания новой базы данных, содержащей управляемые данные, например, для тестовой базы данных или при использовании поставщика данных в памяти либо любой нереляционной базы данных. Обратите внимание, что если база данных уже существует, EnsureCreatedAsync не обновит схему или управляемые данные в базе данных. Для реляционных баз данных не следует вызывать EnsureCreatedAsync, если планируется использовать миграции.

Примечание.

Ранее метод HasData, используемый для наполнения базы данных, называли "data seeding" (посев данных). Это именование задает неправильные ожидания, так как функция имеет ряд ограничений и подходит только для определенных типов данных. Поэтому мы решили переименовать его в "данные, управляемые моделью". Методы UseSeeding и UseAsyncSeeding должны использоваться для общего заполнения данных.

Ограничения управляемых данных модели

Этот тип данных управляется миграциями, а скрипт для обновления данных, уже содержащихся в базе данных, необходимо создать без подключения к самой базе данных. Это накладывает некоторые ограничения:

  • Необходимо указать значение первичного ключа, даже если оно обычно создается базой данных. Он будет использоваться для обнаружения изменений данных между миграциями.
  • Ранее вставленные данные будут удалены, если первичный ключ изменен каким-либо образом.

Поэтому эта функция наиболее полезна для статических данных, которые, как ожидается, не изменяются вне миграции и не зависят от других компонентов базы данных, например ZIP-кодов.

Если сценарий включает в себя любой из следующих способов, рекомендуется использовать UseSeeding и UseAsyncSeeding методы, описанные в первом разделе:

  • Временные данные для тестирования
  • Данные, зависящие от состояния базы данных
  • Данные большого объема (исходные данные фиксируются в миграционных снимках, и большие объемы данных могут быстро привести к огромным файлам и снижению производительности).
  • Данные, для которых база данных должна генерировать ключевые значения, включая сущности, которые используют альтернативные ключи в качестве идентификатора.
  • Данные, требующие пользовательского преобразования (которые не обрабатываются преобразованиями значений), например хэширование паролей
  • Данные, требующие вызовов внешнего API, такие как роли ASP.NET Core Identity и создание пользователей
  • Данные, которые не исправлены и детерминированные, например начальное значение DateTime.Now.

Настройка миграции вручную

При добавлении миграции изменения в указанных данных HasData преобразуются в вызовы InsertData(), UpdateData(), и DeleteData(). Одним из способов обойти некоторые ограничения является ручное добавление вызовов HasData и пользовательских операций в миграцию.

migrationBuilder.InsertData(
    table: "Countries",
    columns: new[] { "CountryId", "Name" },
    values: new object[,]
    {
        { 1, "USA" },
        { 2, "Canada" },
        { 3, "Mexico" }
    });

migrationBuilder.InsertData(
    table: "Languages",
    columns: new[] { "Id", "Name", "Details_PhonemesCount", "Details_Phonetic", "Details_Tonal" },
    values: new object[,]
    {
        { 1, "English", 44, false, false },
        { 2, "French", 36, false, false },
        { 3, "Spanish", 24, true, false }
    });

migrationBuilder.InsertData(
    table: "Cites",
    columns: new[] { "Id", "LocatedInId", "Name" },
    values: new object[,]
    {
        { 1, 1, "Seattle" },
        { 2, 2, "Vancouver" },
        { 3, 3, "Mexico City" },
        { 4, 3, "Puebla" }
    });

migrationBuilder.InsertData(
    table: "LanguageCountry",
    columns: new[] { "CountryId", "LanguageId" },
    values: new object[,]
    {
        { 2, 1 },
        { 2, 2 },
        { 3, 3 }
    });