#c_sharp #sql #winforms #entity_framework #linq
Как работать со связями многие-ко-многим? Я реализовал программу для работы с БД, использовал Entity Framework, т.е. создал классы модели и т.д. При разработке ПО выяснилось что две таблицы из БД должны иметь связь многие ко многим. Когда база была создана (подход Codе First) в базе между двумя таблицами появилась ещё одна. Всё в принципе хорошо программа работает, но вот из-за того что появилась эта таблица я не могу работать программно с ней т.к. я не создавал класса(модель) этой таблицы в итоге когда у меня есть какой нибудь Id первой таблице по которому мне нужно получить что нибудь из второй через LINQ я этого сделать не могу, приходится писать запрос "вручную". Как можно решить данную проблему, что бы можно было программно работать со связями многие ко многим через LINQ? ServiceStationContext db = new ServiceStationContext(); public class WorkOrder1 { public int Id { get; set; } public string Accepter { get; set; } public string Foreman { get; set; } public string myDate { get; set; } public ICollectionGuideWorkTypeStandardHour1s { get; set; } public WorkOrder1() { GuideWorkTypeStandardHour1s = new List (); } } public class GuideWorkTypeStandardHour1 { public int Id { get; set; } public string CodeWork { get; set; } public ICollection WorkOrder1s { get; set; } public GuideWorkTypeStandardHour1() { WorkOrder1s = new List (); } } Сохранение в БД: private void button1_Click_1(object sender, EventArgs e) { WorkOrder1 wo = new WorkOrder1(); wo.myDate = maskedTextBoxData.Text; wo.Accepter = textBoxAccepter.Text; wo.Foreman = textBoxForeman.Text; wo.BestPractice = textBox4Recommendation.Text; db.WorkOrders.Add(wo); db.SaveChanges(); } Считывание из БД с учётом подсказки указанной ниже(т.е. св-ва теперь virtual) List guideWorkTypeStandardHour1; WorkOrder1 workOrder1 = db.WorkOrders.Find(ListWorkOrders.workorderselectedId); var works = db.GuideWorkTypeStandardHour1s; foreach(var w in works) { if(workOrder1.GuideWorkTypeStandardHour1s.Contains(w)) { guideWorkTypeStandardHour1.Add(w); } }
Ответы
Ответ 1
Писать длинные имена классов мне лень - поэтому я поясню работу с MtM-связами на примере вот такой простой модели: public class A { public int Id { get; set; } public virtual ICollection Bs { get; set; } } public class B { public int Id { get; set; } public virtual ICollection As { get; set; } } 1. Сохранение связи в базу 1.1 Создание связи между уже привязанными к контексту сущностями public void Connect(A a, B b) { if (a.Bs == null) // Проверка на случай отсутствия Lazy Loading a.Bs = new List(); a.Bs.Add(b); // И не забыть SaveChanges() } 1.2 Создание связи по Id public void Connect(DbContext ctx, int ida, int idb) { var a = new A { Id = ida }; var b = new B { Id = idb }; // Если сущности c указанными ключами уже загружены в контекст - тут будет ошибка // Постарайтесь, чтобы так не случалось (лучший способ - каждый раз создавать новый контекст) ctx.Entry(a).State = EntityState.Unchanged; ctx.Entry(b).State = EntityState.Unchanged; a.Bs = new List { b }; // Если тут использовать массив - полезут ошибки при отслеживании связей в будущем. Но если контекст - временный, то можно и массив использовать. ctx.SaveChanges(); // Очистка контекста - можно не делать, если контекст больше не будет использоваться ctx.Entry(a).State = EntityState.Detached; ctx.Entry(b).State = EntityState.Detached; } 2. Удаление связи между сущностями 2.1 Сущности уже загружены в контекст public void Disconnect(A a, B b) { a.Bs.Remove(b); // Тут не может быть NPE если обе записи загружены в контекст. // И не забыть SaveChanges() } 2.2 Сущностей в контексте еще нет public void Disconnect(DbContext ctx, int ida, int idb) { var a = new A { Id = ida }; var b = new B { Id = idb }; a.Bs = new List { b }; // Если сущности c указанными ключами уже загружены в контекст - тут будет ошибка // Постарайтесь, чтобы так не случалось (лучший способ - каждый раз создавать новый контекст) ctx.Entry(a).State = EntityState.Unchanged; ctx.Entry(b).State = EntityState.Unchanged; a.Bs.Remove(b); ctx.SaveChanges(); // Очистка контекста - можно не делать, если контекст больше не будет использоваться ctx.Entry(a).State = EntityState.Detached; ctx.Entry(b).State = EntityState.Detached; } 3. Загрузка связей из БД 3.1 Lazy Loading включен a.Bs // оно само загрузится 3.2 Ручная загрузка ctx.Entry(a).Collection(_ => _.Bs).Load() a.Bs // теперь загружено 3.3 Включение в запрос var q = (from a in ctx.As where a.Id = 5 select a).Include(a => a.Bs) 3.4 Получение связей по Id var a = new A { Id = ida }; ctx.Entry(a).State = EntityState.Unchanged; ctx.Entry(a).Collection(_ => _.Bs).Load(); //теперь a.Bs не пусто Я привел примеры работы со связями. Но не рассматривайте их как готовые подпрограммы - число реальных ситуаций намного больше рассмотренных тут (к примеру, одна сущность может быть уже в контексте - а вторая задана своим Id). Это именно примеры. Отдельно замечу: почти любая операция над таблицей связей, кроме добавления новой связи, требует полной загрузки всех связанных записей. Если такое поведение нежелательно - надо создавать отдельную связную сущность, преобразовав MtM-отношение в два 1tM. Примерно так: public class A { public int Id { get; set; } public virtual ICollectionABLinks { get; set; } } public class B { public int Id { get; set; } public virtual ICollection ABLinks { get; set; } } public class ABLink { [Key] public int AId { get; set; } [ForeignKey("AId")] public virtual A A { get; set; } [Key] public int BId { get; set; } [ForeignKey("BId")] public virtual B B { get; set; } } В таком варианте запросы к БД несколько усложняются - зато можно удалять связи зная только Id концов, без операций загрузки данных из БД вообще. Еще можно реализовать в сущности-связи интерфейс IEntityWithChangeTracker, чтобы иметь доступ из нее до контекста БД, чтобы можно было удалять ее зная только ссылку на нее саму, после чего реализовать коллекцию-проекцию, преобразующую ICollection в ICollection - но это уже высший пилотаж. Ответ 2
public class WorkOrder1 { public int Id { get; set; } public string Accepter { get; set; } public string Foreman { get; set; } public string myDate { get; set; } public virtual ICollectionGuideWorkTypeStandardHour1s { get; set; } public WorkOrder1() { GuideWorkTypeStandardHour1s = new List (); } } public class GuideWorkTypeStandardHour1 { public int Id { get; set; } public string CodeWork { get; set; } public virtual ICollection WorkOrder1s { get; set; } public GuideWorkTypeStandardHour1() { WorkOrder1s = new List (); } } что бы получить связанные данные необходимо либо воспользоваться lazy loading для этого необходимо навигационное свойство переделать следующим образом public virtual ICollection GuideWorkTypeStandardHour1s { get; set; } тогда при обращении к навигационному свойству EF подгрузит необходимые данные (аналогичным образом во втором классе), либо необходимо это сделать следующим образом: var order = db.WorkOrder .Where(x=>x.Id==512) .Include(x=>x.GuideWorkTypeStandardHour1s); т.е. подгрузить явно вот смотрите пример только с немного другими моделями: public class Team { public int Id { get; set; } public string Name { get; set; } public virtual ICollection Players { get; set; } public Team() { Players = new List (); } } public class Player { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public string Position { get; set; } public virtual ICollection Teams { get; set; } public Player() { Teams = new List (); } } public class DefaultContext : DbContext { public DbSet Players { get; set; } public DbSet Teams { get; set; } } static void Main(string[] args) { var db = new DefaultContext(); Player pl1 = new Player { Name = "Роналду", Age = 31, Position = "Нападающий" }; Player pl2 = new Player { Name = "Месси", Age = 28, Position = "Нападающий" }; Player pl3 = new Player { Name = "Хави", Age = 34, Position = "Полузащитник" }; Team t1 = new Team { Name = "Барселона" }; t1.Players.Add(pl2); t1.Players.Add(pl3); Team t2 = new Team { Name = "Реал Мадрид" }; t2.Players.Add(pl1); List teams = new List (){ t1,t2 }; db.Teams.AddRange(teams); db.SaveChanges(); var playerInTeam1 = db.Teams.First(); } пример взят здесь Ответ 3
Из за какой то ошибки не могу опубликовать ответ от себя (баллы обнулились). Всем спасибо! Ответ на свой вопрос нашёл благодаря подсказке @Pavel Mayorov: @Vladimir Так у вас, получается, не с MtM проблема, а с устаревшими данными в контексте! Вот решение моей проблемы! код который требовался: db.Entry(workOrder1).Collection(p => p.GuideWorkTypeStandardHour1s).Load();
Комментариев нет:
Отправить комментарий