使用selectmany时删除实体框架参考约束错误

joh*_*y 5 8 c# orm entity-framework

编辑:请仔细阅读,我有并发布了没有笛卡尔生成的加载数据。如果正确加载了引用,则删除数据可以正常工作。

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

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

当我尝试通过使用 select many 加载 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();
Run Code Online (Sandbox Code Playgroud)

关联的 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();
Run Code Online (Sandbox Code Playgroud)

没有任何东西可以删除,所以删除时我得到了我的参考约束。

这是我的数据库结构

Organizations: * => * aspnet_Roles (Many To Many connected by intermediate table **OwnerRoles**)
aspnet_Roles: 1 => * permissions (aspnet_Roles has many permissions)
Run Code Online (Sandbox Code Playgroud)

笔记:

  1. 我不能使用 CascadeOnDelete
  2. 我正在使用 Edmx 和 EntityFramework v6.2
  3. 我不想多次使用保存更改。

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

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

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

Iva*_*oev 5

问题

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

rolesQuery.SelectMany(x => x.Permissions).Load();
Run Code Online (Sandbox Code Playgroud)

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

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

rolesQuery.SelectMany(x => x.Organizations).Load();
Run Code Online (Sandbox Code Playgroud)

context.Organizations.Load();
Run Code Online (Sandbox Code Playgroud)

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

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

解决方案

具有隐式链接实体关系的多对多被实现为独立的关联,并且纯粹由上下文更改跟踪器维护。DbContextAPI 不提供维护此类关系状态的方法,但如EF6 文档的创建和修改关系部分所述,ObjectContextAPI 使用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);
}
Run Code Online (Sandbox Code Playgroud)

示例用法:

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

完整代码:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:上面执行 2 个 db 查询,第二个包含TTargetTSourceKey. 与 include 的区别在于它TSource从查询中删除了列。

可以只检索不需要重复的数据,并且需要执行 3 个 db 查询:


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);
    }
}

Run Code Online (Sandbox Code Playgroud)

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

context.LoadLinks(rolesQuery, role => role.Organizations, role => role.Id, organization => organization.Id));
Run Code Online (Sandbox Code Playgroud)