使用 ModelBuilder 在 EFCore5 中播种多对多数据库?

Zan*_*aes 5 entity-framework-core .net-core

关于在实体框架中播种多对多关系存在许多 问题。然而,它们中的大多数都非常古老,并且多对多行为在 EFCore5 中发生了显着变化。官方文档建议重写OnModelCreating以实现ModelBuilder.Entity<>.HasData().

然而,通过新的多对多行为(没有显式映射),我找不到明确的路径来播种中间表。为了使用本教程的示例,该类BookCategories现在是隐式的。因此,在播种时没有显式声明中间表值的路径。

我也尝试过简单地分配数组,例如:

public class Book
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public ICollection<Category> Categories { get; set; }
}  
public class Category
{
    public int CategoryId { get; set; }
    public string CategoryName { get; set; }
    public ICollection<Book> Books { get; set; }
}  
Run Code Online (Sandbox Code Playgroud)

然后在种子时间:

Book book = new Book() { BookId = 1, Title = "Brave New World" }

Category category = new Category() { CategoryId = 1, CategoryName = "Dystopian" }

category.Books = new List<Book>() { book };
book.Categories = new List<Category>() { category };

modelBuilder.Entity<Book>().HasData(book);
modelBuilder.Entity<Category>().HasData(category);
Run Code Online (Sandbox Code Playgroud)

BookCategories...但在生成的迁移中没有创建任何条目。这在某种程度上是预料之中的,因为本文建议必须明确为中间表设定种子。我想要的是这样的:

modelBuilder.Entity<BookCategory>().HasData(
  new BookCategory() { BookId = 1, CategoryId = 1 }
);
Run Code Online (Sandbox Code Playgroud)

然而,同样,由于 EFCore5 中没有具体的类来描述BookCategories,我能想到的播种表的唯一方法是使用附加MigrationBuilder.InsertData 命令手动编辑迁移,这违背了通过应用程序代码播种数据的目的。

Iva*_*oev 8

BookCategories然而,同样,由于EFCore5 中没有具体的类来描述

实际上,正如“新增功能”链接中所解释的,EF Core 5 允许您拥有显式联接实体

public class BookCategory
{
    public int BookId { get; set; }
    public EBook Book { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

并配置多对多关系来使用它

modelBuilder.Entity<Book>()
    .HasMany(left => left.Categories)
    .WithMany(right => right.Books)
    .UsingEntity<BookCategory>(
        right => right.HasOne(e => e.Category).WithMany(),
        left => left.HasOne(e => e.Book).WithMany().HasForeignKey(e => e.BookId),
        join => join.ToTable("BookCategories")
    );
Run Code Online (Sandbox Code Playgroud)

这样您就可以使用所有正常的实体操作(查询、更改跟踪、数据模型播种等)

modelBuilder.Entity<BookCategory>().HasData(
  new BookCategory() { BookId = 1, CategoryId = 1 }
);
Run Code Online (Sandbox Code Playgroud)

仍然具有新的多对多跳过导航映射。

这可能是最简单也是类型安全的方法。

如果您觉得太多,也可以使用传统的连接实体,但您需要知道共享字典实体类型名称以及两个影子属性名称。按照惯例,您将看到的可能不是您所期望的。

因此,按照惯例,连接实体(和表)名称是

{LeftEntityName}{RightEntityName}

影子属性(和列)名称是

  • {LeftEntityNavigationPropertyName}{RightEntityKeyName}
  • {RightEntityNavigationPropertyName}{LeftEntityKeyName}

第一个问题是 - 哪个是左/右实体?答案是(尚未记录) - 按照惯例,左侧实体是按字母顺序排列名称较小的实体。因此,您的示例Book是左,Category是右,因此连接实体和表名称将为BookCategory

可以更改添加显式

modelBuilder.Entity<Category>()
    .HasMany(left => left.Books)
    .WithMany(right => right.Categories);
Run Code Online (Sandbox Code Playgroud)

现在就是了CategoryBook

在这两种情况下,影子属性(和列)名称都是

  • CategoriesCategoryId
  • BooksBookId

因此,表名和属性/列名都不是您通常所做的。

除了数据库表/列名称之外,实体和属性名称也很重要,因为您需要它们来进行实体操作,包括相关的数据播种。

话虽这么说,即使您不创建显式联接实体,最好流畅地配置 EF Core 约定自动创建的实体:

modelBuilder.Entity<Book>()
    .HasMany(left => left.Categories)
    .WithMany(right => right.Books)
    .UsingEntity("BookCategory", typeof(Dictionary<string, object>),
        right => right.HasOne(typeof(Category)).WithMany().HasForeignKey("CategoryId"),
        left => left.HasOne(typeof(Book)).WithMany().HasForeignKey("BookId"),
        join => join.ToTable("BookCategories")
    );
Run Code Online (Sandbox Code Playgroud)

现在您可以使用实体名称来访问EntityTypeBuilder

modelBuilder.Entity("BookCategories")
Run Code Online (Sandbox Code Playgroud)

您可以像具有匿名类型的影子 FK 属性的普通实体一样为其播种

modelBuilder.Entity("BookCategories")
Run Code Online (Sandbox Code Playgroud)

或者对于这个特定的属性包类型实体,也带有Dictionary<string, object>实例

modelBuilder.Entity("BookCategory").HasData(
  new { BookId = 1, CategoryId = 1 }
);
Run Code Online (Sandbox Code Playgroud)

更新:

人们似乎误解了上述“额外”步骤,并发现它们是多余的、“太多”、不需要的。

我从未说过它们是强制性的。如果您知道常规连接实体和属性名称,请直接进入最后一步并使用匿名类型或Dictionary<string, object>.

我已经解释了采用该路线的缺点 - 失去 C# 类型安全性并使用不受控制的“魔法”字符串。您必须足够聪明才能了解确切的 EF Core 命名约定,并意识到如果将类重命名BookEBook新的联接实体/表名称将从“BookCategory”更改为“CategoryEBook”以及 PK 属性/的顺序列、关联索引等

关于数据播种的具体问题。如果您真的想概括它(OP 在自己的答案中尝试),至少通过使用 EF Core 元数据系统而不是反射和假设来正确实现它。例如,以下代码将从 EF Core 元数据中提取这些名称:

modelBuilder.Entity("BookCategory").HasData(
  new Dictionary<string, object> { ["BookId"] = 1, ["CategoryId"] = 1 }
);
Run Code Online (Sandbox Code Playgroud)

另外,在这里您不需要知道哪种类型是“左”,哪种类型是“右”,也不需要特殊的基类或接口。只需传递实体对的序列,它就会正确地播种传统的连接实体,例如使用 OP 示例,两者

public static void HasJoinData<TFirst, TSecond>(
    this ModelBuilder modelBuilder,
    params (TFirst First, TSecond Second)[] data)
    where TFirst : class where TSecond : class
    => modelBuilder.HasJoinData(data.AsEnumerable());

public static void HasJoinData<TFirst, TSecond>(
    this ModelBuilder modelBuilder,
    IEnumerable<(TFirst First, TSecond Second)> data)
    where TFirst : class where TSecond : class
{
    var firstEntityType = modelBuilder.Model.FindEntityType(typeof(TFirst));
    var secondEntityType = modelBuilder.Model.FindEntityType(typeof(TSecond));
    var firstToSecond = firstEntityType.GetSkipNavigations()
        .Single(n => n.TargetEntityType == secondEntityType);
    var joinEntityType = firstToSecond.JoinEntityType;
    var firstProperty = firstToSecond.ForeignKey.Properties.Single();
    var secondProperty = firstToSecond.Inverse.ForeignKey.Properties.Single();
    var firstValueGetter = firstToSecond.ForeignKey.PrincipalKey.Properties.Single().GetGetter();
    var secondValueGetter = firstToSecond.Inverse.ForeignKey.PrincipalKey.Properties.Single().GetGetter();
    var seedData = data.Select(e => (object)new Dictionary<string, object>
    {
        [firstProperty.Name] = firstValueGetter.GetClrValue(e.First),
        [secondProperty.Name] = secondValueGetter.GetClrValue(e.Second),
    });
    modelBuilder.Entity(joinEntityType.Name).HasData(seedData);
}
Run Code Online (Sandbox Code Playgroud)

modelBuilder.HasJoinData((book, category));
Run Code Online (Sandbox Code Playgroud)

会做。