【问题标题】:Seeding data in many-to-many relation if EF Core 5如果 EF Core 5 以多对多关系播种数据
【发布时间】:2021-02-06 19:27:44
【问题描述】:

我有用户实体

public class User
{
    public int UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }

    public ICollection<Technology> Technologies { get; set; } = new List<Technology>();
}

我有技术实体

public class Technology
{
    public int TechnologyId { get; set; }
    public string TitleTechnology { get; set; }
    public int GroupId { get; set; }
    public Group Group { get; set; }
    public ICollection<User> Users { get; set; } = new List<User>();
}

我想创建多对多关系,所以我有这样的OnModelCreating 方法:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var groupList = new List<Group>
    {
        new Group {GroupId = 1, TitleGroup = ".NET"}
    };

    var technologyList = new List<Technology>
    {
        new Technology {TechnologyId = 1, GroupId = 1, TitleTechnology = ".NET 5"},
        new Technology {TechnologyId = 2, GroupId = 1, TitleTechnology = ".NET Framework 4.8"},
        new Technology {TechnologyId = 3, GroupId = 1, TitleTechnology = "EF 6"},
        new Technology {TechnologyId = 4, GroupId = 1, TitleTechnology = "ASP.NET MVC 5"}
    };

    var userList = new List<User>
    {
        new User
        {
            UserId = 1, FirstName = "Serhii", LastName = "Yurko", Email = "test", Password = "test",
            Technologies = new List<Technology> {technologyList[0], technologyList[1]}
        }
    };

    modelBuilder.Entity<Technology>().HasOne(exp => exp.Group).WithMany(exp => exp.Technologies).HasForeignKey(exp => exp.GroupId);
    modelBuilder.Entity<User>().HasMany(p => p.Technologies).WithMany(p => p.Users)
        .UsingEntity(j => j.ToTable("UserTechnology"));

    modelBuilder.Entity<User>().HasData(userList);
    modelBuilder.Entity<Group>().HasData(groupList);
    modelBuilder.Entity<Technology>().HasData(technologyList);
    base.OnModelCreating(modelBuilder);
}

当我想创建迁移时,我会收到这样的异常 -

无法添加实体类型“用户”的种子实体,因为它有 导航“技术”集。要播种关系,请添加 'TechnologyUser (Dictionary)' 的实体种子和 指定外键值 {'UsersUserId'}。考虑使用 'DbContextOptionsBuilder.EnableSensitiveDataLogging' 查看 涉及属性值。

如何建立适当的关系?

【问题讨论】:

  • 请也显示组类

标签: c# asp.net entity-framework-core


【解决方案1】:

在 EF-core 中,您不能直接播种导航属性。您必须从 User 初始化中删除行 Technologies = ...

here 所述,联结表可以通过扩展UsingEntity() 调用由其自己的HasData 调用播种,如下所示:

.UsingEntity(j => j
    .ToTable("UserTechnology")
    .HasData(new[]
        {
            { UsersID = 1, TechnologiesID = 1 },
            { UsersID = 1, TechnologiesID = 2 }
        }
    ));

如您所见,隐藏的联结实体由具有与表字段相同的属性名称(+ 类型)的匿名类型填充。这需要你知道这些外键字段的命名约定(我希望我猜对了)。

documentationthis Stack Overflow answer 中所述,您可以使用连接表的相当笨重的完全初始化来完成这一切。这允许您强制执行自己的 FK 属性名称并在种子代码中相应地使用它们。

【讨论】:

    【解决方案2】:

    Model seed data 方法有一些已知的限制。最值得注意的是,它不能通过从导航属性推断关系来插入相关数据,您必须显式添加外键值。详情见-Limitations of model seed data

    在您的情况下,最好使用Custom initialization logic 方法,如此处所述 - Custom initialization logic(不要忘记仔细阅读Warning)。

    Custom initialization logic 方法的实现:
    对于模型 -

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    
        public ICollection<Technology> Technologies { get; set; } = new List<Technology>();
    }
    
    public class Technology
    {
        public int Id { get; set; }
        public string TitleTechnology { get; set; }
        public int GroupId { get; set; }
    
        public Group Group { get; set; }
        public ICollection<User> Users { get; set; } = new List<User>();
    }
    
    public class Group
    {
        public int Id { get; set; }
        public string TitleGroup { get; set; }
    
        public ICollection<Technology> Technologies { get; set; } = new List<Technology>();
    }
    

    配置-

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Technology>()
            .HasOne(exp => exp.Group)
            .WithMany(exp => exp.Technologies)
            .HasForeignKey(exp => exp.GroupId);
    
        modelBuilder.Entity<User>()
            .HasMany(p => p.Technologies)
            .WithMany(p => p.Users);
    }
    

    一个可能的实现可能看起来像 -

    private static async Task AddEntities(DbCtx dbCtx)
    {
        List<Group> groupList;
        if (!dbCtx.Groups.Any())
        {
            groupList = new List<Group>
            {
                new Group { TitleGroup = ".NET"}
            };
            dbCtx.Groups.AddRange(groupList);
        }
        else
        {
            groupList = dbCtx.Groups.ToList();
        }
    
        List<Technology> technologyList;
        if (!dbCtx.Technologies.Any())
        {
            technologyList = new List<Technology>
            {
                new Technology { Group = groupList[0], TitleTechnology = ".NET 5"},
                new Technology { Group = groupList[0], TitleTechnology = ".NET Framework 4.8"},
                new Technology { Group = groupList[0], TitleTechnology = "EF 6"},
                new Technology { Group = groupList[0], TitleTechnology = "ASP.NET MVC 5"}
            };
            dbCtx.Technologies.AddRange(technologyList);
        }
        else
        {
            technologyList = dbCtx.Technologies.ToList();
        }
    
        List<User> userList;
        if (!dbCtx.Users.Any())
        {
            userList = new List<User>
            {
                new User
                {
                    Name = "Serhii", Technologies = new List<Technology> { technologyList[0], technologyList[1]}
                }
            };
            dbCtx.Users.AddRange(userList);
        }
    
        await dbCtx.SaveChangesAsync();
    }
    

    这取决于您在哪里以及如何使用它。我自己通过IHost 扩展方法使用它,例如 -

    public static async Task GenerateSeedDataAsync(this IHost host)
    {
        using (var scope = host.Services.CreateScope())
        {
            var provider = scope.ServiceProvider;
            var dbCtx = provider.GetRequiredService<DbCtx>();
            dbCtx.Database.Migrate();
    
            await AddEntities(dbCtx);
        }
    }
    

    这样每次应用启动时,我们都可以选择检查数据库是否需要做种,比如-

    public static void Main(string[] args)
    {
        IHost host = CreateHostBuilder(args).Build();
        host.GenerateSeedDataAsync().Wait();
        host.Run();
    }
    

    【讨论】:

    • 虽然我的回答简单而技术上回答了直接问题,但我必须说在这种情况下(和类似情况)我也更喜欢自定义播种。主要是因为这看起来不仅仅是一些基本数据的简单播种。是业务数据。我什至会更进一步,使用指定的业务方法来创建此类数据。例如,创建用户可能涉及业务逻辑。因此,用户应该通过该逻辑创建。绕过所有这些规则,数据层中不应该有一些阴暗的角落也会发生这种情况。
    • 这意味着:这种方法和布线都可以(+1),但我会在这里调用业务逻辑方法。
    • @GertArnold 谢谢。我非常感谢您的建议,并且必须同意这将是一种更专业的方法。
    猜你喜欢
    • 2021-03-30
    • 1970-01-01
    • 2021-10-11
    • 1970-01-01
    • 2013-05-02
    • 2021-11-05
    • 2020-02-24
    • 2018-06-02
    • 2020-03-23
    相关资源
    最近更新 更多