web-dev-qa-db-ja.com

関連付けテーブルに追加のフィールドを使用して、最初に多対多のコードを作成する

私はこのシナリオがあります:

public class Member
{
    public int MemberID { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual ICollection<Comment> Comments { get; set; }
}

public class Comment
{
    public int CommentID { get; set; }
    public string Message { get; set; }

    public virtual ICollection<Member> Members { get; set; }
}

public class MemberComment
{
    public int MemberID { get; set; }
    public int CommentID { get; set; }
    public int Something { get; set; }
    public string SomethingElse { get; set; }
}

流れるようなAPI との関連付けを設定するにはどうすればいいですか?それとも、関連テーブルを作成するためのより良い方法はありますか?

271
hgdean

カスタマイズされた結合テーブルと多対多の関係を作成することはできません。多対多の関係では、EFは結合テーブルを内部的に隠し管理します。モデルにEntityクラスがないテーブルです。このような結合テーブルを追加のプロパティで使用するには、実際には2つの一対多の関係を作成する必要があります。それはこのように見えるでしょう:

public class Member
{
    public int MemberID { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual ICollection<MemberComment> MemberComments { get; set; }
}

public class Comment
{
    public int CommentID { get; set; }
    public string Message { get; set; }

    public virtual ICollection<MemberComment> MemberComments { get; set; }
}

public class MemberComment
{
    [Key, Column(Order = 0)]
    public int MemberID { get; set; }
    [Key, Column(Order = 1)]
    public int CommentID { get; set; }

    public virtual Member Member { get; set; }
    public virtual Comment Comment { get; set; }

    public int Something { get; set; }
    public string SomethingElse { get; set; }
}

たとえばLastName= "Smith"のメンバーのすべてのコメントを検索したい場合は、次のようなクエリを書くことができます。

var commentsOfMembers = context.Members
    .Where(m => m.LastName == "Smith")
    .SelectMany(m => m.MemberComments.Select(mc => mc.Comment))
    .ToList();

...または...

var commentsOfMembers = context.MemberComments
    .Where(mc => mc.Member.LastName == "Smith")
    .Select(mc => mc.Comment)
    .ToList();

あるいは、 "Smith"という名前のメンバーのリスト(複数あると仮定します)をコメントとともに作成するには、射影を使用できます。

var membersWithComments = context.Members
    .Where(m => m.LastName == "Smith")
    .Select(m => new
    {
        Member = m,
        Comments = m.MemberComments.Select(mc => mc.Comment)
    })
    .ToList();

MemberId= 1でメンバーのすべてのコメントを見つけたい場合は、

var commentsOfMember = context.MemberComments
    .Where(mc => mc.MemberId == 1)
    .Select(mc => mc.Comment)
    .ToList();

これで、結合テーブルのプロパティでフィルタリングすることもできます(多対多の関係では不可能です)。たとえば、メンバ1の99 inプロパティSomethingname__を含むすべてのコメントをフィルタ処理します。

var filteredCommentsOfMember = context.MemberComments
    .Where(mc => mc.MemberId == 1 && mc.Something == 99)
    .Select(mc => mc.Comment)
    .ToList();

遅延ロードのために物事が簡単になるかもしれません。もしあなたがロードされたMembername__を持っているならば、あなたは明示的な問い合わせなしでコメントを得ることができるはずです:

var commentsOfMember = member.MemberComments.Select(mc => mc.Comment);

遅延読み込みは舞台裏で自動的にコメントを取得すると思います。

編集

このモデルでエンティティとリレーションシップを追加する方法とそれらを削除する方法のいくつかの例をもう少し面白くするために:

1)1人のメンバーとこのメンバーの2つのコメントを作成してください。

var member1 = new Member { FirstName = "Pete" };
var comment1 = new Comment { Message = "Good morning!" };
var comment2 = new Comment { Message = "Good evening!" };
var memberComment1 = new MemberComment { Member = member1, Comment = comment1,
                                         Something = 101 };
var memberComment2 = new MemberComment { Member = member1, Comment = comment2,
                                         Something = 102 };

context.MemberComments.Add(memberComment1); // will also add member1 and comment1
context.MemberComments.Add(memberComment2); // will also add comment2

context.SaveChanges();

2)member1の3番目のコメントを追加します。

var member1 = context.Members.Where(m => m.FirstName == "Pete")
    .SingleOrDefault();
if (member1 != null)
{
    var comment3 = new Comment { Message = "Good night!" };
    var memberComment3 = new MemberComment { Member = member1,
                                             Comment = comment3,
                                             Something = 103 };

    context.MemberComments.Add(memberComment3); // will also add comment3
    context.SaveChanges();
}

3)新しいメンバーを作成し、それを既存のコメント2に関連付けます。

var comment2 = context.Comments.Where(c => c.Message == "Good evening!")
    .SingleOrDefault();
if (comment2 != null)
{
    var member2 = new Member { FirstName = "Paul" };
    var memberComment4 = new MemberComment { Member = member2,
                                             Comment = comment2,
                                             Something = 201 };

    context.MemberComments.Add(memberComment4);
    context.SaveChanges();
}

4)既存のmember2とcomment3の間に関係を作成します。

var member2 = context.Members.Where(m => m.FirstName == "Paul")
    .SingleOrDefault();
var comment3 = context.Comments.Where(c => c.Message == "Good night!")
    .SingleOrDefault();
if (member2 != null && comment3 != null)
{
    var memberComment5 = new MemberComment { Member = member2,
                                             Comment = comment3,
                                             Something = 202 };

    context.MemberComments.Add(memberComment5);
    context.SaveChanges();
}

5)この関係をもう一度削除します。

var memberComment5 = context.MemberComments
    .Where(mc => mc.Member.FirstName == "Paul"
        && mc.Comment.Message == "Good night!")
    .SingleOrDefault();
if (memberComment5 != null)
{
    context.MemberComments.Remove(memberComment5);
    context.SaveChanges();
}

6)member1とそのコメントとの関係をすべて削除します。

var member1 = context.Members.Where(m => m.FirstName == "Pete")
    .SingleOrDefault();
if (member1 != null)
{
    context.Members.Remove(member1);
    context.SaveChanges();
}

MemberCommentsname__とMembername__の間、およびMemberCommentsname__とCommentname__の間の1対多の関係は、慣例的に連鎖削除を使用して設定されるため、これもMemberCommentsname__内の関係を削除します。 MemberIdname__内のCommentIdname__とMemberCommentname__はMembername__およびCommentname__ナビゲーションプロパティの外部キープロパティとして検出され、FKプロパティはnull以外のintname__型であるため、関係を削除する必要があるためです。このモデルでは意味があると思います。

497
Slauma

Slaumaによるすばらしい答え。

流暢な API マッピングを使ってこれを行うためのコードをポストします。

public class User {
    public int UserID { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }

    public ICollection<UserEmail> UserEmails { get; set; }
}

public class Email {
    public int EmailID { get; set; }
    public string Address { get; set; }

    public ICollection<UserEmail> UserEmails { get; set; }
}

public class UserEmail {
    public int UserID { get; set; }
    public int EmailID { get; set; }
    public bool IsPrimary { get; set; }
}

あなたのDbContext派生クラスでこれを行うことができます:

public class MyContext : DbContext {
    protected override void OnModelCreating(DbModelBuilder builder) {
        // Primary keys
        builder.Entity<User>().HasKey(q => q.UserID);
        builder.Entity<Email>().HasKey(q => q.EmailID);
        builder.Entity<UserEmail>().HasKey(q => 
            new { 
                q.UserID, q.EmailID
            });

        // Relationships
        builder.Entity<UserEmail>()
            .HasRequired(t => t.Email)
            .WithMany(t => t.UserEmails)
            .HasForeignKey(t => t.EmailID)

        builder.Entity<UserEmail>()
            .HasRequired(t => t.User)
            .WithMany(t => t.UserEmails)
            .HasForeignKey(t => t.UserID)
    }
}

それはいいえより良くも悪くもないという異なるアプローチで、受け入れられた答えと同じ効果を持ちます。

編集: CreatedDateをboolからDateTimeに変更しました。

編集2:時間が足りないため、これが機能することを確認するために取り組んでいるアプリケーションから例を示しました。

93
Esteban

@Esteban、提供したコードは正しいです、ありがとう、しかし不完全です、私はそれをテストしました。 "UserEmail"クラスに欠けているプロパティがあります。

    public UserTest UserTest { get; set; }
    public EmailTest EmailTest { get; set; }

誰かが興味を持った場合は私がテストしたコードを投稿します。よろしく

using System.Data.Entity;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Web;

#region example2
public class UserTest
{
    public int UserTestID { get; set; }
    public string UserTestname { get; set; }
    public string Password { get; set; }

    public ICollection<UserTestEmailTest> UserTestEmailTests { get; set; }

    public static void DoSomeTest(ApplicationDbContext context)
    {

        for (int i = 0; i < 5; i++)
        {
            var user = context.UserTest.Add(new UserTest() { UserTestname = "Test" + i });
            var address = context.EmailTest.Add(new EmailTest() { Address = "[email protected]" + i });
        }
        context.SaveChanges();

        foreach (var user in context.UserTest.Include(t => t.UserTestEmailTests))
        {
            foreach (var address in context.EmailTest)
            {
                user.UserTestEmailTests.Add(new UserTestEmailTest() { UserTest = user, EmailTest = address, n1 = user.UserTestID, n2 = address.EmailTestID });
            }
        }
        context.SaveChanges();
    }
}

public class EmailTest
{
    public int EmailTestID { get; set; }
    public string Address { get; set; }

    public ICollection<UserTestEmailTest> UserTestEmailTests { get; set; }
}

public class UserTestEmailTest
{
    public int UserTestID { get; set; }
    public UserTest UserTest { get; set; }
    public int EmailTestID { get; set; }
    public EmailTest EmailTest { get; set; }
    public int n1 { get; set; }
    public int n2 { get; set; }


    //Call this code from ApplicationDbContext.ConfigureMapping
    //and add this lines as well:
    //public System.Data.Entity.DbSet<yournamespace.UserTest> UserTest { get; set; }
    //public System.Data.Entity.DbSet<yournamespace.EmailTest> EmailTest { get; set; }
    internal static void RelateFluent(System.Data.Entity.DbModelBuilder builder)
    {
        // Primary keys
        builder.Entity<UserTest>().HasKey(q => q.UserTestID);
        builder.Entity<EmailTest>().HasKey(q => q.EmailTestID);

        builder.Entity<UserTestEmailTest>().HasKey(q =>
            new
            {
                q.UserTestID,
                q.EmailTestID
            });

        // Relationships
        builder.Entity<UserTestEmailTest>()
            .HasRequired(t => t.EmailTest)
            .WithMany(t => t.UserTestEmailTests)
            .HasForeignKey(t => t.EmailTestID);

        builder.Entity<UserTestEmailTest>()
            .HasRequired(t => t.UserTest)
            .WithMany(t => t.UserTestEmailTests)
            .HasForeignKey(t => t.UserTestID);
    }
}
#endregion
10
LeonardoX

TLDR;(EF6/VS2012U5のEFエディターのバグに関連する) DBからモデルを生成しても属性付きm:mテーブルが表示されない場合:2つの関連テーブルを削除します - >保存。 edmx - >データベースから生成/追加 - >保存。

EFの.edmxファイルに属性列との多対多の関係をどのように表示するか(現在は表示されず、一連のナビゲーションプロパティとして扱われるため)について疑問に思った人のために、これらのクラスを生成しました。あなたのデータベーステーブルから(またはMSの専門用語ではデータベースファーストです。私は信じています。)

問題の2つのテーブル(OPの例ではMemberとComment)を削除し、[データベースからモデルを生成]を使用してそれらをもう一度追加します。 (つまり、Visual Studioにそれらを更新させないでください - 削除、保存、追加、保存)

それからここで提案されていることに沿って3番目のテーブルを作成します。

これは、最初に純粋な多対多の関係が追加され、後で属性がDBで設計される場合に関連します。

これはこのスレッド/ Googlingからすぐには明らかになりませんでした。これは、問題を探すためのグーグル上のリンク#1であるが、最初にDB側から来るので、そこにそれを出すだけでよい。

0
Andy S

このエラーを解決する方法の1つは、外部キーとして必要なプロパティの上にForeignKey属性を配置して、ナビゲーションプロパティを追加することです。

注:ForeignKey属性では、括弧と二重引用符の間に、この方法で参照されるクラスの名前を入れます。

enter image description here

0
Oscar Echaverry

多対多構成の両方のフレーバーを実現できる解決策を提案したいと思います。

EFはスキーマのテーブルがEntitySetごとに最大で1回マップできることを検証するため、「キャッチ」は結合テーブルをターゲットとするビューを作成する必要があります。

この答えは、前の答えですでに述べたことに追加され、それらのアプローチのいずれも無効にするのではなく、それらに基づいています。

モデル:

public class Member
{
    public int MemberID { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual ICollection<Comment> Comments { get; set; }
    public virtual ICollection<MemberCommentView> MemberComments { get; set; }
}

public class Comment
{
    public int CommentID { get; set; }
    public string Message { get; set; }

    public virtual ICollection<Member> Members { get; set; }
    public virtual ICollection<MemberCommentView> MemberComments { get; set; }
}

public class MemberCommentView
{
    public int MemberID { get; set; }
    public int CommentID { get; set; }
    public int Something { get; set; }
    public string SomethingElse { get; set; }

    public virtual Member Member { get; set; }
    public virtual Comment Comment { get; set; }
}

構成:

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;

public class MemberConfiguration : EntityTypeConfiguration<Member>
{
    public MemberConfiguration()
    {
        HasKey(x => x.MemberID);

        Property(x => x.MemberID).HasColumnType("int").IsRequired();
        Property(x => x.FirstName).HasColumnType("varchar(512)");
        Property(x => x.LastName).HasColumnType("varchar(512)")

        // configure many-to-many through internal EF EntitySet
        HasMany(s => s.Comments)
            .WithMany(c => c.Members)
            .Map(cs =>
            {
                cs.ToTable("MemberComment");
                cs.MapLeftKey("MemberID");
                cs.MapRightKey("CommentID");
            });
    }
}

public class CommentConfiguration : EntityTypeConfiguration<Comment>
{
    public CommentConfiguration()
    {
        HasKey(x => x.CommentID);

        Property(x => x.CommentID).HasColumnType("int").IsRequired();
        Property(x => x.Message).HasColumnType("varchar(max)");
    }
}

public class MemberCommentViewConfiguration : EntityTypeConfiguration<MemberCommentView>
{
    public MemberCommentViewConfiguration()
    {
        ToTable("MemberCommentView");
        HasKey(x => new { x.MemberID, x.CommentID });

        Property(x => x.MemberID).HasColumnType("int").IsRequired();
        Property(x => x.CommentID).HasColumnType("int").IsRequired();
        Property(x => x.Something).HasColumnType("int");
        Property(x => x.SomethingElse).HasColumnType("varchar(max)");

        // configure one-to-many targeting the Join Table view
        // making all of its properties available
        HasRequired(a => a.Member).WithMany(b => b.MemberComments);
        HasRequired(a => a.Comment).WithMany(b => b.MemberComments);
    }
}

文脈:

using System.Data.Entity;

public class MyContext : DbContext
{
    public DbSet<Member> Members { get; set; }
    public DbSet<Comment> Comments { get; set; }
    public DbSet<MemberCommentView> MemberComments { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Configurations.Add(new MemberConfiguration());
        modelBuilder.Configurations.Add(new CommentConfiguration());
        modelBuilder.Configurations.Add(new MemberCommentViewConfiguration());

        OnModelCreatingPartial(modelBuilder);
     }
}

サルマ(@Saluma)から 答え

たとえば、LastName = "Smith"のメンバーのすべてのコメントを今すぐ見つけたい場合は、次のようなクエリを書くことができます。

これはまだ動作します...

var commentsOfMembers = context.Members
    .Where(m => m.LastName == "Smith")
    .SelectMany(m => m.MemberComments.Select(mc => mc.Comment))
    .ToList();

...しかし今はまた...

var commentsOfMembers = context.Members
    .Where(m => m.LastName == "Smith")
    .SelectMany(m => m.Comments)
    .ToList();

あるいは、 "Smith"という名前のメンバーのリスト(複数あると仮定します)をコメントとともに作成するには、射影を使用できます。

これはまだ動作します...

var membersWithComments = context.Members
    .Where(m => m.LastName == "Smith")
    .Select(m => new
    {
        Member = m,
        Comments = m.MemberComments.Select(mc => mc.Comment)
    })
    .ToList();

...しかし今はまた...

var membersWithComments = context.Members
    .Where(m => m.LastName == "Smith")
    .Select(m => new
    {
        Member = m,
        m.Comments
    })
        .ToList();

メンバーからコメントを削除したい場合

var comment = ... // assume comment from member John Smith
var member = ... // assume member John Smith

member.Comments.Remove(comment);

メンバーのコメントをInclude()したい場合は

var member = context.Members
    .Where(m => m.FirstName == "John", m.LastName == "Smith")
    .Include(m => m.Comments);

これはすべて構文糖のように感じますが、追加の設定をしても構わない場合は、いくつかの利点があります。どちらの方法でも、両方の方法をうまく利用できるようです。

0