流利 API,Entity Framework 核心中的多对多

Fluent API, many-to-many in Entity Framework Core

我在 Whosebug 上搜索了生成 多对多 关系的正确解决方案,使用 EF Core、Code first 和 Fluent API。

一个简单的场景是:

public class Person
{
    public Person() {
        Clubs = new HashSet<Club>();
    }
    public int PersonId { get; set; }
    public virtual ICollection<Club> Clubs { get; set; }
}

public class Club
{
    public Club() {
        Persons = new HashSet<Person>();
    }
    public int ClubId { get; set; }
    public virtual ICollection<Person> Persons { get; set; }
}

如果我错了,请纠正我,但老实说,我找不到包含有关如何使用所描述的工具执行此操作的详尽解释的问题。 谁能解释一下这是怎么做到的?

EF 核心 5.0 RC1+

从 EF Core 5.0 RC1 开始,可以在没有显式连接的情况下执行此操作 table。 EF Core 能够为您的问题中显示的多对多关系配置映射,而无需您创建 PersonClub 类型。

有关详细信息,请参阅官方文档中的 What's New in EF Core 5.0, RC1, Many-to-many

以前的版本

如果不使用明确的 class 连接,这在 EF Core 中是不可能的。有关如何执行此操作的示例,请参阅 here

Github 上有一个开放的 issue 要求能够在不需要显式 class 的情况下执行此操作,但尚未完成。

使用您的方案,我链接的示例将推荐以下实体 classes:

public class Person
{
    public int PersonId { get; set; }
    public virtual ICollection<PersonClub> PersonClubs { get; set; }
}

public class Club
{
    public int ClubId { get; set; }
    public virtual ICollection<PersonClub> PersonClubs { get; set; }
}

public class PersonClub
{
    public int PersonId { get; set; }
    public Person Person { get; set; }
    public int ClubId { get; set; }
    public Club Club { get; set; }
}

然后将使用以下 OnModelCreating 进行设置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<PersonClub>()
        .HasKey(pc => new { pc.PersonId, pc.ClubId });

    modelBuilder.Entity<PersonClub>()
        .HasOne(pc => pc.Person)
        .WithMany(p => p.PersonClubs)
        .HasForeignKey(pc => pc.PersonId);

    modelBuilder.Entity<PersonClub>()
        .HasOne(pc => pc.Club)
        .WithMany(c => c.PersonClubs)
        .HasForeignKey(pc => pc.ClubId);
}

如果您觉得有必要,请务必转到我链接的未解决问题并表达您的不满。

编辑:未解决的问题建议使用简单的 Select 来浏览这个有点麻烦的层次结构。为了从 PersonIdClub 的集合,您可以使用 SelectMany。例如:

var clubs = dbContext.People
    .Where(p => p.PersonId == id)
    .SelectMany(p => p.PersonClubs);
    .Select(pc => pc.Club);

我不能保证这是否真的是“最佳实践”,但它肯定可以解决问题,而且我认为公平地说它并不过分丑陋。

所以每个 Person 都有零个或多个 Clubs,每个 Club 都有零个或多个 Persons。正如您所说的那样,这是一个正确的多对多关系。

您可能知道关系数据库需要额外的 table 来实现这种多对多关系。 entity framework 的好处在于它可以识别这种关系并为您创建额外的 table。

乍一看,这个额外的 table 不是 DbContext 中的 dbSet 似乎是个问题:"How to perform a join with this extra table if I don't have a DbSet for it?".

幸运的是,您无需在查询中提及这个额外的 table。

如果您需要像 "Give me all 'Clubs' that ... from every 'Person' who ..." 这样的查询,请不要考虑连接。而是使用 ICollections!

获取所有 "John Doe" 人及其参加的所有乡村俱乐部:

var result = myDbContext.Persons
    .Where(person => person.Name == "John Doe")
    .Select(person => new
    {
        PersonId = person.Id,
        PersonName = person.Name,
        AttendedCountryClubs = person.Clubs
            .Where(club => club.Type = ClubType.CountryClub),
    };

Entity framework 将认识到需要与额外的多对多 table 进行连接,并将执行此连接,而无需您提及此额外的 table.

相反:获取所有乡村俱乐部及其 "John Doe" 人:

var result = myDbContext.Clubs
    .Where(club => club.Type = ClubType.CountryClub)
    .Select(club => new
    {
         ClubId = club.Id,
         ClubName = club.Name,
         AnonymousMembers = club.Persons
             .Where(person => person.Name == "John Doe"),
    }

我有过这样的经历,一旦我开始考虑我想要的结果集合而不是获得这些集合所需的连接,我发现我几乎不使用连接。一对多关系和多对多关系都是如此。 Entity framework 将在内部使用正确的连接。

正确的 "setup" 是:

public class Person
{
    public int PersonId { get; set; }
    public virtual ICollection<PersonClub> PersonClubs { get; set; }
}

public class Club
{
    public int ClubId { get; set; }
    public virtual ICollection<PersonClub> PersonClubs { get; set; }
}

public class PersonClub
{
    public int PersonId { get; set; }
    public Person Person { get; set; }
    public int ClubId { get; set; }
    public Club Club { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<PersonClub>()
        .HasKey(pc => new { pc.PersonId, pc.ClubId });
}

因此,这个用于配置 "glue-table" 的块 不是 必需的,如 @Kirk 示例中所示:

modelBuilder.Entity<PersonClub>()
    .HasOne(pc => pc.Person)
    .WithMany(p => p.PersonClubs)
    .HasForeignKey(pc => pc.PersonId);

modelBuilder.Entity<PersonClub>()
    .HasOne(pc => pc.Club)
    .WithMany(c => c.PersonClubs)
    .HasForeignKey(pc => pc.ClubId);