使用 ModelBuilder 在 EFCore5 中播种多对多数据库? [英] Seeding many-to-many databases in EFCore5 with ModelBuilder?

查看:16
本文介绍了使用 ModelBuilder 在 EFCore5 中播种多对多数据库?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

很多 问题 关于在实体框架中播种多对多关系.但是,它们中的大多数都非常古老,并且多对多行为具有 在 EFCore5 中发生了显着变化.官方文档推荐覆盖了OnModelCreating 来实现 ModelBuilder.Entity<>.HasData().

There are many questions about seeding many-to-many relationships in Entity Framework. However, most of them are extremely old, and many-to-many behavior has changed significantly in EFCore5. The official docs recommend overriding OnModelCreating to implement ModelBuilder.Entity<>.HasData().

但是,使用新的多对多行为(没有显式映射),我找不到明确的路径来为中间表设置种子.要使用本教程的示例,BookCategories 类现在是隐式的.因此,没有在播种时显式声明中间表值的路径.

However, with the new many-to-many behavior (without explicit mappings), I can find no clear path to seed the intermediate tables. To use the example of this tutorial, the BookCategories class is now implicit. Therefore, there is no path to explicitly declare the intermediate table values while seeding.

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

I've also tried simply assigning the arrays, e.g.,:

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; }
}  

然后在种子时间:

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);

... 但在结果迁移中没有为 BookCategories 创建条目.这有点在意料之中,因为这篇文章建议必须明确地为中间表设置种子.我想要的是这样的:

... but there are no entries created for BookCategories in the resulting migration. This was somewhat expected, as this article suggests that one must explicitly seed the intermediate table. What I want is something like this:

modelBuilder.Entity<BookCategory>().HasData(
  new BookCategory() { BookId = 1, CategoryId = 1 }
);

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

However, again, since there is no concrete class to describe BookCategories in EFCore5, the only way I can think of to seed the table is to manually edit the migration with additional MigrationBuilder.InsertData commands, which rather defeats the purpose of seeding data via application code.

推荐答案

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

实际上,如What's new link,EF Core 5 允许你有显式的加入实体

Actually, as explained in the What's new link, EF Core 5 allows you to have explicit join entity

public class BookCategory
{
    public int BookId { get; set; }
    public EBook Book { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}

并配置多对多关系使用

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")
    );

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

This way you can use all normal entity operations (query, change tracking, data model seeding etc.) with it

modelBuilder.Entity<BookCategory>().HasData(
  new BookCategory() { BookId = 1, CategoryId = 1 }
);

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

still having the new many-to-many skip navigations mapping.

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

This is probably the simplest as well as the type-safe approach.

如果您想太多,也可以使用传统的连接实体,但您需要了解 共享字典实体类型名称,以及两个shadow 属性名称.按照惯例,这可能与您期望的不同.

In case you thing it's too much, using the conventional join entity is also possible, but you need to know the shared dictionary entity type name, as well as the two shadow property names. Which as you will see by convention might not be what you expect.

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

So, by convention the join entity (and table) name is

{LeftEntityName}{RightEntityName}

和影子属性(和列)名称是

and the shadow property (and column) names are

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

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

The first question would be - which is the left/right entity? The answer is (not documented yet) - by convention the left entity is the one which name is less in alphabetical order. So with your example Book is left, Category is right, so the join entity and table name would be BookCategory.

可以更改添加显式

modelBuilder.Entity<Category>()
    .HasMany(left => left.Books)
    .WithMany(right => right.Categories);

现在是CategoryBook.

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

In both cases the shadow property (and column) names would be

  • CategoriesCategoryId
  • BooksBookId

因此无论是表名还是属性/列名都不是您通常要做的.

So neither the table name, nor the property/column names are what you'd normally do.

除了数据库表/列名称之外,实体和属性名称也很重要,因为实体操作需要它们,包括有问题的数据播种.

And apart from the database table/column names, the entity and property names are important because you'd need them for entity operations, including the data seeding in question.

话虽如此,即使您不创建显式连接实体,也最好流畅地配置由 EF Core 约定自动创建的实体:

With that being said, even if you don't create explicit join entity, it's better to configure fluently the one created automatically by EF Core convention:

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")
    );

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

Now you can use the entity name to access the EntityTypeBuilder

modelBuilder.Entity("BookCategories")

你可以像 具有阴影 FK 属性的普通实体 具有匿名类型

and you can seed it similar to normal entities with shadow FK properties with anonymous type

modelBuilder.Entity("BookCategory").HasData(
  new { BookId = 1, CategoryId = 1 }
);

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

or for this specific property bag type entity, also with Dictionary<string, object> instances

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


更新:

人们似乎误解了前面提到的额外"步骤并发现它们是多余的和太多",不需要.

People seem to misinterpret the aforementioned "extra" steps and find them redundant and "too much", not needed.

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

I never said they are mandatory. If you know the conventional join entity and property names, go ahead directly to the last step and use anonymous type or Dictionary<string, object>.

我已经解释了采用该方法的缺点 - 失去了 C# 类型安全性并使用了魔法";弦不受你的控制.您必须足够聪明才能知道确切的 EF Core 命名约定,并意识到如果您将类 Book 重命名为 EBook,新的连接实体/表名称将从 "图书类别到类别电子书"以及PK属性/列、关联索引等的顺序.

I already explained the drawbacks of taking that route - loosing the C# type safety and using "magic" strings out of your control. You have to be smart enough to know the exact EF Core naming conventions and to realize that if you rename class Book to EBook the new join entity/table name will change from "BookCategory" to "CategoryEBook" as well as the order of the PK properties/columns, associated indexes etc.

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

Regarding the concrete problem with data seeding. If you really want to generalize it (OP attempt in their own answer), at least make it correctly by using the EF Core metadata system rather than reflection and assumptions. For instance, the following will extract these names from the EF Core metadata:

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);
}

此外,您无需知道哪种类型是左";这是正确的",既不需要特殊的基类或接口.只需传递实体对的序列,它就会正确地播种传统的连接实体,例如以 OP 为例,两者都

Also here you don't need to know which type is "left" and which is "right", neither requires special base class or interface. Just pass sequence of entity pairs and it will properly seed the conventional join entity, e.g. with OP example, both

modelBuilder.HasJoinData((book, category));

modelBuilder.HasJoinData((category, book));

会的.

这篇关于使用 ModelBuilder 在 EFCore5 中播种多对多数据库?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆