Основная проблема при работе с объектами разрыва связи заключается в обнаружении и применении добавленных и удаленных ссылок.И EF Core (на момент написания статьи) мало что дает, если не помогает в этом.
Ответ по ссылке в порядке (пользовательский метод Except
слишком тяжел для того, что он делает IMO), но он имеет некоторые ловушки - существующие ссылки должны быть получены заранее, используя eager / явныйзагрузка (хотя в случае EF Core 2.1 это отложенная загрузка, которая может не вызывать проблем), и в новых ссылках должны быть заполнены только свойства FK - если они содержат свойства ссылочной навигации, EF Core попытается создать новые связанные объекты при вызове Add
/AddRange
.
Некоторое время назад я ответил на похожий, но немного другой вопрос - Общий метод обновления соединений EFCore .Вот более обобщенная и оптимизированная версия пользовательского универсального метода расширения из ответа:
public static class EFCoreExtensions
{
public static void UpdateLinks<TLink, TFromId, TToId>(this DbSet<TLink> dbSet,
Expression<Func<TLink, TFromId>> fromIdProperty, TFromId fromId,
Expression<Func<TLink, TToId>> toIdProperty, IEnumerable<TToId> toIds)
where TLink : class, new()
{
// link => link.FromId == fromId
Expression<Func<TFromId>> fromIdVar = () => fromId;
var filter = Expression.Lambda<Func<TLink, bool>>(
Expression.Equal(fromIdProperty.Body, fromIdVar.Body),
fromIdProperty.Parameters);
var existingLinks = dbSet.AsTracking().Where(filter);
var toIdSet = new HashSet<TToId>(toIds);
if (toIdSet.Count == 0)
{
//The new set is empty - delete all existing links
dbSet.RemoveRange(existingLinks);
return;
}
// Delete the existing links which do not exist in the new set
var toIdSelector = toIdProperty.Compile();
foreach (var existingLink in existingLinks)
{
if (!toIdSet.Remove(toIdSelector(existingLink)))
dbSet.Remove(existingLink);
}
// Create new links for the remaining items in the new set
if (toIdSet.Count == 0) return;
// toId => new TLink { FromId = fromId, ToId = toId }
var toIdParam = Expression.Parameter(typeof(TToId), "toId");
var createLink = Expression.Lambda<Func<TToId, TLink>>(
Expression.MemberInit(
Expression.New(typeof(TLink)),
Expression.Bind(((MemberExpression)fromIdProperty.Body).Member, fromIdVar.Body),
Expression.Bind(((MemberExpression)toIdProperty.Body).Member, toIdParam)),
toIdParam);
dbSet.AddRange(toIdSet.Select(createLink.Compile()));
}
}
Он использует один запрос к базе данных для извлечения исходящих ссылок из базы данных.Издержки - это несколько динамически построенных выражений и скомпилированных делегатов (чтобы максимально упростить вызывающий код) и один временный HashSet
для быстрого поиска.Влияние на производительность построения выражения / делегата должно быть незначительным и при необходимости может быть кэшировано.
Идея состоит в том, чтобы передать только один существующий ключ для одной из связанных сущностей и список выходных ключей для другой связанной сущности.Поэтому, в зависимости от того, какую из ссылок связанных сущностей вы обновляете, она будет вызываться по-разному.
В вашем примере, если вы получаете IEnumerable<PostCategory> postCategories
, процесс будет выглядеть примерно так:
var post = await dbContext.Posts
.SingleOrDefaultAsync(someId);
dbContext.Set<PostCategory>().UpdateLinks(pc =>
pc.PostId, post.Id, pc => pc.CategoryId, postCategories.Select(pc => pc.CategoryId));
await dbContext.SaveChangesAsync();
Обратите внимание, что этот метод позволяет изменить требование и принять IEnumerable<int> postCategoryIds
:
dbContext.Set<PostCategory>().UpdateLinks(pc =>
pc.PostId, post.Id, pc => pc.CategoryId, postCategoryIds);
или IEnumerable<Category> postCategories
:
dbContext.Set<PostCategory>().UpdateLinks(pc =>
pc.PostId, post.Id, pc => pc.CategoryId, postCategories.Select(c => c.Id));
или аналогичные DTO / ViewModels.
Сообщения категории могут обновляться аналогичным образом, при этом соответствующие селекторы меняются местами.
Обновление: В случае, если вы получаете (потенциально) измененный Post post
экземпляр сущности, вся процедура обновления может быть такой:
// Find (load from the database) the existing post
var existingPost = await dbContext.Posts
.SingleOrDefaultAsync(p => p.Id == post.Id);
if (existingPost == null)
{
// Handle the invalid call
return;
}
// Apply primitive property modifications
dbContext.Entry(existingPost).CurrentValues.SetValues(post);
// Apply many-to-many link modifications
dbContext.Set<PostCategory>().UpdateLinks(pc => pc.PostId, post.Id,
pc => pc.CategoryId, post.PostCategories.Select(pc => pc.CategoryId));
// Apply all changes to the db
await dbContext.SaveChangesAsync();
Обратите внимание, чтоEF Core использует отдельный запрос к базе данных для быстрой загрузки связанных сборов.Поскольку вспомогательный метод делает то же самое, нет необходимости Include
связывать связанные данные при извлечении основного объекта из базы данных.