不同DbContext和不同模式之间的实体框架关系

本文关键字:实体 框架 关系 之间 模式 DbContext 不同 | 更新日期: 2023-09-27 17:53:09

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

我在单独的DbContext和单独的类库中拥有Members类。我计划在多个项目中重用这个类库,为了便于区分,我将数据库模式设置为"acc"。我对这个库进行了广泛的测试,可以添加、删除和更新acc中的成员。成员表。

公会职业如下:

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项目的参考,并将映射添加到Members类的DbContext中,而Guild类是其中的一部分。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"}

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

我缩小了错误的原因。当我创建一个新公会时,我将公会领袖的成员ID设置为MemberID。这很好。但是,当我试图将领导者的成员对象添加到公会的成员列表时,这就是导致错误的原因。

更新2

这是我如何创建公会类所在的上下文的代码。(应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; }
}

这是我在repo中保存它的方式:

    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中添加到Member, RolePermission的映射。我必须添加角色和权限,因为成员有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

实体框架似乎不能很好地发挥这一点。为什么?我不确定,但我认为这是因为ContextM创建了成员的代理,并通过将成员克隆为新的成员对象,ContextM不再具有关联。我认为,这允许ContextG自由地使用新的Member对象。我尝试在我的DbContexts中设置ProxyCreationEnabled = false,但是从会话中抽出的成员对象一直是System.Data.Entity.DynamicProxies.Member.

我所做的是:

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

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

这让我完成了99%的任务。我不得不改变我的repo和我如何保存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();

不同DbContext和不同模式之间的实体框架关系

这是工作代码:

汇编"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":

  • 你的Guild
  • 你的Guild映射,尽管WillCascadeOnDelete(false)LeaderMemberInfo映射。
  • 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查询,所以它应该有所有需要的映射信息。

可以将相同的类型注册到两个不同的上下文中,甚至可以从不同的程序集注册。因此,您可以将Member映射到Guild的程序集的上下文中,我们称其为contextG,但仅当

  1. Member不指向contextG中没有映射的其他类型。这可能意味着必须显式忽略Member中的导航属性。
  2. Member不能引用contextG中的类型,因为这些类型不属于Member的上下文。

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

我发现,当我在使用实体和建立关系时遇到问题时,通常是因为我违背了框架的流程,或者试图建立技术上不完善的关系或抽象。我在这里的建议是,在深入研究这个具体问题之前,先回顾一下,分析一下一些事情。

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

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

根据你上面的模型,我可以看到我们在这个领域有几个关键的术语/对象:

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

另外,我们还有一些需要实现的业务规则:

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

因此,给定这些信息,让我们研究一下您的应用程序可能对该数据模型存在哪些问题。如果应用程序可以:

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

好了,现在我们可以切入正题了,就像他们说的…

在这种情况下,在公会和会员之间使用连接表是最优的。它将为您提供在多个公会或没有公会中拥有成员的能力,并提供低锁定更新策略-所以很好的调用!

说到公会领袖,有一些选择可能是有意义的。尽管可能永远不会出现公会中士,但我认为考虑一个名为公会领袖的新实体是有意义的。这种方法的好处是多方面的。你可以在应用程序中缓存公会领袖id列表,这样你就可以在本地应用程序缓存中缓存只有领袖id列表而不是整个领袖对象,而不是进行数据库访问来授权领导者所采取的公会行动;相反,您可以获得公会的领导者列表,无论查询方向如何,您都可以访问核心实体上的聚集索引或易于维护的连接实体上的中间索引。

就像我在开头提到的长"回答",当我遇到像你这样的问题时,通常是因为我违背了实体的谷物。我鼓励您重新考虑您的数据模型,以及如何使用一种更低摩擦的方法——松散多个模式并添加一个中间guild_leader对象。干杯!

除非您明确地说,Member实体应该映射到acc.Members,否则EF将期望它在dbo模式Members表中。为此,您需要为此类型提供EntityTypeConfiguration或用System.ComponentModel.DataAnnotations.Schema.TableAttribute注释它,如[Table("acc.Members")]

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

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

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

这将解决错误

我知道这已经无关紧要了,但我也有同样的问题,想和任何看到这篇文章的人分享我的解决方案:

我对这个问题的理解如下:

  • 你想创建一个模式为"a"和"b"
  • 您想要在这两个模式之间建立连接

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

DBModel模式"a":

    用户

模式"b":

    <
  • 会话/gh>
  • jwt

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

base.OnModelCreating(builder);
builder.HasDefaultSchema("a");
        
builder.Entity<User>(entity =>
{
     entity.Property(x => x.FirstName);
     entity.Property(x => x.LastName);
});
这可能发生,EF Core注意到User类上的导航属性,并在迁移中包含它们,因为我们不想包含它们,我们必须特别忽略它们:

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要从迁移中排除User表,但是由于您已经在模式的上下文中创建了这个表,因此没有必要再次创建它,因此您必须排除它。为什么我们不能使用"builder.Ignore()"然后呢?因为我们必须告诉EF Core表的模式,这只有通过这个方法才能实现。

这样做除了可以更容易地共享代码之外,没有任何直接的好处。例如,您可以为像User这样的公共服务提供一个基本结构。现在您希望在它的基础上进行构建,但总是希望在您的结构中使用User实体,而不同步数据库。现在你可以用User ->会话→Jwt结构并创建与用户结构连接的任何其他服务,例如带有user ->博客→帖子,用户->职位。

这样你总是使用相同的User表,而不需要在服务之间同步。

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