实体框架如何与递归层次结构一起工作

我有一个 ItemItem有一个 Category

CategoryIDNameParentChildrenParentChildren也是 Category

当我为特定的 Item执行 LINQtoEntities 查询时,它不返回相关的 Category,除非我使用 Include("Category")方法。但它没有带来完整的类别,其父母和子女。我可以做 Include("Category.Parent"),但是这个对象类似于树,我有一个递归的层次结构,我不知道它在哪里结束。

我怎样才能使 EF 完全加载 Category,父母和子女,父母和他们的父母和子女,等等?

这并不适用于整个应用程序,出于性能方面的考虑,它只适用于这个特定的实体 Category。

77906 次浏览

您可以引入一个映射表来映射每个类别的父类和子类,而不是将父类和子类属性添加到货物本身。

取决于您需要该信息的频率,可以根据需要查询该信息。通过数据库中的唯一约束,您可以避免可能出现的无限数量的关系。

您可以使用 Load代替使用 Include方法。

然后,您可以为每个子节点执行一个 for,并循环遍历所有子节点,加载它们的子节点。然后为每个人做一个通过他们的孩子,等等。

向下的层次数量将被硬编码为你拥有的每个循环的数量。

下面是使用 Load: http://msdn.microsoft.com/en-us/library/bb896249.aspx的示例

如果你正好加载了所有的递归实体,特别是在分类中,这可能会很危险,你可能会得到比你预期的更多的东西:

Category > Item > OrderLine > Item
OrderHeader > OrderLine > Item
> Item > ...

突然之间,您已经加载了大部分数据库,您还可以加载发票行,然后加载客户,然后加载所有其他发票。

你应该这样做:

var qryCategories = from q in ctx.Categories
where q.Status == "Open"
select q;


foreach (Category cat in qryCategories) {
if (!cat.Items.IsLoaded)
cat.Items.Load();
// This will only load product groups "once" if need be.
if (!cat.ProductGroupReference.IsLoaded)
cat.ProductGroupReference.Load();
foreach (Item item in cat.Items) {
// product group and items are guaranteed
// to be loaded if you use them here.
}
}

然而,一个更好的解决方案是构造查询,用结果构建一个匿名类,这样您只需要访问数据存储一次。

var qryCategories = from q in ctx.Categories
where q.Status == "Open"
select new {
Category = q,
ProductGroup = q.ProductGroup,
Items = q.Items
};

这样,如果需要,您可以返回字典结果。

请记住,您的上下文应该尽可能短暂。

如果你确实想加载整个层次结构,那么如果是我,我会尝试编写一个存储过程,它的工作是返回层次结构中的所有项,返回你首先要求的项(以及随后的子项)。

然后,让 EF 的关系修复确保他们都挂起来。

例如:

// the GetCategoryAndHierarchyById method is an enum
Category c = ctx.GetCategoryAndHierarchyById(1).ToList().First();

如果您正确地编写了存储过程,那么具体化层次结构中的所有项(即 ToList())应该会启动 EF 关系修复。

然后您想要的项目(First ())应该加载所有的子项目,他们应该加载他们的子项目等等。所有这些都是从一个存储过程调用中填充的,因此也没有 MARS 问题。

希望这个能帮上忙

亚历克斯

试试这个

List<SiteActionMap> list = this.GetQuery<SiteActionMap>()
.Where(m => m.Parent == null && m.Active == true)
.Include(m => m.Action)
.Include(m => m.Parent).ToList();


if (list == null)
return null;


this.GetQuery<SiteActionMap>()
.OrderBy(m => m.SortOrder)
.Where(m => m.Active == true)
.Include(m => m.Action)
.Include(m => m.Parent)
.ToList();


return list;

下面是我发现的一个聪明的递归函数 给你,它可以工作于此:

public partial class Category
{
public IEnumerable<Category> AllSubcategories()
{
yield return this;
foreach (var directSubcategory in Subcategories)
foreach (var subcategory in directSubcategory.AllSubcategories())
{
yield return subcategory;
}
}
}

您不希望对层次结构执行递归加载,除非您允许用户迭代地向下/向上钻取树: 每个递归级别都是对数据库的另一次访问。类似地,当您在向页面呈现或者通过 Web 服务发送数据时,需要延迟加载以防止进一步的数据库访问。

相反,翻转您的查询: 获取 Catalog,并获取其中的项目 Include。这将获得层次结构(导航属性)和扁平化的所有项,所以现在您只需要排除根目录中的非根元素,这应该非常简单。

我遇到了这个问题,并为另一个 给你提供了这个解决方案的详细示例

使用这种调用 Include硬编码版本的扩展方法,实现了动态的包含深度级别,效果很好。

namespace System.Data.Entity
{
using Linq;
using Linq.Expressions;
using Text;


public static class QueryableExtensions
{
public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source,
int levelIndex, Expression<Func<TEntity, TEntity>> expression)
{
if (levelIndex < 0)
throw new ArgumentOutOfRangeException(nameof(levelIndex));
var member = (MemberExpression)expression.Body;
var property = member.Member.Name;
var sb = new StringBuilder();
for (int i = 0; i < levelIndex; i++)
{
if (i > 0)
sb.Append(Type.Delimiter);
sb.Append(property);
}
return source.Include(sb.ToString());
}
}
}

用法:

var affiliate = await DbContext.Affiliates
.Include(3, a => a.Referrer)
.SingleOrDefaultAsync(a => a.Id == affiliateId);

无论如何,与此同时,加入 讨论关于它的 EF 回购。

您还可以在数据库中创建一个表值函数,并将其添加到 DBContext 中。然后可以从代码中调用它。

此示例要求从 Nuget.

public class FunctionReturnType
{
public Guid Id { get; set; }


public Guid AnchorId { get; set; } //the zeroPoint for the recursion


// Add other fields as you want (add them to your tablevalued function also).
// I noticed that nextParentId and depth are useful
}


public class _YourDatabaseContextName_ : DbContext
{
[TableValuedFunction("RecursiveQueryFunction", "_YourDatabaseContextName_")]
public IQueryable<FunctionReturnType> RecursiveQueryFunction(
[Parameter(DbType = "boolean")] bool param1 = true
)
{
//Example how to add parameters to your function
//TODO: Ask how to make recursive queries with SQL
var param1 = new ObjectParameter("param1", param1);
return this.ObjectContext().CreateQuery<FunctionReturnType>(
$"RecursiveQueryFunction(@{nameof(param1)})", param1);
}


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//add both (Function returntype and the actual function) to your modelbuilder.
modelBuilder.ComplexType<FunctionReturnType>();
modelBuilder.AddFunctions(typeof(_YourDatabaseContextName_), false);


base.OnModelCreating(modelBuilder);
}


public IEnumerable<Category> GetParents(Guid id)
{
//this = dbContext
return from hierarchyRow in this.RecursiveQueryFunction(true)
join yourClass from this.Set<YourClassThatHasHierarchy>()
on hierarchyRow.Id equals yourClass.Id
where hierarchyRow.AnchorId == id
select yourClass;
}
}

@ Parliament 给了我一个关于 EF6的想法。示例“带方法的类别”将所有父节点加载到根节点和所有子节点。

注意: 仅对非性能关键操作使用此选项。

Loading 1000 cat. with navigation properties took 15259 ms
Loading 1000 cat. with stored procedure took 169 ms

密码:

public class Category
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }


public string Name { get; set; }


public int? ParentId { get; set; }


public virtual Category Parent { get; set; }


public virtual ICollection<Category> Children { get; set; }


private IList<Category> allParentsList = new List<Category>();


public IEnumerable<Category> AllParents()
{
var parent = Parent;
while (!(parent is null))
{
allParentsList.Add(parent);
parent = parent.Parent;
}
return allParentsList;
}


public IEnumerable<Category> AllChildren()
{
yield return this;
foreach (var child in Children)
foreach (var granChild in child.AllChildren())
{
yield return granChild;
}
}
}

我的建议是

var query = CreateQuery()
.Where(entity => entity.Id == Id)
.Include(entity => entity.Parent);
var result = await FindAsync(query);


return result.FirstOrDefault();

这意味着它将加载单个 entity和所有这些 entity.Parent实体 recursive

entity is same as entity.Parent

现在来看一个完全不同的层次数据处理方法,例如填充树视图。

首先,对所有数据进行平面查询,然后在内存中构建对象图:

  var items = this.DbContext.Items.Where(i=> i.EntityStatusId == entityStatusId).Select(a=> new ItemInfo() {
Id = a.Id,
ParentId = a.ParentId,
Name = a.Name,
ItemTypeId = a.ItemTypeId
}).ToList();

获取根目录:

 parent = items.FirstOrDefault(a => a.ItemTypeId == (int)Enums.ItemTypes.Root);

现在构建你的图表:

 this.GetDecendantsFromList(parent, items);




private void GetDecendantsFromList(ItemInfo parent, List<ItemInfo> items)
{
parent.Children = items.Where(a => a.ParentId == parent.Id).ToList();
foreach (var child in parent.Children)
{
this.GetDecendantsFromList(child,items);
}
}
public static class EntityFrameworkExtensions
{
public static ObjectContext GetObjectContext(this DbContext context)
{
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;


return objectContext;
}


public static string GetTableName<T>(this ObjectSet<T> objectSet)
where T : class
{
string sql = objectSet.ToTraceString();
Regex regex = new Regex("FROM (?<table>.*) AS");
Match match = regex.Match(sql);


string table = match.Groups["table"].Value;
return table;
}


public static IQueryable<T> RecursiveInclude<T>(this IQueryable<T> query, Expression<Func<T, T>> navigationPropertyExpression, DbContext context)
where T : class
{
var objectContext = context.GetObjectContext();


var entityObjectSet = objectContext.CreateObjectSet<T>();
var entityTableName = entityObjectSet.GetTableName();
var navigationPropertyName = ((MemberExpression)navigationPropertyExpression.Body).Member.Name;


var navigationProperty = entityObjectSet
.EntitySet
.ElementType
.DeclaredNavigationProperties
.Where(w => w.Name.Equals(navigationPropertyName))
.FirstOrDefault();


var association = objectContext.MetadataWorkspace
.GetItems<AssociationType>(DataSpace.SSpace)
.Single(a => a.Name == navigationProperty.RelationshipType.Name);


var pkName = association.ReferentialConstraints[0].FromProperties[0].Name;
var fkName = association.ReferentialConstraints[0].ToProperties[0].Name;


var sqlQuery = @"
EXEC ('
;WITH CTE AS
(
SELECT
[cte1].' + @TABLE_PK + '
, Level = 1
FROM ' + @TABLE_NAME + ' [cte1]
WHERE [cte1].' + @TABLE_FK + ' IS NULL


UNION ALL


SELECT
[cte2].' + @TABLE_PK + '
, Level = CTE.Level + 1
FROM ' + @TABLE_NAME + ' [cte2]
INNER JOIN CTE ON CTE.' + @TABLE_PK + ' = [cte2].' + @TABLE_FK + '
)
SELECT
MAX(CTE.Level)
FROM CTE
')
";


var rawSqlQuery = context.Database.SqlQuery<int>(sqlQuery, new SqlParameter[]
{
new SqlParameter("TABLE_NAME", entityTableName),
new SqlParameter("TABLE_PK", pkName),
new SqlParameter("TABLE_FK", fkName)
});


var includeCount = rawSqlQuery.FirstOrDefault();


var include = string.Empty;


for (var i = 0; i < (includeCount - 1); i++)
{
if (i > 0)
include += ".";


include += navigationPropertyName;
}


return query.Include(include);
}
}

让我提供一个简单的解决方案,以满足启用/禁用所选部门组织结构的分层数据分支的需要。

表部门根据此 SQL 查找

CREATE TABLE [dbo].[Departments](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](1000) NOT NULL,
[OrganizationID] [int] NOT NULL,
[ParentID] [int] NULL,
[IsEnabled] [bit] NOT NULL,
CONSTRAINT [PK_Departments] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

C # 代码提供了一种非常简单的方法,对我来说很好用。 1. 它异步返回完整的表。 2. 它更改链接行的属性。

public async Task<bool> RemoveDepartmentAsync(int orgID, int depID)
{
try
{
using (var db = new GJobEntities())
{
var org = await db.Organizations.FirstOrDefaultAsync(x => x.ID == orgID); // Check if  the organization exists
if (org != null)
{
var allDepartments = await db.Departments.ToListAsync(); // get all table items
var isExisting = allDepartments.FirstOrDefault(x => x.OrganizationID == orgID && x.ID == depID);
if (isExisting != null) // Check if the department exists
{
isExisting.IsEnabled = false; // Change the property of visibility of the department
var all = allDepartments.Where(x => x.OrganizationID == orgID && x.ID == isExisting.ID).ToList();
foreach (var item in all)
{
item.IsEnabled = false;
RecursiveRemoveDepartment(orgID, item.ID, ref allDepartments); // Loop over table data set to change property of the linked items
}
await db.SaveChangesAsync();
}
return true;
}
}
}
catch (Exception ex)
{
logger.Error(ex);
}


return false;
}


private void RecursiveRemoveDepartment(int orgID, int? parentID, ref List<Department> items)
{
var all = items.Where(x => x.OrganizationID == orgID && x.ParentID == parentID);
foreach (var item in all)
{
item.IsEnabled = false;
RecursiveRemoveDepartment(orgID, item.ID, ref items);
}
}

对于相对较少的记录,这种方法工作得非常快,我猜少于100000条。 可能对于大数据集,您必须实现服务器端存储函数。

好好享受吧!

我发现如果你包含“两个父级别”,你会得到整个父级别,就像这样:

var query = Context.Items
.Include(i => i.Category)
.Include(i => i.Category.Parent.Parent)