问题
除了加载相关数据的 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));