数据种子设定
数据播种是使用一组初始数据填充数据库的过程。
可通过多种方式在 EF Core 中完成此过程:
配置选项 UseSeeding
和 UseAsyncSeeding
方法
EF 9 引入了 UseSeeding
和 UseAsyncSeeding
方法,提供了一种使用初始数据为数据库设定种子的便捷方法。 这些方法旨在改进使用自定义初始化逻辑的体验(如下所述)。 它们提供了一个明确的位置,可在其中放置所有数据种子设定代码。 此外,内部代码 UseSeeding
和 UseAsyncSeeding
方法受迁移锁定机制的保护,以防止并发问题。
新的种子设定方法作为 EnsureCreated
操作、Migrate
和 dotnet ef database update
命令的一部分被调用,即使没有模型更改,也没有应用迁移。
提示
使用 EF Core 时,建议使用 UseSeeding
和 UseAsyncSeeding
并利用初始数据对数据库进行种子设定。
这些方法可以在选项配置步骤中设置。 下面是一个示例:
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
方法调用。 使用此功能时,建议使用类似的逻辑实现 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 迁移可以自动计算需要应用的插入、更新或删除操作。
警告
迁移仅在确定应执行何种操作以使托管数据达到所需状态时考虑模型更改。 因此,对迁移之外执行的任何数据更改都可能会丢失或导致错误。
例如,这将为 OnModelCreating
中的 Country
配置托管数据:
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
方法应用于常规用途数据种子设定。
模型托管数据的限制
这种类型的数据由迁移管理,并且需要在不连接到数据库的情况下生成用于更新数据库中已有数据的脚本。 这施加了一些限制:
- 即使主键值通常由数据库生成,也需要指定它。 它将用于检测迁移之间的数据更改。
- 如果以任何方式更改主键,则先前插入的数据将被删除。
因此,此功能最适用于不应在迁移外部更改且不依赖于数据库中任何其他内容(例如邮政编码)的静态数据。
如果方案包括以下任何一种,建议使用第一节中描述的 UseSeeding
和 UseAsyncSeeding
方法:
- 用于测试的临时数据
- 依赖于数据库状态的数据
- 数据量较大(种子设定数据在迁移快照中捕获,而数据量较大可能会快速导致大型文件和性能下降)。
- 需要由数据库生成键值的数据,包括使用备用键作为标识的实体
- 需要自定义转换(不由值转换处理)的数据,例如某些密码哈希
- 需要调用外部 API 的数据,例如 ASP.NET Core 标识角色和用户创建
手动迁移自定义
添加迁移时,对使用 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 }
});