Створіть код спочатку, багато-багато, за допомогою додаткових полів у таблиці асоціацій


297

У мене такий сценарій:

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 ? Або є кращий спосіб створити таблицю асоціацій?

Відповіді:


524

Неможливо створити відносини «багато-до-багатьох» із спеціалізованою таблицею приєднання. У відносинах "багато-багато" EF управляє таблицею з'єднання внутрішньо і приховано. Це таблиця без класу Entity у вашій моделі. Для роботи з такою таблицею з'єднання з додатковими властивостями вам доведеться створити фактично два відносини один на багато. Це могло виглядати так:

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

Або створити список учасників з прізвищем "Сміт" (ми припускаємо, що їх існує більше) разом з їх коментарями ви можете використовувати проекцію:

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 властивостей Something:

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

Через ледачий завантаження речі можуть стати простішими. Якщо ви завантажили, Memberви можете отримувати коментарі без явного запиту:

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

Я здогадуюсь, що ледача завантаження автоматично отримує коментарі за кадром.

Редагувати

Для розваги ще кілька прикладів, як додати сутності та відносини та як їх видалити в цій моделі:

1) Створіть одного члена та два коментарі цього учасника:

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:

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) Створіть відносини між наявними учасниками2 та коментарем3:

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

Це видаляє відносини в MemberCommentsтеж тому , що один-ко-многим між Memberі MemberCommentsі між ними Commentі MemberCommentsможуть бути сконфігуровані з каскадними видалення за угодою. І це так, тому що MemberIdі CommentIdв MemberCommentідентифікуються як властивості сторонніх ключів для властивостей Memberі Commentнавігації, а оскільки властивості FK мають тип, що не зводиться intдо нуля , потрібне співвідношення, яке, нарешті, спричиняє каскад-видалення-налаштування. Має сенс у цій моделі, я думаю.


1
Дякую. Багато вдячні за надану вами додаткову інформацію.
hgdean

7
@hgdean: Я спамував ще кілька прикладів, вибачте, але це цікава модель, і питання про багато-багато-багато з додатковими даними в таблиці приєднання виникають раз у раз. Тепер наступного разу я маю на що зв’язатись ... :)
Slauma

4
@Esteban: Перевищення немає OnModelCreating. Приклад спирається лише на відображення умовних положень та конспектів даних.
Слаума

4
Примітка: якщо ви використовуєте цей підхід без Fluent API переконайтеся , що ви перевірити в базі даних , що у вас є тільки складовою ключ MemberIdі CommentIdстовпців , а не додатковий третій стовпець Member_CommentId(або що - щось подібне) - це означає , що ви не маєте точних імен , відповідних через об’єкти для ваших ключів
Simon_Weaver

3
@Simon_Weaver (або хтось, хто може знати відповідь) У мене схожа ситуація, але я хотів би мати первинний ключ "MemberCommentID" для цієї таблиці, це можливо чи ні? В даний час я отримую виняток, будь ласка , зверніть увагу на моє запитання, я дійсно потрібна допомога ... stackoverflow.com/questions/26783934 / ...
duxfox--

97

Відмінна відповідь Слауми.

Я просто опублікую код, щоб зробити це, використовуючи вільне відображення 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)
    }
}

Це має той самий ефект, що і прийнята відповідь, з іншим підходом, який ні кращим, ні гіршим.

EDIT: Я змінив CreateDate з bool на DateTime.

EDIT 2: Через брак часу я розмістив приклад програми, над якою працюю, щоб переконатися, що це працює.


1
Я думаю, що це неправильно. Ви створюєте відносини M: M тут, де це повинно бути 1: M для обох об'єктів.
CHS

1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.взято з: msdn.microsoft.com/en-us/data/hh134698.aspx . Джулі Лерман не може помилитися.
Естебан

1
Естебан, відображення відносин дійсно неправильне. @CHS вірно з цього приводу. Джулі Лерман говорить про "справжні" відносини "багато-багато", тоді як у нас є приклад для моделі, яку неможливо відобразити як багато-багато-багато. Ваше відображення навіть не складеться, оскільки у вас немає Commentsвласності Member. І ви не можете просто виправити це, перейменувавши HasManyвиклик, MemberCommentsоскільки MemberCommentсуб'єкт господарювання не має зворотної колекції для WithMany. Насправді вам потрібно налаштувати два відносини один на багато, щоб отримати правильне відображення.
Слаума

2
Дякую. Я дотримувався цього рішення, щоб зробити картування багато-до-багатьох.
Thomas.Benz

Я не знаю, але це краще працює з MySql. Без будівельника, Mysql видав мені помилку, коли я спробував міграцію.
Родріго Пріето

11

@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 = "address@" + 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

3

Я хочу запропонувати рішення, в якому можна досягти обох ароматів конфігурації багатьох до багатьох.

«Улов» - нам потрібно створити представлення, орієнтоване на таблицю приєднання, оскільки EF підтверджує, що таблиця схеми може бути відображена максимум один раз на кожну EntitySet.

Ця відповідь доповнює те, що вже було сказано в попередніх відповідях, і не перекриває жоден із цих підходів, він спирається на них.

Модель:

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

Або створити список учасників з прізвищем "Сміт" (ми припускаємо, що їх існує більше) разом з їх коментарями ви можете використовувати проекцію:

Це все ще працює ...

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

Це все схоже на синтаксичний цукор, однак ви отримуєте кілька піксів, якщо ви готові пройти додаткову конфігурацію. У будь-якому випадку вам здається, що ви зможете отримати найкраще з обох підходів.


Я вдячний за підвищення читабельності при наборі запитів LINQ. Мені, можливо, доведеться просто прийняти цей метод. Мені потрібно запитати, чи автоматично EntitySet автоматично оновлює представлення даних у базі даних? Чи погоджуєтесь ви, що це здається схожим на [Цукор], як описано у плані EF5.0? github.com/dotnet/EntityFramework.Docs/blob/master/…
Krptodr

Мені цікаво, чому вам здається переосмислити EntityTypeConfiguration<EntityType>ключ та властивості типу сутності. Напр., Property(x => x.MemberID).HasColumnType("int").IsRequired();Здається, зайвим public int MemberID { get; set; }. Чи можете ви зрозуміти моє незрозуміле розуміння?
хв.

0

TLDR; (напівзв’язана з помилкою редактора EF у EF6 / VS2012U5) якщо ви генеруєте модель з БД, і ви не можете побачити атрибутовану таблицю m: m: Видаліть дві пов'язані таблиці -> Зберегти .edmx -> Створити / додати з бази даних - > Зберегти.

Для тих, хто прийшов сюди, цікаво, як отримати зв’язок «багато на багато» із стовпцями атрибутів, які відображатимуться у файлі EF .edmx (так як він наразі не відображатиметься та трактується як набір навігаційних властивостей), і ви створили ці класи з таблиці вашої бази даних (або я вважаю, що вперше в MS lingo).

Видаліть дві таблиці, про які йдеться (для прикладу ОП, член та коментар) у вашому .edmx та додайте їх знову через "Створення моделі з бази даних". (тобто не намагайтеся дозволити Visual Studio оновлювати їх - видалити, зберегти, додати, зберегти)

Потім він створить 3-ту таблицю відповідно до запропонованого тут.

Це актуально у випадках, коли спочатку додається чисте співвідношення "багато хто до багатьох", а атрибути розробляються в БД пізніше.

Це було не відразу зрозуміло з цієї теми / Гуглінг. Тож просто виставивши його там, оскільки це посилання №1 в Google, яка шукає проблему, але спочатку йде з боку БД.


0

Один із способів вирішити цю помилку - поставити ForeignKeyатрибут поверх потрібного вам властивості як зовнішній ключ та додати властивість навігації.

Примітка. В ForeignKeyатрибуті між дужками та подвійними лапки розмістіть назву класу, на який посилається таким чином.

введіть тут опис зображення


Будь ласка, додайте мінімальне пояснення до самої відповіді, оскільки надане посилання може бути недоступним у майбутньому.
n4m31ess_c0d3r

2
Це має бути назва властивості навігації , а не клас.
aaron
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.