【问题标题】:Handling dependent entities when deleting the principal with Entity Framework 5使用 Entity Framework 5 删除主体时处理依赖实体
【发布时间】:2013-06-04 14:52:37
【问题描述】:

以下是使用 EF5 代码优先方法最简化的情况:

public abstract class EntityBase<PK>
{
    public PK ID { get; set; }
}
public class Country : EntityBase<string>
{
    public string Name { get; set; }
}
public class Address : EntityBase<int>
{
    [Required]
    public string CountryID { get; set; }
    public Country Country { get; set; }
    // ... other address properties ...
}

AddressCountry 之间的一对多关系设置为没有像这样的级联删除:

modelBuilder.Entity<Address>()
    .HasRequired(a => a.Country)
    .WithMany()
    .HasForeignKey(a => a.CountryID)
    .WillCascadeOnDelete(false);

最后,我有一个带有 CRUD 方法的通用基础存储库类,它在底层 DbContext 上调用 SaveChanges 以原子地提交数据更改。例如:

public class EFRepository<T, PK> : IRepository<T, PK> where T : EntityBase<PK>
{
    //
    // ... other methods ... 
    //
    public virtual void Delete(T instance)
    {
        // ... trigger validations, write to log, etc...
        _dbContext.Set<T>().Remove(instance);
        try
        {
            _dbContext.SaveChanges();
        }
        catch(Exception ex)
        {
            // ... handle the error ...
        }
    }
}

第 1 部分:

场景:

var countryRepo = new EFRepository<Country>();
var country = countryRepo.Save(new Country() { ID="??", Name="Test Country" });

var addressRepo = new EFRepository<Address>();
var address = addressRepo.Save(new Address() { Country=country });

countryRepo.Delete(country);

由于存在依赖 Address,这应该会失败。但是,之后地址在CountryID 中以null 结束,这是无效的,因为Address.CountryID 是必需的,因此后续的SaveChanges 调用会抛出验证异常,除非地址被分离。

我希望当一个对象被删除时,EF5 将足够聪明地首先检查任何类似上述的级联删除约束,如果找不到任何约束,然后继续删除数据。但情况似乎恰恰相反。

这是正常行为还是我做错了什么?

第 2 部分:

SaveChanges 调用失败后,一些Addresses 现在在我的 DbContext 中处于无效状态,需要恢复到它们的原始值。当然,我总是可以通过创建专门的存储库类并覆盖 Delete 来为每种实体类型(CountryStateOrder 等)明确地这样做,但它闻起来很香。在SaveChanges 调用失败后,我更愿意编写一些通用代码来优雅地恢复相关实体。

它需要询问 DbContext 以获取其中一个实体(例如 Country)是主体的所有关系,无论其类是否定义了依赖实体的导航属性。

例如Country 没有 Addresses 属性,所以我需要以某种方式在 DbContext 中找到 CountryAddress 之间的一对多关系的定义,并使用它将所有相关的 Addresses 恢复为其原始值.

这可能吗?

【问题讨论】:

    标签: entity-framework entity-framework-5


    【解决方案1】:

    第 2 部分中回答我自己的问题:

    这是我在删除多对一关系的主体端的实体时检查相关依赖项的方法,并且依赖项不作为导航集合在主体中公开(例如,Address 类有一个 @ 987654322@ 属性,但 Country 类没有 Addresses 集合。

    DbContext

    将以下方法添加到上下文类中:

    /// <summary>
    /// Returns an array of entities tracked by the 
    /// context that satisfy the filter criteria.
    /// </summary>
    public DbEntityEntry[] GetTrackedEntities<T>(
        Expression<Func<DbEntityEntry<T>, bool>> filterCriteria) 
        where T : class
    {
        var result = new List<DbEntityEntry>();
        var doesItMatch = filterCriteria.Compile();
    
        foreach (var entry in this.ChangeTracker.Entries<T>())
        {
            if (doesItMatch(entry))
                result.Add(entry);
        }
        return result.ToArray();
    }
    

    存储库

    为每个具有一些依赖项的类创建一个存储库,覆盖Delete 方法并使用新的GetTrackedEntities&lt;T&gt; 方法获取所有相关的依赖项以及:

    • 如果它们在代码中可级联删除,则显式删除它们
    • 如果它们在数据库本身中是可级联删除的,则将它们从上下文中分离出来
    • 如果它们不可级联删除,则抛出异常。

    后一种情况的例子:

    public class EFCountryRepository : 
        EFReadWriteRepository<Country, string>, 
        ICountryRepository 
    {
        public override void Delete(Country instance)
        {
            // Allow the Country to be deleted only if there are no dependent entities
            // currently in the context that are NOT cascade-deletable.
            if (
                // are there any Regions in the context that belong to this Country?
                _dbContext.GetTrackedEntities<Region>(e => 
                    e.Entity.CountryID == instance.ID || 
                    e.Entity.Country == instance).Length > 0  
                ||
                // are there any Addresses in the context that belong to this Country?
                _dbContext.GetTrackedEntities<Address>(e =>
                    e.Entity.CountryID == instance.ID || 
                    e.Entity.Country == instance).Length > 0
            )
                throw new Exception(String.Format(
                    "Country '{0}' is in use and cannot be deleted.", instance.ID));
    
            base.Delete(instance);
        }
        // ... other methods ...
    }
    

    级联删除将由数据库本身完成的示例,因此我们需要做的就是将依赖项从上下文中分离出来:

    public class EFOrderRepository : 
        EFReadWriteRepository<Order, string>, 
        IOrderRepository 
    {
        public override void Delete(Order instance)
        {
            foreach (var orderItem in _dbContext.GetTrackedEntities<OrderItem>(e => 
                    e.Entity.OrderID == instance.ID || 
                    e.Entity.Order == instance))
            {
                _dbContext.Entry(orderItem).State = System.Data.EntityState.Detached;
            }
            base.Delete(instance);
        }
        // ... other methods ...
    }
    

    希望有人会发现此解决方案有帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-01-09
      • 1970-01-01
      • 2021-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多