Entity Framework 不同 DbContext 和不同模式之间的关系

Entity Framework relationships between different DbContext and different schemas

所以,我有两个主要对象,会员和公会。一个会员可以拥有一个公会,一个公会可以拥有多个会员。

我在单独的 DbContext 和单独的 class 库中有成员 class。我计划在多个项目中重用这个 class 库,为了帮助区分,我将数据库模式设置为 "acc"。我已经广泛测试了这个库,可以在 acc.Members table.

中添加、删除和更新成员

公会class是这样的:

public class Guild
{
    public Guild()
    {
        Members = new List<Member>();
    }

    public int ID { get; set; }
    public int MemberID { get; set; }
    public virtual Member LeaderMemberInfo { get; set; }
    public string Name { get; set; }
    public virtual List<Member> Members { get; set; }
}

映射为:

internal class GuildMapping : EntityTypeConfiguration<Guild>
{
    public GuildMapping()
    {
        this.ToTable("Guilds", "dbo");
        this.HasKey(t => t.ID);
        this.Property(t => t.MemberID);
        this.HasRequired(t => t.LeaderMemberInfo).WithMany().HasForeignKey(t => t.MemberID);
        this.Property(t => t.Name);
        this.HasMany(t => t.Members).WithMany()
            .Map(t =>
            {
                t.ToTable("GuildsMembers", "dbo");
                t.MapLeftKey("GuildID");
                t.MapRightKey("MemberID");
            });
    }
}

但是,当我尝试创建一个新公会时,它说没有dbo.Members。

我参考了成员的 EF 项目,并将成员 class 的映射添加到 Guild class 所属的 DbContext。 modelBuilder.Configurations.Add(new MemberMapping()); (不确定这是否是最好的方法。)

这导致了这个错误:

{"The member with identity 'GuildProj.Data.EF.Guild_Members' does not exist in the metadata collection.\r\nParameter name: identity"}

如何利用这两个 table 之间的外键跨 DbContexts 和不同的数据库模式?

更新

我缩小了错误原因的范围。当我创建一个新公会时,我将会长的会员ID设置为MemberID。这很好用。但是,当我随后尝试将该领导者的成员对象添加到公会的成员列表(成员)时,这就是导致错误的原因。

更新 2

这是我如何创建公会 class 所在的上下文的代码。(根据 Hussein Khalil 的要求)

public class FSEntities : DbContext
{
    public FSEntities()
    {
        this.Configuration.LazyLoadingEnabled = false;
        Database.SetInitializer<FSEntities>(null);
    }

    public FSEntities(string connectionString)
        : base(connectionString)
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new GuildMapping());
        modelBuilder.Configurations.Add(new KeyValueMappings());
        modelBuilder.Configurations.Add(new LocaleMappings());

        modelBuilder.Configurations.Add(new MemberMapping());
    }

    public DbSet<Guild> Guilds { get; set; }
    public DbSet<KeyValue> KeyValues { get; set; }
    public DbSet<Locale> Locales { get; set; }
}

这就是我在回购中保存它的方式:

    public async Task CreateGuildAsync(Guild guild)
    {
        using (var context = new FSEntities(_ConnectionString))
        {
            context.Entry(guild.Members).State = EntityState.Unchanged;
            context.Entry(guild).State = EntityState.Added;
            await context.SaveChangesAsync();
        }
    }

最终决议

因此,我必须在包含 Guild 的 DbContext 中添加到 MemberRolePermission 的映射。我必须添加角色和权限,因为成员有 List<Role> Roles 并且每个角色有 List<Permission> Permissions.

这让我离解决方案更近了一步。我仍然收到如下错误:

{"The member with identity 'GuildProj.Data.EF.Member_Roles' does not exist in the metadata collection.\r\nParameter name: identity"}

在这里,当您从 Session 中拉取 Member 时,您会得到如下内容:

System.Data.Entity.DynamicProxies.Member_FF4FDE3888B129E1538B25850A445893D7C49F878D3CD40103BA1A4813EB514C

Entity Framework 似乎不太适合这个。为什么?我不确定,但我认为这是因为 ContextM 创建了 Member 的代理,并且通过将 Member 克隆到新的 Member 对象中,ContextM 不再具有关联。我认为,这允许 ContextG 自由使用新的 Member 对象。我尝试在我的 DbContexts 中设置 ProxyCreationEnabled = false,但是从 Session 中拉出的 Member 对象一直是 System.Data.Entity.DynamicProxies.Member.

类型

所以,我所做的是:

Member member = new Member((Member)Session[Constants.UserSession]);

我必须在各自的构造函数中克隆每个 Role 和每个 Permission

这让我完成了 99% 的工作。我不得不改变我的回购以及我如何保存 Guild 对象。

            context.Entry(guild.LeaderMemberInfo).State = EntityState.Unchanged;
            foreach(var member in guild.Members)
            {
                context.Entry(member).State = EntityState.Unchanged;
            }
            context.Entry(guild).State = EntityState.Added;
            await context.SaveChangesAsync();

我发现,当我在实体和建立关系方面遇到问题时,通常是因为我违背了框架的流程,或者试图建立技术上结构不佳的关系或抽象。我在这里的建议是在深入研究这个特定问题之前快速退后一步并分析一些事情。

首先,我很好奇为什么您在这里使用不同的模式来表示可能是单个应用程序访问对象图。多个模式在某些情况下可能很有用,但我认为 Brent Ozar 在这方面提出了一个非常突出的观点 article。鉴于这些信息,我倾向于首先建议您将多个模式合并为一个,然后转为对数据库使用单个 DB 上下文。

接下来要解决的是对象图。至少对我来说,我在数据建模方面遇到的最大困难是当我没有首先弄清楚应用程序对数据库有什么问题时。我的意思是首先弄清楚应用程序在其各种上下文中需要什么数据,然后研究如何优化这些数据结构以在关系上下文中提高性能。让我们看看如何做到这一点......

根据您上面的模型,我可以看到我们在域中有几个键 terms/objects:

  • 公会合集
  • 会员合集
  • 公会领袖合集。

此外,我们还有一些业务规则需要实施:

  • 一个公会可以有 1 位领导者(可能超过 1 位?)
  • 公会会长必须是会员
  • 公会有 0 个或更多成员的列表
  • 一个会员可以属于一个公会(可能超过 1 个?)

根据这些信息,让我们调查一下您的应用程序可能对此数据模型有哪些问题。我的应用程序可以:

  • 查找成员并查看其属性
  • 查找成员并查看他们的属性以及他们是公会会长
  • 查找公会并查看其所有成员的列表
  • 查找公会并查看公会领袖列表
  • 查找所有公会领袖的列表

好的,现在我们可以开始正题了,正如他们所说...

在这种情况下,在公会和会员之间使用连接 table 是最佳选择。它将为您提供在多个公会或没有公会中拥有成员的能力,并提供低锁定更新策略 - 太棒了!

关于公会领袖,有一些可能有意义的选择。尽管可能永远不会出现公会中士的情况,但我认为考虑一个称为公会领袖的新实体是有意义的。这种方法允许的是几倍。您可以在应用程序中缓存公会领导者 ID 列表,因此与其进行数据库访问以授权领导者采取的公会行动,不如访问仅具有领导者 ID 列表而不是整个领导者对象的本地应用程序缓存;反过来,你可以得到一个公会的领导者列表,无论查询方向如何,你都可以在核心实体上找到聚簇索引,或者在连接实体上找到易于维护的中间索引。

就像我在这篇文章开头提到的那样 "answer",当我 运行 遇到像你这样的问题时,通常是因为我违背了 Entity 的原则。我鼓励您重新考虑您的数据模型以及如何使用摩擦力较低的方法 - 松开多重模式并添加一个中间 guild_leader 对象。干杯!

除非您明确说明 Member 实体应映射到 acc.Members,否则 EF 将期望它位于 dbo 架构 Members table 中.为此,您需要为此类型提供 EntityTypeConfiguration 或使用 System.ComponentModel.DataAnnotations.Schema.TableAttribute 对其进行注释,例如 [Table("acc.Members")]

这是工作代码:

在汇编中"M":

public class Member
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class MemberMapping : EntityTypeConfiguration<Member>
{
    public MemberMapping()
    {
        this.HasKey(m => m.Id);
        this.Property(m => m.Name).IsRequired();
    }
}

正在组装 "G":

  • 你的Guildclass
  • 你的 Guild 映射,尽管 LeaderMemberInfo 映射中有 WillCascadeOnDelete(false)
  • modelBuilder.Configurations.Add(new GuildMapping());modelBuilder.Configurations.Add(new MemberMapping());

代码:

var m = new Member { Name = "m1" };
var lm = new Member { Name = "leader" };
var g = new Guild { Name = "g1" };
g.LeaderMemberInfo = lm;
g.Members.Add(lm);
g.Members.Add(m);
c.Set<Guild>().Add(g);
c.SaveChanges();

已执行 SQL:

INSERT [dbo].[Members]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[Members]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'leader' (Type = String, Size = -1)

INSERT [dbo].[Guilds]([MemberID], [Name])
VALUES (@0, @1)
SELECT [ID]
FROM [dbo].[Guilds]
WHERE @@ROWCOUNT > 0 AND [ID] = scope_identity()
-- @0: '1' (Type = Int32)
-- @1: 'g1' (Type = String, Size = -1)

INSERT [dbo].[GuildsMembers]([GuildID], [MemberID])
VALUES (@0, @1)
-- @0: '1' (Type = Int32)
-- @1: '1' (Type = Int32)

INSERT [dbo].[Members]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[Members]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'm1' (Type = String, Size = -1)

INSERT [dbo].[GuildsMembers]([GuildID], [MemberID])
VALUES (@0, @1)
-- @0: '1' (Type = Int32)
-- @1: '2' (Type = Int32)

这在关联现有对象时也有效。


更一般情况的原始答案:

您不能将不同上下文中的类型组合到一个对象图中。这意味着,你不能做类似

的事情
from a in context.As
join b in context.Bs on ...

...因为总有一个上下文应该创建整个 SQL 查询,所以它应该具有所有必需的映射信息。

您可以将同一类型注册到两个不同的上下文中,甚至可以从不同的程序集中注册。所以你可以在 Guild 的程序集的上下文中映射 Member,我们称它为 contextG,但前提是

  1. Member 不引用 映射到 contextG 的其他类型。这可能意味着必须显式忽略 Member 中的导航属性。
  2. Member 无法引用 contextG 中的类型,因为这些类型不是 Member 上下文的一部分。

如果无法满足这些条件中的任何一个,您最好的办法是在 Guild 的程序集中创建一个新的 Member class 并在上下文中注册其映射.也许您想使用不同的名称来防止歧义,但这是剩下的唯一选择。

我正在回答您更新后的问题:

在更新上下文之前尝试使用这一行

context.Entry(Guild.Members).State = Entity.EntityState.Unchanged

这将解决您遇到的错误

我知道这不再相关了,但我遇到了同样的问题,想与遇到此问题的任何人分享我的解决方案 post:

我对问题的理解如下:

  • 您想创建一个具有模式“a”和“b”的数据库
  • 您想在这两个模式之间建立联系

我的解决方案(使用 EFCore 5):

数据库模型 架构“a”:

  • 用户

架构“b”:

  • 会话数
  • Jwts

以下代码片段位于模式“a”上下文的 OnModelCreating 方法中:

base.OnModelCreating(builder);

builder.HasDefaultSchema("a");
        
builder.Entity<User>(entity =>
{
     entity.Property(x => x.FirstName);
     entity.Property(x => x.LastName);
});

EF Core 可能会注意到用户的导航属性 class 并将它们包含在迁移中,因为我们不想包含它们,所以我们必须特别忽略它们:

builder.Ignore<Session>();
builder.Ignore<Jwt>();

以下代码片段位于模式“b”上下文的 OnModelCreating 方法中:

base.OnModelCreating(builder);

builder.HasDefaultSchema("b");
        
builder.Entity<Session>(entity =>
{
     entity.HasOne(x => x.User)
         .WithMany(x => x.Sessions)
         .HasForeignKey(x => x.UserId);

     entity.HasMany(x => x.Jwts)
         .WithOne(x => x.Session)
         .HasForeignKey(x => x.SessionId);

     entity.Property(x => x.UserAgent);
});

builder.Entity<User>(entity => {
    entity.ToTable("Users", "a", t => t.ExcludeFromMigrations())
});

这有点违反直觉,因为您告诉 EF Core 从迁移中排除用户 table,但由于您已经在架构“a”的上下文中创建了 table,因此不需要再次创建它,所以你必须排除它。 那为什么我们不应该使用“builder.Ignore()”呢?因为我们必须告诉 EF Core table 的模式,这只能通过这种方法实现。

这样做除了可以更轻松地共享代码外,没有任何直接的优势。例如,您可以为像 User 这样的公共服务创建一个基本结构。现在你想在它的基础上构建,但总是希望你结构中的用户实体,而不同步数据库。您现在可以使用 User -> Session -> Jwt 结构构建 UserService 并创建与用户结构相关的任何其他服务,例如使用 User -> Blog -> Posts、User -> Posts 的 Blog Posts。

这样您就可以始终使用相同的用户 table 并且不必在服务之间进行同步。

小心: 这有一个主要缺点,因为如果特定实体有大量更新,您可能会降低数据库性能。这是因为一个特定的实体只能被一个进程改变,对于像用户这样的东西来说这不是一个大问题,但如果满足特定的情况就可以成为一个。