Присвоение начальных значений данных
Заполнение данных — это процесс заполнения базы данных исходным набором данных.
Это можно сделать несколькими способами в EF Core:
- Настройка параметров заполнения данных (
UseSeeding
) - Настраиваемая логика инициализации
- Управляемые данные модели (
HasData
) - Настройка миграции вручную
Параметры конфигурации UseSeeding
и UseAsyncSeeding
методы
В EF 9 появились UseSeeding
и UseAsyncSeeding
методы, которые обеспечивают удобный способ заполнения базы данных начальными данными. Эти методы направлены на улучшение опыта использования пользовательской логики инициализации (описано ниже). Они предоставляют одно четкое расположение, где можно разместить весь код заполнения данных. Кроме того, код внутри UseSeeding
и UseAsyncSeeding
методы защищены механизмом блокировки миграции, чтобы предотвратить проблемы с параллелизмом.
Новые методы заполнения вызываются как часть EnsureCreated
операции и dotnet ef database update
команды, Migrate
даже если изменения модели отсутствуют и миграции не были применены.
Совет
Использование 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
вызывается из метода и UseAsyncSeeding
вызывается из EnsureCreated
EnsureCreatedAsync
метода. При использовании этой функции рекомендуется реализовать оба UseSeeding
UseAsyncSeeding
метода с использованием аналогичной логики, даже если код с помощью EF является асинхронным. В настоящее время средства EF Core используют синхронную версию метода и не будут правильно заполнять базу данных, если UseSeeding
метод не реализован.
Настраиваемая логика инициализации
Простой и эффективный способ выполнения заполнения данных — использовать DbContext.SaveChanges()
до начала выполнения основной логики приложения. Рекомендуется использовать UseSeeding
и UseAsyncSeeding
для этой цели, однако иногда использование этих методов не является хорошим решением. Пример сценария заключается в том, что для заполнения требуется использовать два разных контекста в одной транзакции. Ниже приведен пример кода, выполняющий настраиваемую инициализацию в приложении напрямую:
using (var context = new DataSeedingContext())
{
context.Database.EnsureCreated();
var testBlog = context.Blogs.FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Blogs.Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
}
Предупреждение
Код начального значения не должен быть частью нормального выполнения приложения, так как это может привести к проблемам параллелизма при выполнении нескольких экземпляров, а также требуется, чтобы приложение иметь разрешение на изменение схемы базы данных.
В зависимости от ограничений развертывания код инициализации можно выполнять разными способами:
- Локальное выполнение приложения инициализации
- Развертывание приложения инициализации с помощью основного приложения, вызов подпрограммы инициализации и отключение или удаление приложения инициализации.
Обычно это можно автоматизировать с помощью профилей публикации.
Управляемые данные модели
Данные также могут быть связаны с типом сущности в рамках конфигурации модели. Затем миграции EF Core могут автоматически вычислять операции вставки, обновления или удаления, которые необходимо применить при обновлении базы данных до новой версии модели.
Предупреждение
Миграции учитывают изменения модели только при определении того, какая операция должна выполняться для получения управляемых данных в требуемое состояние. Таким образом, любые изменения данных, выполненные вне миграции, могут быть потеряны или вызваны ошибкой.
Например, это приведет к настройке управляемых данных для Country
OnModelCreating
:
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, который можно предварительно просмотреть перед выполнением.
Кроме того, можно использовать context.Database.EnsureCreated()
для создания новой базы данных, содержащей управляемые данные, например для тестовой базы данных или при использовании поставщика в памяти или любой нереляционной базы данных. Обратите внимание, что если база данных уже существует, EnsureCreated()
не обновит схему или управляемые данные в базе данных. Для реляционных баз данных не следует вызывать EnsureCreated()
, если планируется использовать миграции.
Примечание.
Заполнение базы данных с помощью метода, используемого HasData
для обозначения "заполнение данных". Это именование задает неправильные ожидания, так как функция имеет ряд ограничений и подходит только для определенных типов данных. Именно поэтому мы решили переименовать его в "управляемые данные модели". 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 }
});