В двух своих классах я создал связь «многие ко многим»:
public class Author
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[DisplayName("^First Name")]
public required string FirstName { get; set; }
[DisplayName("Last Name")]
public required string LastName { get; set; }
public ICollection<Book> Books { get; set; } = [];
}
public class Book : Product
{
public required string ISBN { get; set; }
[Range(1, 1000)]
[Display(Name = "List Price")]
public required double ListPrice { get; set; }
[Range(1, 1000)]
[Display(Name = "Price for 1-49")]
public required double Price { get; set; }
[Range(1, 1000)]
[Display(Name = "Price for 50-99")]
public required double Price50 { get; set; }
[Range(1, 1000)]
[Display(Name = "Price for 100+")]
public required double Price100 { get; set; }
[ValidateNever]
public virtual required Category Category { get; set; }
[ValidateNever]
public virtual required ICollection<Author> Authors { get; set; } = [];
}
public class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public required string Title { get; set; }
public string Description { get; set; } = string.Empty;
[AllowNull]
public string? ImageUrl { get; set; } = "";
}
public class BookAuthor
{
public required int BookId { get; set; }
public required int AuthorId { get; set; }
}
А в DbContext
я настроил отношения так:
modelBuilder.Entity<Author>()
.HasMany(ba => ba.Books)
.WithMany(ba => ba.Authors)
.UsingEntity<BookAuthor>();
Моя проблема в том, что я хочу обновить авторов книг. Он не удаляет старые данные в таблице BookAuthor
, а только пытается вставить новые записи для обновленной книги. Я не хочу удалять записи о родстве и вставлять их заново.
Я использую этот код для обновления книг:
bookVM.book.Authors = new List<Author>();
foreach (int authorId in bookVM.AuthorsId)
{
var author = _UnitOfWork.Author.Get(cat => cat.Id == authorId);
bookVM.book.Authors.Add(author);
}
if (bookVM.book.Id == 0)
{
_UnitOfWork.Book.Add(bookVM.book);
}
else
{
_UnitOfWork.Book.Update(bookVM.book);
}
_UnitOfWork.Save();
Есть ли способ, которым EF Core может сделать это автоматически?
Глядя на предоставленный код, я вижу несколько проблем. Следующее выглядит как два красных флажка:
bookVM.book.Authors = new List<Author>();
Во-первых, когда дело касается сущностей EF, если вы собираетесь использовать модели представлений (рекомендуется), никогда не смешивайте модели представлений и сущности. (bookVM.Book) Виртуальная машина книги должна содержать столбцы данных, относящиеся к сущности книги, где сущность загружается из DbContext при обновлении или создается и добавляется, если она новая.
Во-вторых, свойства навигации по коллекциям в сущностях никогда не должны иметь доступа к общедоступным сеттерам. В сущности «Книга» свойство навигации «Авторы» должно выглядеть следующим образом:
public virtual ICollection<Author> Authors { get; protected set; } = [];
Вы никогда не захотите увидеть код вне сущности, делающий что-то вроде book.Authors = ....
. Это основной виновник дублирования данных или дополнительных ссылок, поскольку EF рассчитывает на то, что авторы по доверенности будут отслеживать изменения при добавлении и удалении ссылок. Использование сеттера нарушает это отслеживание.
Далее, использование вами оболочки единицы работы может запутать ситуацию, но вы хотите быть уверены, что имеете дело с реальными, отслеживаемыми ссылками, насколько это возможно, и будьте очень осторожны с отдельными данными. Обычно я советую избегать методов стиля «Upsert», вместо этого использовать отдельные явные методы Insert и Update, чтобы вы могли лучше обрабатывать ситуации, когда пользователь ожидает вставить книгу (которая может уже существовать) или обновить книгу (которая не существует). Однако в сценарии Upsert рассмотрите следующий скорректированный метод (с использованием DbContext
):
// First we can fetch our Authors to use whether inserting or updating.
var authors = _context.Authors
.Where(x => bookVM.AuthorIds.Contains(x.Id))
.ToList();
// Consider validating whether authors.Count == bookVM.AuthorIds.Count in
// case one or more Authors were not found...
if (bookVM.Id == 0)
{ // Insert
Book book = new Book(bookVM.Id, bookVM.Title, authors, /* + Required (not null) properties */)
{
/* + optional properties... */
};
_context.Books.Add(book);
}
else
{ // Update
var book = _context.Books
.Include(x => x.Authors)
.Single(x => x.Id == bookVM.Id);
var existingAuthorIds = book.Authors.Select(x => x.Id).ToList();
var authorIdsToAdd = bookVM.AuthorIds.Except(existingAuthorIds);
var authorIdsToRemove = existingAuthorIds.Except(bookVM.AuthorIds);
if (authorIdsToRemove.Any())
{
var authorsToRemove = book.Authors
.Where(x => authorIdsToRemove.Contains(x.Id))
.ToList();
foreach(var author in authorsToRemove)
book.Authors.Remove(author);
}
if (authorIdsToAdd.Any())
{
var authorsToAdd = authors
.Where(x => authorIdsToAdd.Contains(x.Id))
.ToList();
foreach(var author in authorsToAdd)
book.Authors.Add(author);
}
}
_context.SaveChanges();
При обновлении связанных объектов в EF извлеките объект из базы данных и сразу же загрузите все связанные данные, которые вы, возможно, обновляете. Вам нужно быть немного более осознанным, чтобы определить, каких авторов следует добавить, а кого удалить, а не пытаться заменить коллекцию, чтобы она отражала текущее состояние. Когда вы замените коллекцию, EF будет ожидать, что вы просто добавляете все новые ассоциации, и не будет знать, что какие-либо записи ассоциаций необходимо удалить.
При выполнении вставок вы можете инициализировать коллекции, в идеале с помощью конструктора, поскольку вы не хотите предоставлять общедоступный установщик для ссылки на коллекцию. Я рекомендую иметь общедоступный конструктор, который принимает все необходимые свойства и ссылки, а затем оставляет все необязательные, допускающие нулевые значения, заданными через их общедоступные установщики. Это помогает гарантировать, что сущность всегда сохраняется в допустимом состоянии до ее сохранения. При использовании конструктора аргументов для сущности просто включите конструктор по умолчанию protected
, который EF будет использовать при создании сущностей, считываемых из базы данных.
Никто не знает, что происходит внутри UnitOfWork.