【问题标题】:Entity Framework Reference constraint error on delete when using selectmany使用selectman时删除实体框架引用约束错误
【发布时间】:2020-05-20 20:39:16
【问题描述】:

编辑:请仔细阅读,我有并发出 Loading 没有笛卡尔生产的数据。如果引用正确加载,删除数据工作正常。

我正在尝试删除一大组实体,但是由于OwnerRoles 的多对多关联,每当我尝试删除它们时都会收到错误消息:

SqlException:DELETE 语句与 REFERENCE 约束“FK_OwnerRoles_aspnet_Roles_RoleId”冲突

当我尝试通过使用多选加载 aspnet_roles 来避免笛卡尔生产时,

var rolesQuery = context.Organizations
       .Where(x => x.OrganizationId == organizationId)
       .SelectMany(x => x.aspnet_Roles);

var roles = rolesQuery.ToArray();
rolesQuery.SelectMany(x => x.Permissions).Load();
rolesQuery.SelectMany(x => x.Organizations).Load();

未加载关联的 OwnerRoles,因此当我尝试删除所有引用时:

roles.ForEach(r => r.Organizations.ToArray().ForEach(o => r.Organizations.Remove(o)));

context.Permissions.RemoveRange(roles.SelectMany(x => x.Permissions));
context.aspnet_Roles.RemoveRange(roles);

context.SaveChanges();

没有加载任何要删除的内容,所以删除时我得到了参考约束。

这是我的数据库结构

Organizations: * => * aspnet_Roles (Many To Many connected by intermediate table **OwnerRoles**)
aspnet_Roles: 1 => * permissions (aspnet_Roles has many permissions)

注意事项:

  1. 我无法使用 CascadeOnDelete
  2. 我正在将 Edmx 与 EntityFramework v6.2 一起使用
  3. 我不希望多次使用保存更改。

如果我使用包含而不是 SelectMany,一切正常,因为包含加载连接的表但是我想进行单独的查询以避免笛卡尔生产,因此通过网络发送回的结果集不是那么大。

如何正确加载我的数据以避免笛卡尔产生,同时仍然能够删除多对多的集合?

我正在寻找一种方法来显式加载集合表的引用(例如,没有为该实体设置 Poco 类或 DB)或者我正在寻找一种从 EntityFramework 中显式删除的方法(无需调用一个存储过程,因为这会绕过审计日志)

【问题讨论】:

  • 你确定你删除它的顺序是正确的,如果你这样做了外键不会被破坏。您可能需要提交您在第一个表中删除的内容并重新查询第二个表以避免错误。
  • @bestinamir,是的,当我使用包括时一切正常,当我使用 selectmany 时它不会尝试删除引用。
  • 您到底要删除什么?角色和权限?
  • 据我所知,您必须执行两种不同的操作。第一次删除所有相关的“权限”,第二次删除所有相关的“aspnet_Roles”。从技术上讲,两者都应该在一个单独的“交易”中。
  • 另一个选项是(如果可能的话),创建一个存储过程,它根据您的首选顺序执行删除。 SP 的输入参数是“organizationId”。

标签: c# entity-framework orm


【解决方案1】:

问题

除了加载相关数据的 3 种标准方式(急切、显式和惰性)之外,EF6 还通过称为“导航属性修复”的过程支持另一种方式,像您这样的查询使用这种方式

rolesQuery.SelectMany(x => x.Permissions).Load();

请注意,Load 这个名称有点误导。 Load 是 EF 自定义扩展方法,它只执行查询并迭代结果集,类似于 ToList,但不创建列表。

前 3 种方法适用于任何类型的关系。但是最后一个不适用于具有隐式链接实体关系的多对多,因为无法在 LINQ 查询中指定“链接”实体,因此以下

rolesQuery.SelectMany(x => x.Organizations).Load();

和

context.Organizations.Load();

是等效的 - 都返回(和加载)Organization 实体。

标准解决方案是使用 3 种标准方式中的一些。但是急切加载会产生巨大的联合数据集,而显式加载和延迟加载会产生 N + 1 个数据库查询。

解决方案

具有隐式链接实体关系的多对多被实现为独立关联,并且纯粹由上下文更改跟踪器维护。 DbContext API 不提供维护此类关系状态的方法,但正如 EF6 文档的 Creating and modifying relationships 部分所述,ObjectContext API 使用 ObjectStateManager 的 ChangeRelationshipState 方法。

以下是一个自定义的通用扩展方法,它利用上述方法解决了这个问题。最重要的部分是

// Query to retrieve IEnumerable<Tuple<TSourceKey, TTarget>> from database
// and group it by TSourceKey in memory
var groupedLinksQuery = sourceDbQuery
    .SelectLinks(keySelector, collectionSelector)
    .AsEnumerable()
    .GroupBy(e => e.Item1, e => e.Item2);
// Execute the query and perform the fix-up
foreach (var group in groupedLinksQuery)
{
    var source = sourceDbSet.Find(group.Key);
    foreach (var target in group)
        stateManager.ChangeRelationshipState(source, target, collectionPropertyName, EntityState.Unchanged);
}

示例用法:

var roles = rolesQuery.ToArray();
rolesQuery.SelectMany(role => role.Permissions).Load();
context.LoadLinks(rolesQuery, role => role.Id, role => role.Organizations); // <--

完整代码:

using System;
using System.Collections.Generic;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;

namespace System.Data.Entity
{
    public static partial class EF6Extensions
    {
        public static void LoadLinks<TSource, TSourceKey, TTarget>(this DbContext dbContext, IQueryable<TSource> sourceDbQuery, Expression<Func<TSource, TSourceKey>> keySelector, Expression<Func<TSource, ICollection<TTarget>>> collectionSelector)
            where TSource : class
            where TTarget : class
        {
            // Disable AutoDetectChanges for better performance
            bool autoDetectChanges = dbContext.Configuration.AutoDetectChangesEnabled;
            dbContext.Configuration.AutoDetectChangesEnabled = false;
            try
            {
                var sourceDbSet = dbContext.Set<TSource>();
                var collectionPropertyName = ((MemberExpression)collectionSelector.Body).Member.Name;
                var stateManager = dbContext.GetObjectStateManager();
                // Query to retrieve IEnumerable<Tuple<TSourceKey, TTarget>> from database
                // and group it by TSourceKey in memory
                var groupedLinksQuery = sourceDbQuery
                    .SelectLinks(keySelector, collectionSelector)
                    .AsEnumerable()
                    .GroupBy(e => e.Item1, e => e.Item2);
                // Execute the query and perform the fix-up
                foreach (var group in groupedLinksQuery)
                {
                    var source = sourceDbSet.Find(group.Key);
                    foreach (var target in group)
                        stateManager.ChangeRelationshipState(source, target, collectionPropertyName, EntityState.Unchanged);
                }
            }
            finally { dbContext.Configuration.AutoDetectChangesEnabled = autoDetectChanges; }
        }

        static IQueryable<Tuple<TSourceKey, TTarget>> SelectLinks<TSource, TSourceKey, TTarget>(this IQueryable<TSource> sourceQuery, Expression<Func<TSource, TSourceKey>> keySelector, Expression<Func<TSource, ICollection<TTarget>>> collectionSelector)
        {
            // sourceQuery.SelectMany(source => source.Collection, (source, target) => Tuple(source.Key, target))
            var source = keySelector.Parameters[0];
            var target = Expression.Parameter(typeof(TTarget), "target");
            var resultType = typeof(Tuple<TSourceKey, TTarget>);
            var constructor = resultType.GetConstructor(new[] { typeof(TSourceKey), typeof(TTarget) });
            var args = new[] { keySelector.Body, target };
            var members = new[] { resultType.GetProperty("Item1"), resultType.GetProperty("Item2") };
            var body = Expression.New(constructor, args, members);
            var selector = Expression.Lambda<Func<TSource, TTarget, Tuple<TSourceKey, TTarget>>>(
                body, source, target);
            return sourceQuery.SelectMany(collectionSelector.AsEnumerable(), selector);
        }

        static Expression<Func<TSource, IEnumerable<TTarget>>> AsEnumerable<TSource, TTarget>(this Expression<Func<TSource, ICollection<TTarget>>> collectionSelector)
            => Expression.Lambda<Func<TSource, IEnumerable<TTarget>>>(collectionSelector.Body, collectionSelector.Parameters);

        public static ObjectContext GetObjectContext(this IObjectContextAdapter source) => source.ObjectContext;

        public static ObjectStateManager GetObjectStateManager(this IObjectContextAdapter source) => source.ObjectContext.ObjectStateManager;
    }
}

更新:上面执行了 2 个 db 查询,第二个包含重复的 TTarget 记录与 TSourceKey 配对。与 include 的区别在于它从查询中消除了 TSource 列。

只检索需要的数据而不重复是可能的,并且需要执行 3 个数据库查询:


public static partial class EF6Extensions
{
    public static void LoadLinks<TSource, TTarget, TSourceKey, TTargetKey>(this DbContext dbContext, IQueryable<TSource> sourceQuery, Expression<Func<TSource, ICollection<TTarget>>> collectionSelector, Expression<Func<TSource, TSourceKey>> sourceKeySelector, Expression<Func<TTarget, TTargetKey>> targetKeySelector)
        where TSource : class
        where TTarget : class
    {
        // Disable AutoDetectChanges for better performance
        bool autoDetectChanges = dbContext.Configuration.AutoDetectChangesEnabled;
        dbContext.Configuration.AutoDetectChangesEnabled = false;
        try
        {
            var sourceDbSet = dbContext.Set<TSource>();
            var targetDbSet = dbContext.Set<TTarget>();
            // Query to retrieve link keys from database
            var linksDbQuery = sourceQuery.SelectLinks(collectionSelector, sourceKeySelector, targetKeySelector);
            // Query to retrieve distinct target keys from database
            var targetKeysDbQuery = linksDbQuery.Select(e => e.Item2).Distinct();
            // Query to retrieve unique target entities
            var targetDbQuery = targetDbSet
                .Join(targetKeysDbQuery, targetKeySelector, key => key, (target, key) => target);
            // Execute the target entities query and build map by Id in memory
            var targetMap = targetDbQuery
                .ToDictionary(targetKeySelector.Compile());
            // Execute the links query and perform the fix-up 
            var stateManager = dbContext.GetObjectStateManager();
            var collectionPropertyName = ((MemberExpression)collectionSelector.Body).Member.Name;
            var sourceMap = new Dictionary<TSourceKey, TSource>();
            foreach (var link in linksDbQuery)
            {
                if (!sourceMap.TryGetValue(link.Item1, out var source))
                    sourceMap.Add(link.Item1, source = sourceDbSet.Find(link.Item1));
                var target = targetMap[link.Item2];
                stateManager.ChangeRelationshipState(source, target, collectionPropertyName, EntityState.Unchanged);
            }
        }
        finally { dbContext.Configuration.AutoDetectChangesEnabled = autoDetectChanges; }
    }

    static IQueryable<Tuple<TSourceKey, TTargetKey>> SelectLinks<TSource, TTarget, TSourceKey, TTargetKey>(this IQueryable<TSource> sourceQuery, Expression<Func<TSource, ICollection<TTarget>>> collectionSelector, Expression<Func<TSource, TSourceKey>> sourceKeySelector, Expression<Func<TTarget, TTargetKey>> targetKeySelector)
    {
        // sourceQuery.SelectMany(source => source.Collection, (source, target) => Tuple(source.Key, target.Key))
        var source = sourceKeySelector.Parameters[0];
        var target = targetKeySelector.Parameters[0];
        var resultType = typeof(Tuple<TSourceKey, TTargetKey>);
        var constructor = resultType.GetConstructor(new[] { typeof(TSourceKey), typeof(TTargetKey) });
        var args = new[] { sourceKeySelector.Body, targetKeySelector.Body };
        var members = new[] { resultType.GetProperty("Item1"), resultType.GetProperty("Item2") };
        var body = Expression.New(constructor, args, members);
        var selector = Expression.Lambda<Func<TSource, TTarget, Tuple<TSourceKey, TTargetKey>>>(
            body, source, target);
        return sourceQuery.SelectMany(collectionSelector.AsEnumerable(), selector);
    }
}

并且用法需要为两个键传递一个选择器,例如

context.LoadLinks(rolesQuery, role => role.Organizations, role => role.Id, organization => organization.Id));

【讨论】:

  • 谢谢,完美,这看起来不错,只需给我一天左右的时间来验证!
  • 扩展有问题。它完全充当包含。例如 context.LoadLinks(rolesQuery, r =&gt; r.RoleId, r =&gt; r.Organizations) 如果在没有加载角色或组织的情况下运行它。它同时加载组织和角色。这意味着它将创建笛卡尔生产
  • 不完全是。如果您有 A、B 和 AB,a.Include(a.Bs) 将使用 A 中的所有字段和 B 中的所有字段创建查询。当然会有重复的 As 和重复的 Bs。上述方法消除了 A 字段。仍然会有与不同 AId 相关联的重复 B 记录,所以如果这就是“笛卡尔生产”的意思,是的,但是查询列会少得多。加载没有任何重复是可能的,但需要执行 3 db 查询(与此方法中的 2 相对)。
  • 感谢您的澄清
  • 我知道这可能不是联系您的最佳方式,但您过去曾多次帮助我处理与 EF 相关的事情,我创建了一个关于 EF 的bountied question如果你有时间并且有兴趣
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多