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

19

所以,我有两个主要的对象,Member和Guild。一个Member可以拥有一个Guild,而一个Guild可以有多个Members。

我把Members类放在了一个单独的DbContext和单独的类库中。我计划在多个项目中重复使用这个类库,并为了区分不同的项目,将数据库模式设置为“acc”。我已经对这个库进行了广泛的测试,并且可以在acc.Members表中添加、删除和更新Members。

Guild类如下:

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。

我参考了Member的EF项目,并将Members类的映射添加到了Guild类所在的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"}

如何在不同的数据库架构和DbContext之间利用这两个表之间的外键?

更新

我找到了错误的原因。当我创建新公会时,我将公会领袖的成员ID设置为MemberID。这很好地运行。但是,当我尝试将该领导者的成员对象添加到公会成员列表(Members)中时,就会出现错误。

更新2

这是我创建Guild类所在的上下文的代码。(由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();
        }
    }

最终解决方案

因此,我不得不在DbContext中为包含GuildMemberRolePermission添加映射。我不得不添加RolePermission,因为MemberList<Role> Roles,而每个Role都有List<Permission> Permissions

这让我更接近解决方案。但我仍然会遇到诸如以下错误:

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

在这里,当你从 Session 中获取成员时,你会得到类似以下的内容:

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]);

我不得不在各自的构造函数中克隆每个RolePermission

这让我接近成功,但我还需要修改我的仓库以及保存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();
5个回答

13
这是可运行的代码:
在汇编语言"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 映射,尽管在 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查询,所以它应该具有所有必需的映射信息。

您可以在两个不同的上下文中注册相同类型,即使来自不同的程序集。因此,您可以将Member映射到Guild程序集中的上下文(称为contextG),但前提是:

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

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


1
能否使用同一个Member类呢?虽然在不同的上下文中映射是不同的,而且一个Member对象如果没有从第一个上下文中分离出来就无法在上下文之间传输,但这似乎是可能的。 - jjj
@jjj 是的,但我假设“Member”将有其他不应映射的关联。如果“Member”是一个POCO,并且映射是通过流畅的映射完成的,则可以通过明确地保留导航属性未映射来实现。但我认为,在一个应用程序中拥有可以由不同上下文实现的“Member”对象是令人困惑的。 - Gert Arnold
公会领袖属性是成员类型,正确地保存到数据库中,但成员列表属性却无法正常工作,这一点毫无意义。两者都只保存ID。 - ScubaSteve
我稍微修改了我的答案。同时我自己也尝试了你的模型。一个程序集中的成员,在另一个公会中,映射相同。没有任何问题(EF6.1.3)。 - Gert Arnold
是的,我也在使用EF6.1.3。你能够让成员列表正常工作吗?就像我说的,使用Member属性可以正常工作,但是使用List<Member>属性却不能正常工作,这毫无意义。 - ScubaSteve
显示剩余10条评论

2
我发现当我遇到实体和建立关系的问题时,往往是因为我违背了框架的流程或尝试建立不符合技术规范的关系或抽象。在深入探究这个具体问题之前,我建议您先退一步分析一下几个事情。
首先,我想知道为什么您在可能使用单个应用程序访问对象图的情况下在此处使用不同的模式。多个模式在某些情况下可能很有用,但我认为Brent Ozar在这篇article中提出了一个非常重要的观点。鉴于这些信息,我倾向于建议您将多个模式合并为一个,并推荐使用单个DB上下文来管理数据库。
接下来要解决的问题是对象图。至少对我来说,数据建模最大的挑战是当我没有首先弄清楚应用程序对数据库的需求时。我的意思是先确定应用程序在各种上下文中需要哪些数据,然后再考虑如何在关系上下文中优化这些数据结构以获得更好的性能。让我们看看如何做到这一点...
根据您上面的模型,我可以看到我们在该领域中有一些关键术语/对象:
  • 公会集合
  • 成员集合
  • 公会领袖集合。
此外,我们需要实现一些业务规则:
  • 公会可以有1个领袖(可能不止一个?)
  • 公会领袖必须是成员
  • 公会有一个包含0个或多个成员的列表
  • 成员可以属于一个公会(可能不止一个?)
因此,考虑到这些信息,让我们探究一下您的应用程序对此数据模型可能具有的问题。该应用程序可以:
  • 查找成员并查看其属性
  • 查找成员并查看其属性以及他们是否是公会领袖
  • 查找公会并查看其所有成员的列表
  • 查找公会并查看其公会领袖列表
  • 查找所有公会领袖的列表
好的,现在我们可以进入正题了...
在这种情况下,使用公会和成员之间的连接表是最佳选择。它将使您能够在多个公会或没有公会中拥有成员,并提供低锁定更新策略 - 所以做得好!
接下来谈到公会领袖,有几个选择可能是有意义的。即使可能永远不会出现公会中士兵的情况,我认为考虑一个名为公会领袖的新实体是有意义的。这种方法允许的几个方面。您可以在应用程序中缓存公会领袖ID列表,因此,与其进行数据库操作以授权由领导者采取的公会行动,您可以访问本地应用程序缓存,该缓存仅具有领导者ID列表而不是整个领导者对象;相反,您可以获取公会领袖的列表,并且无论查询方向如何,都可以在核心实体上击中聚集索引或易于维护的连接实体上的索引。
正如我在这个过长的“答案”开始时所指出的那样,当我遇到像你这样的问题时,通常是因为我正在反对Entity的潮流。我鼓励您重新思考数据模型以及如何通过更低摩擦的方法 - 放弃多个模式并添加一个中间的guild_leader对象。干杯!

1
这对于阐明在应用程序中思考数据使用的过程非常有帮助。 :) - the VBE - it's right for me

1

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


在我的成员映射类中,我确实告诉它使用acc模式。原始成员代码中的所有内容都可以正常工作,例如:添加、删除、更新成员。但当我尝试将其拉入另一个DbContext时,就会出现问题。 - ScubaSteve
您不能将来自不同上下文的类型合并到一个图中。如果您想使用一个使用来自两个不同上下文的表的图树,则只需在其中一个上下文中创建缺失的类型即可。 - mcs_dodo

0
我正在回答你的更新问题:
在更新你的上下文之前,尝试使用这行代码。
context.Entry(Guild.Members).State = Entity.EntityState.Unchanged

这将解决你遇到的错误


你可以更新你的问题并附上代码,我会告诉你具体在哪里放置这行代码。 - Hussein Khalil
我尝试了这个,但是出现了错误:{"实体类型List`1不是当前上下文的模型的一部分。"} - ScubaSteve
请更新您的问题并说明如何创建您的上下文,以便我能够提供帮助。您面临的错误是由于上下文创建错误导致的。 - Hussein Khalil
我更新了它,包括了Guild类的上下文设置。 - ScubaSteve
你具体遇到了什么错误?我已经进行了模拟,一切正常。请查看此链接,它可能会有所帮助:https://msdn.microsoft.com/zh-cn/data/jj592676.aspx - Hussein Khalil

0

我知道这个问题已经不相关了,但我遇到了同样的问题,想要分享我的解决方案给任何看到这篇文章的人:

我理解这个问题如下:

  • 你想创建一个带有模式"a"和"b"的数据库
  • 你想在这两个模式之间建立连接

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

DBModel 模式"a":

  • Users

模式"b":

  • Sessions
  • 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会注意到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 表格从迁移中,但是由于你已经在 schema "a" 的上下文中创建了表格,所以没有必要再次创建它,因此你必须将其排除。

那么为什么我们不使用 "builder.Ignore()" 呢?因为我们必须告诉 EF Core 表格的模式,而这只能通过这种方法实现。

这样做没有任何直接的优势,除了你可以更轻松地共享代码。例如,你可以拥有一个基本结构用于像 User 这样的常见服务。现在你想要在其基础上构建,但始终希望在你的结构中包含 User 实体,而无需同步数据库。你现在可以构建一个 UserService,其中包含 User -> Session -> Jwt 结构,并创建与用户结构相关的任何其他服务,例如 User -> Blog -> Posts、User -> Posts。

这样你就可以始终使用相同的 User 表格,而无需在服务之间进行同步。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接