级联软删除而不加载Ef-Core中的整个关系



我正在对相关实体进行级联软删除。到目前为止,我有以下解决方案,但它要求我使用Include、TheInInclude加载相关数据。。。为了工作。有没有更好的方法来实现同样的目标?问题是,如果我忘记加载相关数据,相应的Delete方法将引发null异常。

public class Entity1
{
public string Name { get; set; }
public IList<Entity2> Entity2s { get; set; }

public void Delete()
{
if (IsDeleted == null)
{
Name = $"{Guid.NewGuid()}${Name}";
IsDeleted = DateTime.Now;
foreach (var entity2 in Entity2s) // this raises null reference exception if Entity2s is not loaded
{
entity2.Delete();
}
}
}
public class Entity2
{
public string Name { get; set; }
public IList<Entity3> Entity3s{ get; set; }

public void Delete()
{
if (IsDeleted == null)
{
Name = $"{Guid.NewGuid()}${Name}";
IsDeleted = DateTime.Now;
foreach (var entity3 in Entity3s) // this raises null reference exception if Entity3s is not loaded
{
entity3.Delete();
}
}
}

然后在删除时我有

var ent = await _context.Entity1.Include(x => x.Entity2s)
.ThenInclude(x => x.Entity3s)
.FirstOrDefaultAsync(x => x.Id == id);
ent.Delete();
await _context.SaveChangesAsync(cancellationToken);

这个例子只在3个实体之间,但在实际应用中有很多关系。所以对于每一个关系,我都要做Include,TheInInclude。。。请帮忙。如果你也能给我一个更好的解决方案,我真的很感激。我已经在谷歌上搜索过了,但到目前为止,我找不到合适的解决方案。

由于您正在将业务逻辑应用于删除过程,例如记录删除时间和重命名已删除的项目,因此这几乎是删除它们的机制。将Delete作为域操作放在实体中,我建议启用延迟加载,因为这将确保它能够可靠地运行,正如你所期望的那样,它只有在实体处于DbContext的范围内时才会被调用。当然,建议进行紧急加载,但实体可以合理地确保,如果忘记了某些内容,删除操作将成功。(有"次优"性能错误比操作取消错误要好(

如果您想完全禁用延迟加载,那么我会考虑将Delete方法从实体中移到存储库或服务方法中,以确保加载所提供的实体和相关实体,然后继续对整个图执行删除操作。实体仍然可以使用内部Delete()方法来标准化日期设置,并在必要时进行重命名,只是不必担心图形,这将是存储库/服务的责任。这样,您的控制器或这样的代码可能";参见";实体,但不能调用Delete,他们必须使用服务删除支持的任何级别的实体,并且服务确保更新相关的实体图。

我过去使用过一种更通用的方法,在DbContext.cs类中重写SaveChanges()SaveChangesAsync()EF方法,如下所示。。。

public override int SaveChanges()
{
UpdateDateTrackingColumns();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
UpdateDateTrackingColumns();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void UpdateDateTrackingColumns()
{
foreach (var entry in ChangeTracker.Entries())
{
if (!entry.CurrentValues.EntityType.ToString().Contains("Identity"))
{
switch (entry.Metadata.Name)
{
// Entities where there is no DateCreated, DateUpdated, or DateDeleted columns in the entity are processed this way...
// For example, EF Core generated many-to-many intersect tables will not have these columns.
case "IgnoreTheseEntities": // Exclude entities by listing them as 'case "EntityName":' lines.
break;
// All entities with DateCreated, DateUpdated, and DateDeleted columns are processed in this way...
default:
var saveUtcDate = DateTime.UtcNow;
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["DateCreated"] = saveUtcDate;
entry.CurrentValues["DateUpdated"] = saveUtcDate;
entry.CurrentValues["DateDeleted"] = DateTime.MinValue;
break;
case EntityState.Modified:
entry.CurrentValues["DateUpdated"] = saveUtcDate;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["DateDeleted"] = saveUtcDate;
break;
}
break;
}
}
}
}

可能值得注意的是,在上面的例子中,我也使用了Microsoft Identity Framework。。。默认情况下,这些表没有日期列。所以我选择忽略这些表,因此使用了if (!entry.CurrentValues.EntityType.ToString().Contains("Identity"))子句。如果不使用Identity Framework,则可以删除此if子句。

[Edit]您可能还需要担心DeleteBehaviour。您可以在EntityTypeConfiguration类中使用类似以下内容来防止级联删除。。。

builder
.HasMany(p => p.Teams)
.WithOne(d => d.Location)
.OnDelete(DeleteBehavior.NoAction);

您可能还想向每个实体添加查询筛选器。这将防止EF查询返回软删除的行。我使用EntityTypeConfiguration类来处理这些。这方面的一个例子看起来像。。。

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Common.Models;
using System;
namespace MyQT.DAL.EntityTypeConfiguration
{
public class AddressTypeEntityTypeConfiguration : IEntityTypeConfiguration<AddressType>
{
public void Configure(EntityTypeBuilder<AddressType> builder)
{
builder
.Property(b => b.AddressTypeId)
.HasColumnType("uniqueidentifier")
.ValueGeneratedOnAdd()
.IsRequired()
.HasComment("Unique identifier for a given AddressType.");
builder
.Property(b => b.Name)
.HasMaxLength(200)
.IsRequired()
.HasComment("The AddressType name.");
builder
.Property(b => b.DateCreated)
.IsRequired()
.HasComment("The UTC date/time that the row was inserted into the database.");
builder
.Property(b => b.DateUpdated)
.IsRequired()
.HasComment("The UTC date/time that any given row was last updated. Upon record creation this will be set to DateCreated value.");
builder
.Property(b => b.DateDeleted)
.HasComment("The UTC date/time that any given row was "deleted".  Data is NOT actually deleted.  It will instead be "archived" by populating the DateDeleted for any given row.");
builder
.Property(b => b.TimeStamp)
.IsRowVersion()  // Alternatively use ".IsConcurrencyToken()"... similar but different
.HasComment("Concurrency token.");
// Soft delete automated query filter
builder
.HasQueryFilter(m => EF.Property<DateTime>(m, "DateDeleted") == DateTime.MinValue);
// Possible better way...
//.HasQueryFilter(p => p.DateDeleted.Value == DateTime.MinValue);
}
}
}

最新更新