首页 > 解决方案 > 使用 EF Core 5 使用存储在不同表中的信息填充一个实体中的列表

问题描述

概括

我有两个表,一个用户表和一个赞助表。Sponsorships 表有两个指向 Users 表的外键。这里的关系是一个用户可以被几个不同的用户赞助,一个用户可以赞助几个不同的用户。我想使用此信息来填写每个用户的赞助商列表。

语境

我目前正在处理一个项目,我必须创建一个 API,该 API 使用在不同项目中使用的现有数据库,并且已决定将使用 .NET 5。已经存在的项目有成千上万的用户,所以我无法对数据库进行任何更改。为简洁起见,我删除了与问题无关的代码。

数据库

我连接的数据库有很多表,但其中只有两个与我的问题相关,用户和赞助。下面我说明了这些表的外观和它们的关系,删除了与此问题无关的列。这些列不以任何方式提供用户表和赞助表之间的关系。

用户

用户身份
1
2
3
4
5

赞助

用户身份 赞助商用户 ID
1 3
1 4
1 5
2 3
2 4

Sponsorships 中的两列是用户表中 UserId 的主键和外键,因此我们正在处理复合键。这个表中还有一列也是主键和外键,称为RolesId。它是不同表的外键,但这不应该是相关的,因为它与用户表没有关系。此外,Sponsorships 表没有自动递增的 Id 列,只有这三个主键和一个 DateTime 类型的不相关列。我决定不编辑对 RolesId 的引用,以使相关代码尽可能接近原始代码。

一个用户可以由多个不同的用户赞助,也可以由多个不同的用户赞助。使用此信息,应该可以为每个用户创建两个列表,一个列表包含该用户赞助的所有用户,另一个列表包含该用户赞助的所有用户。

我的代码

到目前为止,我的代码包含一个继承 DbContext 的类、两个实体类,一个用于 User 和 Sponsorship,它有两个实现 IEntityTypeConfiguration 接口的相应配置类。一个使用在继承 DbContext 的类中定义的 DbSet 访问数据库的存储库。还有一个控制器可以让消费者可以使用这些信息。您可以在下面找到这些类,为简洁起见进行了编辑,以使我的解决方案更加清晰。一些类名和属性已被编辑以避免复制粘贴专有代码。

public class MyContext: DbContext, IMyContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Sponsorship> Sponsorships { get; set; }

    public MyContext(DbContextOptions<MyContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new SponsorshipConfiguration());
        modelBuilder.ApplyConfiguration(new UserConfiguration());
    }
}

public class User
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public List<Sponsorship> AffiliateRelations { get; set; }
    public List<Sponsorship> SponsorRelations { get; set; }
    public List<User> Sponsors { get; set; }
}


public class Sponsorship
{
    public int RoleId { get; set; }
    public int UserId { get; set; }
    public int SponsorUserId { get; set; }
    public User User { get; set; }
    public User Sponsor { get; set; }
}


public class SponsorshipConfiguration : IEntityTypeConfiguration<Sponsorship>
{
    public void Configure(EntityTypeBuilder<Sponsorship> builder)
    {
        builder.ToTable("Sponsorships");
        builder.HasKey(s => new { s.RoleId, s.UserId, s.SponsorUserId });
        builder.Property(s => s.RoleId).HasColumnName("Roles_Id");

        builder.HasOne(s => s.Sponsor)
               .WithMany(u => u.SponsorRelations)
               .HasForeignKey(s => s.SponsorUserId);

        builder.HasOne(s => s.User)
               .WithMany(u => u.AffiliateRelations)
               .HasForeignKey(s => s.UserId);

    }
}


public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.ToTable("Users");
        builder.HasKey("Id");
    }
}

    
public class UserRepository: BaseRepository, IUserRepository
{
    public UserRepository(IMyContext context) : base(context)
    {
    }
    public async Task<List<Sponsorship>> GetSponsorships()
    {
        return await Context.Sponsorships.Include(s => s.Sponsor).Include(s => s.User).OrderBy( s => s.UserId).ToListAsync();
    }
    public async Task<List<User>> GetUsersAsync()
    {
        return await Context.Users.ToListAsync();
    }
    public async Task<List<User>> GetUsersWithSponsors()
    {
        return await Context.Users.Include(u => u.Sponsors).ToListAsync()
    }
}

如有必要,我还可以包含控制器代码,但这只是调用 UserRepository 中声明的各种方法,稍后我将添加一个控制器可以使用的 UserService。

问题

GetUsersWithSponsors() 确实执行成功,但在 Sponsors 列表中缺少信息。这是使用上面定义的数据的示例。UserId = 1 的用户对象有一个赞助商列表,它只包含一个元素,而它应该有 3。我通过查询数据库确认了这一点,并确保在选择所有行时赞助表确实有 3 行。用户身份。

我在 Stack Overflow 上查看了很多不同的问题,这些问题似乎解决了一个非常相似的问题,但他们的解决方案对我不起作用。例如Entity Framework Code First - 来自同一个表的两个外键我只能猜测我在两个配置类中使用 Fluent API 定义关系的方式存在缺陷和/或缺少信息。我将非常感谢一些帮助。谢谢你。

附带说明一下,我知道微软建议在 Fluent API 之前使用数据注释。我不知道这是否属实,但我会研究一下并考虑转换 Fluent API 代码。但是,如果这个问题的解决方案是使用 Fluent API 提供的,或者如果需要更改或添加配置代码,我将不胜感激。

标签: c#sqlentity-framework-core.net-5

解决方案


User除了类的这个属性之外的所有东西

public List<User> Sponsors { get; set; }

是具有显式连接实体的经典多对多自我关系(在您的情况下,Sponsorship它是具有附加字段的连接实体),由其他两个集合导航属性表示。

这个属性虽然需要注意。如果没有额外的流畅配置,它将被视为单独的一对多自我关系(很可能将User.UserId在呈现的模型中看起来很奇怪的属性视为 FK 并加入它),这不是您想要的。相反,它必须配置为跳过导航(EF Core 5.0 中的一个新概念,用于通过隐式连接实体实现多对多)并绑定到现有的“常规”关系,如连接Sponsorship中所述(连同示例) EF Core 文档的实体类型配置部分。

但为了做到这一点,您需要定义另一个集合导航属性(所讨论的“逆”属性),因为这是多对多映射的当前要求。

所以让我们添加一个 - 名称无关紧要,只是类型 -IEnumerable<User>或派生:

public List<User> Sponsoring { get; set; }

并在内部使用以下流利UserConfiguration的配置(从中删除关系配置代码,SponsorshipConfiguration因为它们必须/将被配置为User下面的关系配置的一部分):

builder
    .HasMany(e => e.Sponsors)
    .WithMany(e => e.Sponsoring)
    .UsingEntity<Sponsorship>(
        j => j
            .HasOne(e => e.Sponsor)
            .WithMany(e => e.SponsorRelations)
            .HasForeignKey(e => e.SponsorUserId),
        j => j
            .HasOne(e => e.User)
            .WithMany(e => e.AffiliateRelations)
            .HasForeignKey(e => e.UserId)
    );

就这样。现在两者

Context.Users.Include(u => u.Sponsors)

Context.Users.Include(u => u.Sponsoring)

而且当然

Context.Users.Include(u => u.Sponsors).Include(u => u.Sponsoring)

可用于获取(正确!)您需要的信息。


推荐阅读