英文:
Delete child table records from database using EF Core 7
问题 {#heading}
我有一个解决方案,其中我使用了存储库模式和 UnitOfWork。
我有一个用于聚合根(ParentTable
)的存储库,它与ChildOne
、ChildTwo
表有关系。
ParentTable? parentTable = await _unitOfWork.ParentRepository.GetSingleAsync(ParentId);
if (parentTable == null)
return false;
List<ChildTwo> childrenToDelete = new()
{
new ChildTwo { ChildTwoId = 3, ParentId = parentTable.ParentId },
new ChildTwo { ChildTwoId = 4, ParentId = parentTable.ParentId }
};
parentTable.Name = "Parent Name Updated";
foreach (var T in childrenToDelete)
parentTable.ChildTwo.Remove(T);
await _unitOfWork.SaveChangesAsync();
在这段代码中,在调用 SaveChangesAsync()
后,它会更新 ParentTable
的行,但不会删除添加到导航 ICollection
属性的 ChildTwo
行。
我已经查看了网上提供的各种解决方案,但似乎没有任何有效的方法。在 EF Core 7 中,没有 Attach()
方法来标记删除。
英文:
I have a solution where I am using the repository pattern along with UnitOfWork.
I have a repository for aggregate root (ParentTable
) which has a relation with ChildOne
, ChildTwo
tables.
ParentTable? parentTable = await _unitOfWork.ParentRepository.GetSingleAsync(ParentId);
if (parentTable == null)
return false;
List\<ChildTwo\> childrenToDelete = new()
{
new ChildTwo { ChildTwoId = 3, ParentId = parentTable.ParentId },
new ChildTwo { ChildTwoId = 4, ParentId = parentTable.ParentId }
};
parentTable.Name = \"Parent Name Updated\";
foreach (var T in childrenToDelete)
parentTable.ChildTwo.Remove(T);
`await _unitOfWork.SaveChangesAsync();
`
In this code, after calling SaveChangesAsync()
, it is updating ParentTable
rows, but not deleting ChildTwo
rows added to navigation ICollection
property.
I've looked various solutions available online but nothing seems to be working, in EF Core 7 there is no Attach()
method to mark it deleted.
答案1 {#1}
得分: 1
另一个不使用通用仓储模式的原因...
检查或增强存储库方法的一个选项是检索与父实体相关联的实体的选项。例如:
async Task<T?> GetSingleAsync<T>(int id, params string[] includes)
{
var query = _context.Set<T>()
.Where(x => x.Id == id);
foreach(var include in includes)
query = query.Include(include);
return await query.SingleOrDefaultAsync();
}
或者可以使用表达式的 params
进行,但无论哪种方式都对如何处理引用深度有限制,比如支持 Include
和 ThenInclude
。有了上面这样的方法,您可以使用:
ParentTable? parentTable = await _unitOfWork.ParentRepository.GetSingleAsync(ParentId, "ChildTwo");
if (parentTable == null)
return false;
foreach(var childId in childIdsToRemove)
{
var child = parentTable.ChildTwo.FirstOrDefault(x => x.Id == childId);
if (child != null)
parentTable.ChildTwo.Remove(child);
}
_unitOfWork.SaveChanges();
当修改实体的相关子项时,您基本上希望要么急切加载集合,要么显式加载集合。作为一个快速的解决方法,您可以附加一个存根并以这种方式删除项目,但我不建议对子实体使用这种方法,因为您始终需要检查 DbContext 是否碰巧在附加之前跟踪了引用,否则在遇到该情况时会出现看似间歇性/情景性的异常。
这是我不建议在 EF 中使用通用或按实体的仓库模式的一个很好的例子。如果代码只是使用了一个注入的 DbContext
,或者一个公开了 IQueryable
的存储库模式,解决方案将是很简单的,只需添加 .Include(x => x.ChildTwos)
或者在 EF Core 中,甚至可以只加载要删除的项目:
var childrenToDelete = await _context.Entry(parent)
.Collection(x => x.ChildTwo)
.Query()
.Where(c => childIdsToDelete.Contains(c.Id))
.ToListAsync();
foreach(var child in childrenToDelete)
parent.ChildTwo.Remove(child);
这样只显式加载了在 ID 列表中找到的父实体的子实体,而不是像 Include()
一样急切加载所有子实体的笛卡尔积。
英文:
One more reason not to use Generic Repository patterns...
One option to check for, or enhance your repository method is the option to retrieve related entities with the parent. For instance:
Task<T?> async GetSingleAsync<T>(int id, params string[] includes)
{
var query = _context.Set<T>()
.WHere(x => x.Id == id);
foreach(var include in includes)
query = query.Include(include);
return await query.SingleOrDefaultAsync();
`}
`
Alternatively this could be done with a params
of Expressions, but either way has limitations on how to handle reference depth like supporting Include
and ThenInclude
for instance. With a method like the above available you could use:
ParentTable? parentTable = await _unitOfWork.ParentRepository.GetSingleAsync(ParentId, "ChildTwo");
if (parentTable == null)
return false;
foreach(var childId in childIdsToRemove)
{
var child = parentTable.ChildTwo.FirstOrDefault(x =\> x.Id == childId);
if (child != null)
parentTable.ChildTwo.Remove(child);
}
`_unitOfWork.SaveChanges();
`
When modifying an entity's related children, you essentially want to either eager load the collection or explicitly load the collection. As a quick hack you can Attach a stub and remove items that way but I do not recommend using that approach for child entities as you will always need to check if the DbContext happens to be tracking a reference for each item before attaching it otherwise you will get an seemingly intermittent/situational exception when it encounters that scenario.
This is a good example of why I don't recommend using a Generic or per-Entity Repository pattern with EF. If the code had just used an injected DbContext
, or a repository pattern that exposed IQueryable
, the solution would be as simple as adding the .Include(x => x.ChildTwos)
or with EF Core you could even Just load the items to delete using:
var childrenToDelete = await _context.Entry(parent)
.Collection(x => x.ChildTwo)
.Query()
.Where(c => childIdsToDelete.Contains(c.Id))
.ToListAsync();
`foreach(var child in childrenToDelete)
parent.ChildTwo.Remove(child);
`
That explicitly loads only the child entities from the parent that were found in the ID list rather than eager loading a Cartesian Product of all children like Include()
would do.