如何制定IQueryable来查询递归数据库表?

我有一个像这样的数据库表:

Entity --------------------- ID int PK ParentID int FK Code varchar Text text 

ParentID字段是一个外键,同一个表中有另一个记录(递归)。 所以结构代表一棵树。

我正在尝试编写一个方法来查询此表,并根据路径获取一个特定的实体。 路径将是表示实体的Code属性和父实体的字符串。 因此,示例路径将是"foo/bar/baz" ,这意味着Code == "baz"的一个特定实体,父Code == "bar"和父Code == "foo"的父Code == "foo"

我的尝试:

 public Entity Single(string path) { string[] pathParts = path.Split('/'); string code = pathParts[pathParts.Length -1]; if (pathParts.Length == 1) return dataContext.Entities.Single(e => e.Code == code && e.ParentID == 0); IQueryable entities = dataContext.Entities.Where(e => e.Code == code); for (int i = pathParts.Length - 2; i >= 0; i--) { string parentCode = pathParts[i]; entities = entities.Where(e => e.Entity1.Code == parentCode); // incorrect } return entities.Single(); } 

我知道这是不正确的,因为for循环中的Where只是为当前实体而不是父实体添加了更多条件,但我该如何纠正呢? 在单词中,我希望for循环说“并且父代码必须是x,父代码的父代必须是y,父代码的父代的父代必须是z ……等”。 除此之外,出于性能原因,我希望它是一个IQueryable,因此只有一个查询进入数据库。

如何制定IQueryable来查询递归数据库表? 我希望它是一个IQueryable,所以只有一个查询进入数据库。

我不认为使用entity framework可以使用单个翻译查询遍历分层表。 原因是您需要实现循环或递归,并且据我所知,它们都不能转换为EF对象存储查询。

UPDATE

@Bazzz和@Steven让我思考,我不得不承认我完全错了:动态地为这些要求构建IQueryable是可能的,也很容易。

可以递归调用以下函数来构建查询:

 public static IQueryable Traverse(this IQueryable source, IQueryable table, LinkedList parts) { var code = parts.First.Value; var query = source.SelectMany(r1 => table.Where(r2 => r2.Code == code && r2.ParentID == r1.ID), (r1, r2) => r2); if (parts.Count == 1) { return query; } parts.RemoveFirst(); return query.Traverse(table, parts); } 

根查询是一种特殊情况; 这是调用Traverse的一个工作示例:

 using (var context = new TestDBEntities()) { var path = "foo/bar/baz"; var parts = new LinkedList(path.Split('/')); var table = context.TestTrees; var code = parts.First.Value; var root = table.Where(r1 => r1.Code == code && !r1.ParentID.HasValue); parts.RemoveFirst(); foreach (var q in root.Traverse(table, parts)) Console.WriteLine("{0} {1} {2}", q.ID, q.ParentID, q.Code); } 

使用此生成的代码仅查询数据库一次:

 exec sp_executesql N'SELECT [Extent3].[ID] AS [ID], [Extent3].[ParentID] AS [ParentID], [Extent3].[Code] AS [Code] FROM [dbo].[TestTree] AS [Extent1] INNER JOIN [dbo].[TestTree] AS [Extent2] ON ([Extent2].[Code] = @p__linq__1) AND ([Extent2].[ParentID] = [Extent1].[ID]) INNER JOIN [dbo].[TestTree] AS [Extent3] ON ([Extent3].[Code] = @p__linq__2) AND ([Extent3].[ParentID] = [Extent2].[ID]) WHERE ([Extent1].[Code] = @p__linq__0) AND ([Extent1].[ParentID] IS NULL)',N'@p__linq__1 nvarchar(4000),@p__linq__2 nvarchar(4000),@p__linq__0 nvarchar(4000)',@p__linq__1=N'bar',@p__linq__2=N'baz',@p__linq__0=N'foo' 

虽然我更喜欢原始查询的执行计划(见下文),但这种方法是有效的,也许是有用的。

更新结束

使用IEnumerable

我们的想法是一次性从表中获取相关数据,然后使用LINQ to Objects在应用程序中进行遍历。

这是一个递归函数,它将从序列中获取一个节点:

 static TestTree GetNode(this IEnumerable table, string[] parts, int index, int? parentID) { var q = table .Where(r => r.Code == parts[index] && (r.ParentID.HasValue ? r.ParentID == parentID : parentID == null)) .Single(); return index < parts.Length - 1 ? table.GetNode(parts, index + 1, q.ID) : q; } 

你可以像这样使用:

 using (var context = new TestDBEntities()) { var path = "foo/bar/baz"; var q = context.TestTrees.GetNode(path.Split('/'), 0, null); Console.WriteLine("{0} {1} {2}", q.ID, q.ParentID, q.Code); } 

这将为每个路径部分执行一个数据库查询,因此如果您只想查询数据库一次,请使用以下代码:

 using (var context = new TestDBEntities()) { var path = "foo/bar/baz"; var q = context.TestTrees .ToList() .GetNode(path.Split('/'), 0, null); Console.WriteLine("{0} {1} {2}", q.ID, q.ParentID, q.Code); } 

一个明显的优化是在遍历之前排除路径中不存在的代码:

 using (var context = new TestDBEntities()) { var path = "foo/bar/baz"; var parts = path.Split('/'); var q = context .TestTrees .Where(r => parts.Any(p => p == r.Code)) .ToList() .GetNode(parts, 0, null); Console.WriteLine("{0} {1} {2}", q.ID, q.ParentID, q.Code); } 

除非您的大多数实体具有类似的代码,否则此查询应该足够快。 但是,如果您绝对需要最佳性能,则可以使用原始查询。

SQL Server原始查询

对于SQL Server,基于CTE的查询可能是最好的:

 using (var context = new TestDBEntities()) { var path = "foo/bar/baz"; var q = context.Database.SqlQuery(@" WITH Tree(ID, ParentID, Code, TreePath) AS ( SELECT ID, ParentID, Code, CAST(Code AS nvarchar(512)) AS TreePath FROM dbo.TestTree WHERE ParentID IS NULL UNION ALL SELECT TestTree.ID, TestTree.ParentID, TestTree.Code, CAST(TreePath + '/' + TestTree.Code AS nvarchar(512)) FROM dbo.TestTree INNER JOIN Tree ON Tree.ID = TestTree.ParentID ) SELECT * FROM Tree WHERE TreePath = @path", new SqlParameter("path", path)).Single(); Console.WriteLine("{0} {1} {2}", q.ID, q.ParentID, q.Code); } 

根节点限制数据很容易,并且可能在性能方面非常有用:

 using (var context = new TestDBEntities()) { var path = "foo/bar/baz"; var q = context.Database.SqlQuery(@" WITH Tree(ID, ParentID, Code, TreePath) AS ( SELECT ID, ParentID, Code, CAST(Code AS nvarchar(512)) AS TreePath FROM dbo.TestTree WHERE ParentID IS NULL AND Code = @parentCode UNION ALL SELECT TestTree.ID, TestTree.ParentID, TestTree.Code, CAST(TreePath + '/' + TestTree.Code AS nvarchar(512)) FROM dbo.TestTree INNER JOIN Tree ON Tree.ID = TestTree.ParentID ) SELECT * FROM Tree WHERE TreePath = @path", new SqlParameter("path", path), new SqlParameter("parentCode", path.Split('/')[0])) .Single(); Console.WriteLine("{0} {1} {2}", q.ID, q.ParentID, q.Code); } 

脚注

所有这些都是使用.NET 4.5,EF 5,SQL Server 2012测试的。数据设置脚本:

 CREATE TABLE dbo.TestTree ( ID int not null IDENTITY PRIMARY KEY, ParentID int null REFERENCES dbo.TestTree (ID), Code nvarchar(100) ) GO INSERT dbo.TestTree (ParentID, Code) VALUES (null, 'foo') INSERT dbo.TestTree (ParentID, Code) VALUES (1, 'bar') INSERT dbo.TestTree (ParentID, Code) VALUES (2, 'baz') INSERT dbo.TestTree (ParentID, Code) VALUES (null, 'bla') INSERT dbo.TestTree (ParentID, Code) VALUES (1, 'blu') INSERT dbo.TestTree (ParentID, Code) VALUES (2, 'blo') INSERT dbo.TestTree (ParentID, Code) VALUES (null, 'baz') INSERT dbo.TestTree (ParentID, Code) VALUES (1, 'foo') INSERT dbo.TestTree (ParentID, Code) VALUES (2, 'bar') 

我的测试中的所有示例都返回了ID为3的'baz'实体。假设该实体实际存在。 error handling超出了本文的范围。

UPDATE

为了解决@ Bazzz的评论,带有路径的数据如下所示。 代码在级别上是唯一的,而不是全局的。

 ID ParentID Code TreePath ---- ----------- --------- ------------------- 1 NULL foo foo 4 NULL bla bla 7 NULL baz baz 2 1 bar foo/bar 5 1 blu foo/blu 8 1 foo foo/foo 3 2 baz foo/bar/baz 6 2 blo foo/bar/blo 9 2 bar foo/bar/bar 

诀窍是反过来做,并建立以下查询:

 from entity in dataContext.Entities where entity.Code == "baz" where entity.Parent.Code == "bar" where entity.Parent.Parent.Code == "foo" where entity.Parent.Parent.ParentID == 0 select entity; 

有点幼稚(硬编码)的解决方案是这样的:

 var pathParts = path.Split('/').ToList(); var entities = from entity in dataContext.Entities select entity; pathParts.Reverse(); for (int index = 0; index < pathParts.Count+ index++) { string pathPart = pathParts[index]; switch (index) { case 0: entities = entities.Where( entity.Code == pathPart); break; case 1: entities = entities.Where( entity.Parent.Code == pathPart); break; case 2: entities = entities.Where(entity.Parent.Parent.Code == pathPart); break; case 3: entities = entities.Where( entity.Parent.Parent.Parent.Code == pathPart); break; default: throw new NotSupportedException(); } } 

通过构建表达式树动态地执行此操作并非易事,但可以通过仔细查看C#编译器生成的内容来完成(例如,使用ILDasm或Reflector)。 这是一个例子:

 private static Entity GetEntityByPath(DataContext dataContext, string path) { List pathParts = path.Split(new char[] { '/' }).ToList(); pathParts.Reverse(); var entities = from entity in dataContext.Entities select entity; // Build up a template expression that will be used to create the real expressions with. Expression> templateExpression = entity => entity.Code == "dummy"; var equals = (BinaryExpression)templateExpression.Body; var property = (MemberExpression)equals.Left; ParameterExpression entityParameter = Expression.Parameter(typeof(Entity), "entity"); for (int index = 0; index < pathParts.Count; index++) { string pathPart = pathParts[index]; var entityFilterExpression = Expression.Lambda>( Expression.Equal( Expression.Property( BuildParentPropertiesExpression(index, entityParameter), (MethodInfo)property.Member), Expression.Constant(pathPart), equals.IsLiftedToNull, equals.Method), templateExpression.Parameters); entities = entities.Where(entityFilterExpression); // TODO: The entity.Parent.Parent.ParentID == 0 part is missing here. } return entities.Single(); } private static Expression BuildParentPropertiesExpression(int numberOfParents, ParameterExpression entityParameter) { if (numberOfParents == 0) { return entityParameter; } var getParentMethod = typeof(Entity).GetProperty("Parent").GetGetMethod(); var property = Expression.Property(entityParameter, getParentMethod); for (int count = 2; count <= numberOfParents; count++) { property = Expression.Property(property, getParentMethod); } return property; } 

你需要一个递归函数而不是你的循环。 这样的事情应该做的工作:

 public EntityTable Single(string path) { List pathParts = path.Split('/').ToList(); string code = pathParts.Last(); var entities = dataContext.EntityTables.Where(e => e.Code == code); pathParts.RemoveAt(pathParts.Count - 1); return GetRecursively(entities, pathParts); } private EntityTable GetRecursively(IQueryable entity, List pathParts) { if (!(entity == null || pathParts.Count == 0)) { string code = pathParts.Last(); if (pathParts.Count == 1) { return entity.Where(x => x.EntityTable1.Code == code && x.ParentId == x.Id).FirstOrDefault(); } else { pathParts.RemoveAt(pathParts.Count - 1); return this.GetRecursively(entity.Where(x => x.EntityTable1.Code == code), pathParts); } } else { return null; } } 

如您所见,我只是返回最终的父节点。 如果你想获得所有EntityTable对象的列表,那么我会使递归方法返回一个已找到节点的ID列表,最后 – 在Single(…)方法中 – 运行一个简单的LINQ查询来获取使用此ID列表的IQueryable对象。

编辑:我试图完成你的任务,但我认为存在一个根本问题:有些情况下你无法识别单一路径。 例如,你有两个“foo / bar / baz”和“foo / bar / baz / bak”,其中“baz”实体是不同的。 如果您将寻找路径“foo / bar / baz”,那么您将始终找到两个匹配的路径(一个将是四实体路径的一部分)。 虽然你可以正确地获得你的“baz”实体,但这太令人困惑了,我只是重新设计它:要么设置一个唯一的约束,以便每个实体只能使用一次,或者在“代码”列中存储完整路径。