然而,同样,因为在 EFCore5 中没有具体的类来描述 BookCategories
实际上,正如What's new link 中所述,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; }
}
并配置多对多关系以使用它
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")
);
通过这种方式,您可以使用所有正常的实体操作(查询、更改跟踪、数据模型播种等)
modelBuilder.Entity<BookCategory>().HasData(
new BookCategory() { BookId = 1, CategoryId = 1 }
);
仍有新的多对多跳过导航映射。
这可能是最简单且类型安全的方法。
如果你觉得它太多了,也可以使用传统的连接实体,但你需要知道shared dictionary entity type 名称,以及两个shadow property 名称。按照惯例,这可能不是您所期望的。
所以,按照惯例,连接实体(和表)的名称是
{LeftEntityName}{RightEntityName}
阴影属性(和列)名称是
{LeftEntityNavigationPropertyName}{RightEntityKeyName}
{RightEntityNavigationPropertyName}{LeftEntityKeyName}
第一个问题是 - 左/右实体是哪个?答案是(尚未记录) - 按照惯例,左侧实体是名称按字母顺序排列较少的实体。所以你的例子Book是左边,Category是右边,所以连接实体和表名是BookCategory。
添加显式可以更改
modelBuilder.Entity<Category>()
.HasMany(left => left.Books)
.WithMany(right => right.Categories);
现在是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")
);
现在您可以使用实体名称访问EntityTypeBuilder
modelBuilder.Entity("BookCategories")
您可以使用匿名类型将其播种为类似于normal entities with shadow FK properties
modelBuilder.Entity("BookCategory").HasData(
new { BookId = 1, CategoryId = 1 }
);
或者对于这个特定的属性包类型实体,也有Dictionary<string, object>实例
modelBuilder.Entity("BookCategory").HasData(
new Dictionary<string, object> { ["BookId"] = 1, ["CategoryId"] = 1 }
);
更新:
人们似乎误解了上述“额外”步骤,并认为它们是多余的和“太多”,不需要。
我从未说过它们是强制性的。如果您知道常规的连接实体和属性名称,请直接进入最后一步并使用匿名类型或Dictionary<string, object>。
我已经解释了采用这种方法的缺点 - 失去 C# 类型的安全性并使用您无法控制的“魔术”字符串。您必须足够聪明才能知道确切的 EF Core 命名约定,并意识到如果将类 Book 重命名为 EBook,新的连接实体/表名称将从“BookCategory”更改为“CategoryEBook”以及PK 属性/列、关联索引等的顺序。
关于数据播种的具体问题。如果你真的想概括它(OP 在他们自己的答案中尝试),至少通过使用 EF Core 元数据系统而不是反射和假设来正确地做到这一点。例如,以下将从 EF Core 元数据中提取这些名称:
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为例,两者都
modelBuilder.HasJoinData((book, category));
和
modelBuilder.HasJoinData((category, book));
会的。