web-dev-qa-db-ja.com

異なるDbContextと異なるスキーマ間のEntityFrameworkの関係

つまり、メンバーとギルドという2つの主要なオブジェクトがあります。 1人のメンバーがギルドを所有でき、1人のギルドが複数のメンバーを持つことができます。

別のDbContextと別のクラスライブラリにMembersクラスがあります。このクラスライブラリを複数のプロジェクトで再利用する予定であり、区別しやすくするために、データベーススキーマを「acc」に設定します。このライブラリを広範囲にテストし、acc.Membersテーブルのメンバーを追加、削除、および更新できます。

ギルドクラスはそのようなものです:

_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プロジェクトへの参照を取得し、Guildクラスが含まれているDbContextのMembersクラスへのマッピングを追加しました。 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"}
_

これら2つのテーブル間でDbContext間で、異なるデータベーススキーマを使用して外部キーを利用するにはどうすればよいですか?

[〜#〜] update [〜#〜]

エラーの原因を絞り込みました。新しいギルドを作成するときは、ギルドリーダーのメンバーIDをMemberIDに設定します。これは正常に機能します。しかし、そのリーダーのメンバーオブジェクトをギルドのメンバーリスト(メンバー)に追加しようとすると、それがエラーの原因になります。

更新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();
        }
    }
_

最終的な解決策

そのため、Memberを含むDbContextのRolePermission、およびGuildにマッピングを追加する必要がありました。メンバーには_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
_

EntityFrameworkはこれでうまく機能していないようです。どうして?よくわかりませんが、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();
_
16
ScubaSteve

これは機能するコードです:

アセンブリ「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)
_

これは、既存のオブジェクトを関連付けるときにも機能します。


より一般的なケースの元の回答:

異なるコンテキストのタイプを1つのオブジェクトグラフに結合することはできません。つまり、あなたは次のようなことをすることはできません

_from a in context.As
join b in context.Bs on ...
_

... SQLクエリ全体を作成する必要があるコンテキストは常に1つあるため、必要なすべてのマッピング情報が含まれている必要があります。

ただし、異なるアセンブリからでも、同じタイプを2つの異なるコンテキストに登録できます。したがって、MemberのアセンブリのコンテキストでGuildをマップできます。これを、contextGと呼びましょう。ただし、次の場合に限ります。

  1. Memberは、ないcontextGにマップされている他のタイプを参照しません。これは、Memberのナビゲーションプロパティを明示的に無視する必要があることを意味する場合があります。
  2. MembercontextGの型を参照できません。これは、これらの型がMemberのコンテキストの一部ではないためです。

これらの条件のいずれかを満たせない場合は、Memberのアセンブリに新しいGuildクラスを作成し、そのマッピングをコンテキストに登録するのが最善の方法です。あいまいさを防ぐために別の名前を使用したい場合もありますが、これが唯一の選択肢です。

12
Gert Arnold

Memberエンティティを_acc.Members_にマップする必要があることを明示的に指定しない限り、EFはそれがdboスキーマMembersテーブルにあることを期待します。このためには、このタイプにEntityTypeConfigurationを指定するか、[Table("acc.Members")]のように_System.ComponentModel.DataAnnotations.Schema.TableAttribute_で注釈を付ける必要があります。

1
mcs_dodo

エンティティと関係の構築に問題がある場合、フレームワークの流れに逆らったり、技術的に整形式ではない関係や抽象化を構築しようとしていることが原因であることがよくあります。ここで提案するのは、この特定の問題を詳しく調べる前に、少し前に戻っていくつかのことを分析することです。

まず、オブジェクトグラフにアクセスする単一のアプリケーションである可能性が高いものに対して、ここで異なるスキーマを使用している理由に興味があります。状況によっては複数のスキーマが役立つ場合がありますが、Brent Ozarはこの点で非常に重要な点を示していると思います 記事 。その情報を踏まえて、最初に、複数のスキーマを1つにマージし、データベースに単一のDBコンテキストを使用するようにプッシュすることをお勧めします。

次に取り組むべきことは、オブジェクトグラフです。少なくとも私にとって、データモデリングで私が抱えていた最大の苦労は、アプリケーションがデータベースに対してどのような質問をしているのかを最初に理解していないときです。これが意味するのは、最初にアプリケーションがさまざまなコンテキストでどのデータを必要としているかを把握し、次にリレーショナルコンテキストでのパフォーマンスのためにそれらのデータ構造を最適化する方法を検討することです。それがどのように行われるか見てみましょう...

上記のモデルに基づいて、ドメインにいくつかの重要な用語/オブジェクトがあることがわかります。

  • ギルドのコレクション
  • メンバーのコレクション
  • ギルドリーダーのコレクション。

さらに、実装する必要のあるビジネスルールがいくつかあります。

  • ギルドは1人のリーダー(そしておそらく1人以上?)を持つことができます
  • ギルドリーダーはメンバーでなければなりません
  • ギルドには0人以上のメンバーのリストがあります
  • メンバーはギルドに所属できます(おそらく1つ以上?)

したがって、この情報を前提として、アプリケーションがこのデータモデルに対してどのような質問をする可能性があるかを調べてみましょう。私はアプリケーションができます:

  • メンバーを検索して、そのプロパティを確認します
  • メンバーを調べて、彼らの特性と彼らがギルドリーダーであることを確認してください
  • ギルドを調べて、そのすべてのメンバーのリストを見る
  • ギルドを調べて、ギルドリーダーのリストを見る
  • すべてのギルドリーダーのリストを検索する

さて、彼らが言うように、今、私たちは真鍮の鋲に取り掛かることができます...

この場合、ギルドとメンバー間の結合テーブルの使用が最適です。それはあなたに複数のギルドにメンバーを置くか、ギルドを持たない能力を提供し、低ロックの更新戦略を提供します-とても良い電話です!

ギルドリーダーに移ると、理にかなっているかもしれないいくつかの選択肢があります。ギルド軍曹というケースは決してないかもしれませんが、ギルドリーダーと呼ばれる新しい組織を検討することは理にかなっていると思います。このアプローチで可能なことはいくつかあります。ギルドリーダーIDのリストをアプリにキャッシュできるため、リーダーが実行するギルドアクションを承認するためにデータベースにアクセスするのではなく、リーダーIDリストのみを持ち、リーダーオブジェクト全体を持たないローカルアプリケーションキャッシュをヒットできます。逆に、ギルドのリーダーのリストを取得でき、クエリの方向に関係なく、コアエンティティのクラスター化インデックスまたは結合エンティティの保守が容易な中間インデックスをヒットできます。

この方法の冒頭で長い「答え」について述べたように、私があなたのような問題に遭遇したとき、それは通常、私がエンティティの粒に反対しているためです。データモデルと、摩擦の少ないアプローチでどのように考えるかを再考することをお勧めします。複数のスキーマを緩め、中間のguild_leaderオブジェクトを追加します。乾杯!

1
Bill Berry

私はあなたの更新された質問に答えています:

コンテキストを更新する前に、この行を使用してみてください

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

これはあなたが持っているエラーを解決します

0
Hussein Khalil