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)
笔记:
如果我使用包含而不是 SelectMany,一切正常,因为包含加载连接的表但是我想进行单独的查询以避免笛卡尔产生,因此通过线路发送回的结果集不是那么大。
如何正确加载我的数据以避免笛卡尔生成,同时仍然能够删除多对多集合?
我正在寻找一种方法来显式加载集合表的引用(例如,该实体没有 Poco 类或 DB 集)或者我正在寻找一种从 EntityFramework 中显式删除的方法(无需调用存储过程)因为这会绕过审计日志)
除了加载相关数据的 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 查询,第二个包含TTarget与TSourceKey. 与 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)