EF Core обновляет неотслеживаемую сущность со множеством коллекций

#entity-framework

Вопрос:

Я сталкиваюсь с проблемой с ядром и коллекциями EF; У меня есть люди, которые читают книги, книги могут читать несколько человек, и люди могут читать несколько книг (это отношение «многие ко многим»). Мой EF генерирует 3 таблицы Books , Persons и BookPersons .

Когда я вставляю новых людей с набором книг, которые они читают, проблем не возникает. Тем не менее, когда я воссоздаю одного из людей вне контекста БД (тот же идентификатор, но измененная коллекция прочитанных книг) и пытаюсь его сохранить, это не удается в отношении «многие ко многим». Потому что связь между существующим уже существует (не уникальное ограничение)

Я пытался:

  • чтобы прикрепить коллекцию книг к контексту (та же ошибка)
  • человек (без ошибок, но и без изменений)
  • только измените данные о человеке, а не коллекцию (неотслеживаемая сущность сохраняется, но мои прочитанные книги не сохраняются)

Я не очень люблю управлять BookPersons таблицей или сначала выполнять запросы для получения существующих сущностей. Моя цель-обновить человека и его прочитанные книги за один раз. Я знаю, как написать это на SQL, но, похоже, EF-довольно сложная задача.

Если вы хотите просмотреть мой код, посетите: https://github.com/CasperCBroeren/EfCollectionsProblem/blob/master/Program.cs

Спасибо, что объяснили, что я упускаю или не понимаю

Ответ №1:

Я бы создал модель PersonBooks, чтобы справиться с этим,

Модель книги

  public class Book
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int Id { get; set; }

        public string Title { get; set; }

        [ForeignKey("BookId")]
        public virtual ICollection<PersonBook> PersonBooks { get; set; }
    }
 

Модель человека

 public class Person
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int Id { get; set; }

        public string Name { get; set; }
        
        [ForeignKey("PersonId")]
        public virtual ICollection<PersonBook> PersonBooks { get; set; }
    }
 

Модель персональной книги

 public class PersonBook
    {
        public int Id { get; set; }

        public int PersonId { get; set; }

        public int BookId { get; set; }

        public virtual Person Person { get; set; }

        public virtual Book Book  { get; set; }
    }
 

затем вы можете получить все идентификаторы книг, прочитанные лично, используя

 var personId = 15; // what ever you want
db.PersonBooks.Where(a=> a.PersonId == personId);
 

или получить все удостоверения личности людей, которые читали книгу, по идентификатору

 var bookId= 11; // what ever you want
db.PersonBooks.Where(a=> a.BookId== bookId);
 

Примечание:
вы можете связаться с объектом книги, используя, например

 db.PersonBooks.Where(a=> a.PersonId == personId).FirstOrDefault().Book;
 

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

1. Спасибо, но это касается только поиска. Моя проблема больше связана с сохранением обновленной коллекции

Ответ №2:

Ключевым фактором в EF является работа со ссылками на объекты. Любая ссылка, которую DbContext не отслеживает, будет рассматриваться как новая сущность. На Update самом деле следует избегать использования метода в наборах баз данных, так как это может привести к неэффективным и потенциально опасным изменениям данных.

Этот параметр: «прикрепить коллекцию книг к контексту» работает с отдельными ссылками, но не работает с коллекциями. Проблема в том, что вы хотите сказать: «добавьте любую книгу, с которой человек еще не связан», однако DbContext не знает, с какими книгами этот человек уже связан, если вы сначала не получите эту информацию.

… или сначала выполняет запросы, чтобы получить существующие сущности.

Это на самом деле то, что вы должны делать в большинстве случаев. В случае простого консольного приложения для проверки идей и изучения того, как работает EF, это может показаться излишним, но в реальных системах это рекомендуемый подход по ряду причин.

  1. Сохраняя полезную нагрузку небольшой. Возьмите API или веб — сайт, на котором вы разрешаете пользователю связывать книги с людьми. Отправка полного представления людей, их книг и т. Д. Туда и обратно между сервером и клиентом Может быть потенциально дорогостоящей с точки зрения размера данных. Если у меня есть API, который позволяет мне связывать книги с человеком, если эти книги уже отражают известное состояние данных (уже существуют в БД), то все, что мне нужно передать, — это идентификаторы. При передаче данных в представления идея состоит в том, чтобы передавать только то, что нужно представлению, а не целые графики сущностей.
  2. Обеспечение безопасности полезной нагрузки. Передача целых объектов и использование подобных методов Update могут сделать ваши системы подверженными вмешательству. Update обновит все столбцы в сущности независимо от того, ожидаете ли вы, разрешите ли им измениться или нет. Сводя к минимуму количество возвращаемых данных, вы гарантируете, что могут измениться только ожидаемые детали, и по определению подтверждаете безопасность предоставленных значений.

Например, если у меня есть служба, которая хотела обновить книги, связанные с человеком. В пользовательском интерфейсе, который я загрузил, у Джона была «Книга джунглей (ID: 1)», и я хотел обновить ассоциации, чтобы у Джона теперь была «Книга джунглей» и «Том Сойер». Хотя мой пользовательский интерфейс теперь может это разрешить, безусловно, возможно , что клиентский браузер может перехватить вызов моего контроллера / веб-службы и, увидев Book { ID: 1, Name: "Jungle Book" } , как эти данные будут отправлены, вмешаться в отправку Book { ID: 1, Name: "Hitchhiker's Guide to the Galaxy"} . При условии, что вы решили эту проблему таким образом, что это привело к присоединению сущностей и выполнению Update или тому подобному, следствием такого вмешательства будет то, что злоумышленник может переименовать книгу. Это будет иметь эффект потока для каждого Человека, который ссылается на идентификатор книги № 1.

Вместо этого, если я хочу иметь что-то вроде метода «Книги обновлений», который может переназначать книги для человека, у меня был бы метод примерно такой:

 private void UpdateBooks(int personId, params int[] bookIds)
{
    using (var context = new AppDbContext())
    {
        var person = context.Persons
            .Include(x => x.Books)
            .Single(x => x.PersonId == personId);

       var existingBookIds = person.Books.Select(x => x.BookId).ToList();
       var bookIdsToAdd = bookIds.Except(existingBookIds).ToList();
       var bookIdsToRemove = existingBookIds.Except(bookIds).ToList();

       foreach(var bookId in bookIdsToRemove)
       {
           var book = person.Books.Single(x => x.BookId == bookId);
           person.Books.Remove(book);
       }
       
       if (bookIdsToAdd.Any())
       {
           var booksToAdd = context.Books
               .Where(x => bookIdsToAdd.Contains(x.BookId))
               .ToList();
           if(booksToAdd.Count != bookIdsToAdd.Count)
           {
              // Handle scenario where one or more book IDs provided weren't found.
           }
           person.Books.AddRange(booksToAdd);
      }
      
      context.SaveChanges();
    }
}
 

Это предполагает, что EF обрабатывает личные книги полностью за кулисами, где личная книга состоит только из идентификатора личности и идентификатора книги, так как у человека может быть коллекция Книг, а не личные книги.

В этом примере выполняется до двух запросов SELECT. Один для получения человека и его текущих книг, а другой для получения любых новых книг, если таковые необходимо добавить. Нет никакого риска подделки книг, и мы можем легко проверить такие сценарии, как передача идентификатора неизвестной книги. Может возникнуть соблазн избежать запросов, считая это дорогостоящим, но в большинстве случаев EF может предоставлять данные довольно быстро и эффективно. Это скорее исключение, чем норма, и вам, возможно, придется проявить творческий подход, чтобы обойти возможные узкие места в производительности при доступе к данным.

  1. Третье соображение состоит в том, чтобы сосредоточиться на сохранении атомарности операций, особенно для таких вещей, как веб-сервисы / веб-приложения. Это неприменимо, когда вы просто знакомитесь с работой EF, сущностей и тому подобного, но учитываете более реальные приложения. Вместо использования более сложных методов, таких как UpdateBooks (), использование таких действий, как «Добавить книгу» и «Удалить книгу», может ускорить и упростить операции. Одним из аргументов в пользу более крупного метода является то, что вы можете ожидать, что все операции будут зафиксированы (или нет) как одна операция, например, UpdateBooks вызывается как часть одного большого метода «SavePerson», отражающего изменения в человеке и все связанные с ним детали. В этих случаях по-прежнему рекомендуется выполнять атомарные действия, за исключением того, что вместо обновления состояния данных они могут обновлять состояние сервера (сеанса), ожидая вызова «Сохранить», чтобы сохранить изменения как одну операцию, или отменить изменения. Методы добавления/удаления все еще могут обеспечивать проверку подлинности, в конечном итоге настраивая объекты для загрузки, изменения и сохранения.

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

1. Спасибо за подробный ответ. Я думаю, что в том, как вы это объясняете, есть полный смысл. У меня вроде как была реализация, которая делала то же самое. И все же я сомневался, можно ли это сделать быстрее. Что касается вашего совета по атомным операциям. Я полностью за это, но в данном случае мне нужно быть осторожным в отношении смены личности и книг для чтения. Честно говоря, эта настройка является упрощением моей реальной проблемы. Эта проблема должна сохранить дочерние элементы вместе с основными элементами.