代码优先 Entity Framework 多对多集合

Code-First Entity Framework Multiple Collections with Many to Many

我还有一个 Entity Framework 问题。我有一个名为 Book 的复杂对象,该对象有许多 Contributor 类型的集合,例如 Writer、Letterer、Colorist 等。Contributors 不一定限于特定角色。因此,例如,同一个贡献者(具有相同的 ContributorId)可能既是作家又是调色师。

public Book {
        public ICollection<Contributor> Writers { get; set; }
        public ICollection<Contributor> Artists { get; set; }
        public ICollection<Contributor> Pencilers { get; set; }
        public ICollection<Contributor> Inkers { get; set; }
        public ICollection<Contributor> Colorists { get; set; }
        public ICollection<Contributor> Letterers { get; set; }
        public ICollection<Contributor> CoverArtists { get; set; }
        public ICollection<Contributor> OtherContributors { get; set; }
}

public Contributor {
     public int ContributorId { get; set; }
     public string Name { get; set; }
}

我遇到了问题,查看我在此处和其他站点上找到的示例,确定如何表示适当的模型。我希望有一个像这样的 Db 模型。我想避免的是一个模型,其中我为每个贡献者角色都有一个单独的 table,或者在贡献者中有一个单独的行 table 对于贡献者与任何角色的一本书相关联的每个实例.

+ Books 
     --BookId
+ Contributors
      --ContributorId
+ BookContributors
      --BookId
      --ContributorId
      --Discriminator

我和 ADO.NET 一样,我并不真的觉得这太有趣了,但我决心至少精通这个重要的框架。

快速说明: 自从打开这个问题后,我就被工作抽离了,没有时间彻底复习答案并玩弄结果。但我不想让赏金悬而未决,因为我很感激每个人提供的答案。所以我一开始就选择了我最感兴趣的答案。尽管如此,我还是要感谢大家。

我已经研究了一个解决方案来实现您提出的模型,尽管它的工作方式与您预期的有点不同。希望这能回答您的问题。

型号

[Table("Book")]
public class Book
{
    [Column("BookId")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int BookId { get; set; }

    [NotMapped]
    public ICollection<Contributor> Writers { get; set; }

    [NotMapped]
    public ICollection<Contributor> Artists { get; set; }

    [NotMapped]
    public ICollection<Contributor> Pencilers { get; set; }

    [NotMapped]
    public ICollection<Contributor> Inkers { get; set; }

    [NotMapped]
    public ICollection<Contributor> Colorists { get; set; }

    [NotMapped]
    public ICollection<Contributor> Letterers { get; set; }

    [NotMapped]
    public ICollection<Contributor> CoverArtists { get; set; }

    [NotMapped]
    public ICollection<Contributor> OtherContributors { get; set; }
}

[Table("Contributor")]
public class Contributor
{
    [Column("ContributorId")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ContributorId { get; set; }
}

// Contributor Type is one of the following options: Writer, Artist, Penciler, etc.
[Table("ContributorType")]
public class ContributorType
{
    [Column("ContributorTypeId")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ContributorTypeId { get; set; }

    [Column("Name")]
    public string Name { get; set; }
}

[Table("BookContributor")]
public class BookContributor
{
    [Column("BookContributorId")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int BookContributorId { get; set; }

    [Column("BookId")]
    public int BookId { get; set; }

    [Column("ContributorId")]
    public int ContributorId { get; set; }

    [Column("RoleId")]
    public int RoleId { get; set; }

    [ForeignKey("BookId")]
    public virtual Book Book { get; set; }

    [ForeignKey("ContributorId")]
    public virtual Contributor Contributor { get; set; }

    [ForeignKey("RoleId")]
    public virtual ContributorType Role { get; set; }
}

数据库上下文

AppDbContext.cs:

public class AppDbContext : DbContext
{
    public AppDbContext()
    {
        Database.SetInitializer<AppDbContext>(new AppDbInitializer());
    }

    public AppDbContext(string connectionString)
        : base(connectionString)
    {
        Database.SetInitializer<AppDbContext>(new AppDbInitializer());
    }

    public DbSet<Book> Books { get; set; }

    public DbSet<Contributor> Contributors { get; set; }

    public DbSet<ContributorType> ContributorTypes { get; set; }

    public DbSet<BookContributor> BookContributors { get; set; }
}

AppDbInitializer.cs:

public class AppDbInitializer : DropCreateDatabaseAlways<AppDbContext>
{
    protected override void Seed(AppDbContext context)
    {
        // default contributor types
        var contributorTypes = new List<ContributorType>();
        contributorTypes.Add(new ContributorType() { Name = "Writer" });
        contributorTypes.Add(new ContributorType() { Name = "Artist" });
        contributorTypes.Add(new ContributorType() { Name = "Penciler" });
        contributorTypes.Add(new ContributorType() { Name = "Inker" });
        contributorTypes.Add(new ContributorType() { Name = "Colorist" });
        contributorTypes.Add(new ContributorType() { Name = "Letterer" });
        contributorTypes.Add(new ContributorType() { Name = "CoverArtist" });
        contributorTypes.Add(new ContributorType() { Name = "OtherContributor" });

        // adding it to the context
        foreach (var type in contributorTypes)
            context.ContributorTypes.Add(type);

        base.Seed(context);
    }
}

将所有内容包装在一起

Program.cs:

class Program
{
    static void Main(string[] args)
    {
        // enter name of the connection string in App.Config file
        var connectionSettings = ConfigurationManager.ConnectionStrings["..."];
        using (var dbContext = new AppDbContext(connectionSettings.ConnectionString)) 
        {
            // Creating a book
            var book = new Book();
            dbContext.Books.Add(book);
            dbContext.SaveChanges();

            // Creating contributor
            var contributor = new Contributor();
            dbContext.Contributors.Add(contributor);
            dbContext.SaveChanges();

            // Adding contributor to the book
            var bookContributor = new BookContributor()
            {
                BookId = book.BookId,
                ContributorId = contributor.ContributorId,
                RoleId = dbContext.ContributorTypes.First(t => t.Name == "Writer").ContributorTypeId
            };
            dbContext.BookContributors.Add(bookContributor);
            dbContext.SaveChanges();

            // retrieving a book
            var book = dbContext.Books.Where(b => b.BookId == 2).FirstOrDefault();
            if (book != null) 
            {
                book.Writers = 
                    from contributor in dbContext.Contributors
                    join bookContributor in dbContext.BookContributors on contributor.BookId equals bookContributor.BookId
                    join contributorType in dbContext.ContributorTypes on contributorType.ContributorTypeId equals bookContributor.ContributorTypeId
                    where 
                        bookContributor.BookId == 2 and
                        contributorType.Name == "Writer"
                    select contributor;

                // do the same for other types of contributors
            }
        }
    }
}

我在 test.polarcomputer.com
写书 如果你有一个书对象,而这个对象有作家、出版商、设计师......任何人,
你只需要 3 个对象:

1.book 对象
2.contributor 对象。
3.integration对象

图书对象有
- bookid
- 书名

贡献者对象有
- 贡献者编号
- 姓名
- typeofcontributor // 0-writer 1-colorist 2-CoverArtists 3-whoever

集成对象有
- bookid
- 贡献者编号
- typeofcontributor // 0-writer 1-colorist 2-CoverArtists 3-whoever

检查这个如果我真的理解它,我可以给你完整的解决方案。

使用 M:N 映射在 Contributor 实体中创建类似的集合,并使用 InverseProperty 属性声明 Contributor class 中的哪个集合对应于 Book class 中的哪个集合。

public class Book
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Contributor> Writers { get; set; }
    public virtual ICollection<Contributor> Artists { get; set; }
    public virtual ICollection<Contributor> Pencilers { get; set; }
    public virtual ICollection<Contributor> Inkers { get; set; }
    public virtual ICollection<Contributor> Colorists { get; set; }
    public virtual ICollection<Contributor> Letterers { get; set; }
    public virtual ICollection<Contributor> CoverArtists { get; set; }
    public virtual ICollection<Contributor> OtherContributors { get; set; }

    public Book()
    {
        Writers = new List<Contributor>();
        Artists = new List<Contributor>();
        Pencilers = new List<Contributor>();
        Inkers = new List<Contributor>();
        Colorists = new List<Contributor>();
        Letterers = new List<Contributor>();
        CoverArtists = new List<Contributor>();
        OtherContributors = new List<Contributor>();
    }
}

public class Contributor
{
    public int ContributorId { get; set; }
    public string Name { get; set; }

    [InverseProperty("Writers")]
    public virtual ICollection<Book> WriterOfBooks { get; set; }

    [InverseProperty("Artists")]
    public virtual ICollection<Book> ArtistOfBooks { get; set; }

    [InverseProperty("Pencilers")]
    public virtual ICollection<Book> PencilerOfBooks { get; set; }

    [InverseProperty("Inkers")]
    public virtual ICollection<Book> InkerOfBooks { get; set; }

    [InverseProperty("Colorists")]
    public virtual ICollection<Book> ColoristOfBooks { get; set; }

    [InverseProperty("Letterers")]
    public virtual ICollection<Book> LettererOfBooks { get; set; }

    [InverseProperty("CoverArtists")]
    public virtual ICollection<Book> CoverArtistOfBooks { get; set; }

    [InverseProperty("OtherContributors")]
    public virtual ICollection<Book> OtherContributorOfBooks { get; set; }

    public Contributor()
    {
        WriterOfBooks = new List<Book>();
        ArtistOfBooks = new List<Book>();
        PencilerOfBooks = new List<Book>();
        InkerOfBooks = new List<Book>();
        ColoristOfBooks = new List<Book>();
        LettererOfBooks = new List<Book>();
        CoverArtistOfBooks = new List<Book>();
        OtherContributorOfBooks = new List<Book>();
    }
}

那么用法就很简单了:

using (var dc = new MyDbContext())
{
    // create sample data
    var book1 = new Book() { Name = "Book 1" };
    dc.Books.Add(book1);

    var contrib1 = new Contributor() { Name = "Contributor 1" };
    var contrib2 = new Contributor() { Name = "Contributor 2" };
    var contrib3 = new Contributor() { Name = "Contributor 3" };
    dc.Contributors.Add(contrib1);
    dc.Contributors.Add(contrib2);
    dc.Contributors.Add(contrib3);

    dc.SaveChanges();



    // add relationships
    book1.Writers.Add(contrib1);
    book1.Artists.Add(contrib1);
    book1.Artists.Add(contrib2);
    book1.OtherContributors.Add(contrib3);
    dc.SaveChanges();
}

// verify that the contributor 1 has both Artist and Writer relations
using (var dc = new MyDbContext())
{
    var contrib1 = dc.Contributors.Single(c => c.Name == "Contributor 1");
    var hasWriter = contrib1.WriterOfBooks.Count == 1;
    var hasArtist = contrib1.ArtistOfBooks.Count == 1;
    if (!hasWriter || !hasArtist)
    {
        throw new Exception("Houston, we have a problem.");
    }
}

你展示的数据模型没问题,但有一点很清楚。您不能将其映射为纯粹的多对多关联。这只有在结点 table BookContributors 只包含 BookIdContributorId.

时才有可能

所以你总是需要一个明确的 BookContributor class,并且获取其中一种贡献者类型的集合总是会采用这种基本形状:

book.BookContributors
    .Where(bc => bc.Type == type)
    .Select(bc => bc.Contributor)

笨拙,与您的想法相比。但恐怕没有办法绕过它。剩下的就是实现细节中的几个选项。

选项 1:获取所有贡献者,稍后过滤。

首先,让我们弄清楚基本模型:

public class Book
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public virtual ICollection<BookContributor> BookContributors { get; set; }
}

public class Contributor
{
    public int ContributorId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<BookContributor> BookContributors { get; set; }
}

public class BookContributor
{
    public int BookId { get; set; }
    public virtual Book Book { get; set; }

    public int ContributorId { get; set; }
    public virtual Contributor Contributor { get; set; }

    public string Type { get; set; }
}

和映射:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<Book>().HasMany(b => b.BookContributors)
        .WithRequired(bc => bc.Book)
        .HasForeignKey(bc => bc.BookId);
    modelBuilder.Entity<Contributor>().HasMany(c => c.BookContributors)
        .WithRequired(bc => bc.Contributor)
        .HasForeignKey(bc => bc.ContributorId);
    modelBuilder.Entity<BookContributor>()
                .HasKey(bc => new {bc.BookId, bc.ContributorId, bc.Type});
}

(顺便说一句,这里我避免使用 'Discriminator' 一词,因为它暗示 TPH 继承,目前尚不适用)。

现在您可以像这样向 Book 添加属性:

[NotMapped]
public IEnumerable<Contributor> Writers
{ 
    get 
    {
        return BookContributors.Where(bc => bc.Type == "writer")
                               .Select(bc => bc.Contributor);
    }
}

这种方法的缺点是您始终必须确保加载书籍时包含 BookContributors Contributor,或者延迟加载是可能的。并且您不能在 LINQ 查询中直接使用这些属性。此外,很难获得书籍并且只有他们的独特贡献者(即独特的)。

选项 2:继承 - 本质上相同

您可以使 BookContributor 成为具有多个继承者的抽象基础 class:

public abstract class BookContributor
{
    public int Id { get; set; }
    public int BookId { get; set; }
    public virtual Book Book { get; set; }
    public int ContributorId { get; set; }
    public virtual Contributor Contributor { get; set; }
}

public class Artist : BookContributor
{ }

public class Writer : BookContributor
{ }

BookContributor 现在需要一个代理键,Id,因为 EF 现在将使用一个字段 Discriminator,它是隐藏的,所以它不能配置为主键。

现在 Book 可以拥有像...

这样的属性
    [NotMapped]
    public ICollection<Artist> Artists
    {
        get { return BookContributors.OfType<Artist>().ToList(); }
    }

...但是这些仍然具有与上述相同的缺点。唯一可能的优势是您现在可以使用类型(通过编译时检查)代替字符串(或枚举值)来获得各种 BookContributor 类型。

选项 3:不同的模型

也许最有前途的方法是稍微不同的模型:书籍和贡献者,其中它们之间的每个关联都可以有一个贡献者类型的集合。 BookContributor 现在看起来像这样:

public class BookContributor
{
    public int BookId { get; set; }
    public virtual Book Book { get; set; }
    public int ContributorId { get; set; }
    public virtual Contributor Contributor { get; set; }
    
    public virtual ICollection<BookContributorType> BookContributorTypes { get; set; }
}

还有一个新类型:

public class BookContributorType
{
    public int ID { get; set; }
    public int BookId { get; set; }
    public int ContributorId { get; set; }
    public string Type { get; set; }
}

修改映射:

modelBuilder.Entity<BookContributor>().HasKey(bc => new { bc.BookId, bc.ContributorId });

附加映射:

modelBuilder.Entity<BookContributor>().HasMany(bc => bc.BookContributorTypes).WithRequired();
modelBuilder.Entity<BookContributorType>().HasKey(bct => bct.ID);

使用此模型,如果您对贡献者的类型不感兴趣,您可以只获取书籍及其不同的贡献者...

context.Books.Include(b => b.BookContributors
                            .Select(bc => bc.Contributor))

...或类型...

context.Books.Include(b => b.BookContributors
                            .Select(bc => bc.Contributor))
             .Include(b => b.BookContributors
                            .Select(bc => bc.BookContributorTypes));

...或只有作者的书...

context.Books.Select(b => new
                     {
                         Book = b,
                         Writers = b.BookContributors
                                    .Where(bc => bc.BookContributorTypes
                                                   .Any(bct => bct.Type == "artist"))
                     })

同样,后一个查询可以包装在 属性...

    [NotMapped]
    public ICollection<Artist> Artists
    {
        get
        {
            return BookContributors
                     .Where(bc => bc.BookContributorTypes
                                .Any(bct => bct.Type == "artist"))
                     .Select(bc => bc.Contributor).ToList();
        }
    }

...但是,考虑到上述所有注意事项。

你的模型应该是这样的:

    [Table("tblBooks")]
    public class BookTbl
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int BookID { get; set; }
        public string BookName { get; set; }
    }

    [Table("tblContributor")]
    public class ContributorTbl
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ContID { get; set; }
        public string Contributor { get; set; }
    }

    [Table("tblIntegration")]
    public class IntegrationTbl
    {
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int IntID { get; set; }
        public int BookID { get; set; }
        [ForeignKey("BookID")]
        public BookTbl Book { get; set; }
        public int ContID { get; set; }
        [ForeignKey("ContID")]
        public IntegrationTbl Integration { get; set; } 
    }