Страницы

Поиск по вопросам

вторник, 28 января 2020 г.

Как работать со связями многие-ко-многим, используя LINQ?

#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 ICollection GuideWorkTypeStandardHour1s {
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 ICollection ABLinks { 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 ICollection GuideWorkTypeStandardHour1s { 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();

Комментариев нет:

Отправить комментарий