使用断开链接实体时的主要挑战是检测和应用添加和删除的链接。而ef core(在写作的时候)几乎没有帮助。
链接的答案是正常的(自定义
Except
方法对于IMO来说太重了),但它有一些陷阱-必须使用热切/显式加载提前检索现有链接(尽管EFcore2.1延迟加载可能不是问题),并且新链接应该只填充FK属性-如果它们包含Reference导航属性,ef core将在调用时尝试创建新的链接实体
Add
/
AddRange
.
不久前,我回答了类似但略有不同的问题-
Generic method for updating EFCore joins
。下面是来自答案的自定义通用扩展方法的更通用和优化版本:
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/视图模型。
分类文章可以以类似的方式更新,并交换相应的选择器。
更新:
以防您收到(可能)修改的
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使用单独的数据库查询来进行与预加载相关的收集。因为helper方法做的是相同的,所以不需要
Include
从数据库中检索主实体时链接相关数据。