【问题标题】:Seeding many-to-many databases in EFCore5 with ModelBuilder?使用 ModelBuilder 在 EFCore5 中播种多对多数据库?
【发布时间】:2021-02-23 00:41:54
【问题描述】:

manyquestions关于在实体框架中播种多对多关系。但是,它们中的大多数都非常旧,并且多对多的行为有changed significantly in EFCore5official docs recommend 覆盖 OnModelCreating 以实现 ModelBuilder.Entity<>.HasData()

但是,使用新的多对多行为(没有显式映射),我找不到为中间表播种的明确路径。要使用this tutorial 的示例,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; }
}  

然后在种子时间:

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 创建条目。这在某种程度上是意料之中的,因为this article suggests 必须明确地为中间表提供种子。我想要的是这样的:

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

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

【问题讨论】:

    标签: .net-core entity-framework-core


    【解决方案1】:

    然而,同样,因为在 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&lt;string, object&gt;实例

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

    更新:

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

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

    我已经解释了采用这种方法的缺点 - 失去 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));
    

    会的。

    【讨论】:

    • 嗯,是的,我可以明确定义映射 — 从而解决了我首先升级到 EFCore5 的整个原因(我有一个 很多 b> m2m 表,这足以使我需要编写的模型数量增加一倍甚至三倍)。我也不同意您的评估,即“最好流畅地配置由 EF Core 约定自动创建的那个”。为什么?您基本上是在争辩说 EFcore5 m2m 功能是开发人员不应该使用的“gocha”。也就是说,您确实解决了我的基于名称访问的问题,所以谢谢!
    • 看起来,如果您明确设置映射,则提供的解决方案仅适用于 RC1,由于此问题:github.com/dotnet/efcore/issues/22521 ...但 RC2 刚刚发布,所以试一试……
    • @ZaneClaes (1) 新的 m2m 映射的主要好处是从普通 crud 中使用的跳过导航属性。但是映射显式类((使用简单的样板代码)的能力是对 EF6 的改进,并且允许您在某些情况下避免该模型的一些缺点,而不会丢失强类型。(2)显式映射不是强类型必要的,如果您知道提到的 3 个名称和 EFC 约定(并且后者以后不会更改并破坏您的“魔术”字符串的使用。您可以使用我向您展示的名称,但您会在没有这个问题的情况下知道它们吗? ?
    • 如果 RC2 破坏了它,这只是另一个证明,您不应该使用未发布的软件并且还应该依赖(未记录的)EFC 约定。
    • 嗯,我链接的问题表明,如果没有显式映射,您的代码在 RC1 中不起作用。当我尝试使用您的代码时,我在那里粘贴了确切的错误消息。它适用于 RC2(我刚刚在升级后确认)。
    【解决方案2】:

    更新(EF Core 5.0.2)

    使用关联表的名称效果很好:

        builder.Entity("ContractDeclarationType").HasData(
            new { ContractsId = 1L, DeclarationTypesId = 1L },
            new { ContractsId = 1L, DeclarationTypesId = 2L },
            new { ContractsId = 1L, DeclarationTypesId = 3L });
    

    【讨论】:

      【解决方案3】:

      我最终根据 Ivan 的回答想出了一个通用的解决方案来解决这个问题(谢谢!)。我现在可以使用以下语法为我的所有 M2M 表播种:

      // Add book1 and book2 to category1:
      modelBuilder.HasM2MData(new [] { book1, book2 }, new [] { category1 });
      

      这可能并不完全可靠,但它应该适用于传统的 M2M 映射。

      它做了一些假设:

      • T1 & T2 继承自一些提供 Id 属性的 ModelBase
      • T1 & T2 拥有一个 ICollection&lt;OtherType&gt; 属性。
      • 您知道正确的顺序(哪个型号是T1,哪个型号是T2) - 这可以通过首先运行表的迁移并检查迁移来发现。
      • 您正在运行 EFCore5 RC2 或更高版本(请参阅 this issue)。
      public static void HasM2MData<T1, T2>
        (this ModelBuilder mb, T1[] t1s, T2[] t2s)
        where T1 : ModelBase where T2 : ModelBase
      {
        string table = $"{typeof(T1).Name}{typeof(T2).Name}";
        PropertyInfo t1Prop = GetM2MProperty<T1, T2>();
        PropertyInfo t2Prop = GetM2MProperty<T2, T1>();
        string t1Key = $"{t1Prop.Name}Id";
        string t2Key = $"{t2Prop.Name}Id";
        foreach (T1 t1 in t1s) {
          foreach (T2 t2 in t2s) {
            mb.Entity(table).HasData(new Dictionary<string, object>() { [t2Key] = t1.Id, [t1Key] = t2.Id });
          }
        }
      }
      
      // Get a property on T1 which is assignable to type ICollection<T2>, representing the m2m relationship
      private static PropertyInfo GetM2MProperty<T1, T2>() {
        Type assignableType = typeof(ICollection<T2>);
        List<PropertyInfo> props = typeof(T1).GetProperties()
                                             .Where(pi => pi.PropertyType.IsAssignableTo(assignableType))
                                             .ToList();
        if (props.Count() != 1) {
          throw new SystemException(
            $"Expected {typeof(T1)} to have exactly one column of type {assignableType}; got: {props.Count()}");
        }
        return props.First();
      }
      

      在迁移过程中,我们看到如下内容:

      migrationBuilder.InsertData(
      table: "BookCategory",
      columns: new[] { "BooksId", "CategoriesId" },
      values: new object[,]
      {
          { "book1", "category1" },
          { "book2", "category1" }
      });
      

      【讨论】:

        猜你喜欢
        • 2013-06-21
        • 2018-09-06
        • 1970-01-01
        • 1970-01-01
        • 2021-01-25
        • 1970-01-01
        • 2020-11-05
        • 2014-03-01
        • 1970-01-01
        相关资源
        最近更新 更多