我有一个这样的数据库表:
Entity
---------------------
ID int PK
ParentID int FK
Code varchar
Text text
ParentID
字段是外键,同一个表中有另一条记录(递归)。所以结构代表一棵树。
我正在尝试编写一个查询此表的方法,并根据路径获取1个特定实体。路径将是表示实体和父实体的Code
属性的字符串。因此,示例路径为"foo/bar/baz"
,表示Code == "baz"
,父级Code == "bar"
和父级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<Entity> 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();
}
我知道这不正确,因为Where
循环中的for
只会为当前实体而不是父实体添加更多条件,但我该如何纠正?在单词中,我希望for循环说“并且父代码必须是x,父代码的父代必须是y,父代码的父代的父代必须是z ......等”。除此之外,出于性能原因,我希望它是一个IQueryable,因此只有一个查询进入数据库。
答案 0 :(得分:4)
如何制定IQueryable来查询递归数据库表? 我希望它是一个IQueryable所以只有一个查询 到数据库。
我不认为使用Entity Framework可以使用单个翻译查询遍历分层表。原因是你需要实现一个循环或递归,据我所知,它们都不能被翻译成EF对象存储查询。
<强>更新强>
@Bazzz和@Steven让我思考,我不得不承认我完全错了:动态地为这些要求构建IQueryable
是可能的,也很容易。
可以递归调用以下函数来构建查询:
public static IQueryable<TestTree> Traverse(this IQueryable<TestTree> source, IQueryable<TestTree> table, LinkedList<string> 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<string>(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<TestTree> 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<TestTree>(@"
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<TestTree>(@"
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'实体。假设该实体实际存在。错误处理超出了本文的范围。
<强>更新强>
要解决@ 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
答案 1 :(得分:3)
诀窍是反过来做,并建立以下查询:
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<string> pathParts = path.Split(new char[] { '/' }).ToList<string>();
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<Func<Entity, bool>> 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<Func<Entity, bool>>(
Expression.Equal(
Expression.Property(
BuildParentPropertiesExpression(index, entityParameter),
(MethodInfo)property.Member),
Expression.Constant(pathPart),
equals.IsLiftedToNull,
equals.Method),
templateExpression.Parameters);
entities = entities.Where<Entity>(entityFilterExpression);
// TODO: The entity.Parent.Parent.ParentID == 0 part is missing here.
}
return entities.Single<Entity>();
}
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;
}
答案 2 :(得分:1)
你需要一个递归函数而不是你的循环。这样的事情应该可以胜任:
public EntityTable Single(string path)
{
List<string> 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<EntityTable> entity, List<string> 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”实体,但这太令人困惑了,我只是重新设计它:要么设置一个唯一约束,这样每个实体只能使用一次,或者在“代码”列中存储完整路径。 / p>