Baz*_*zzz 5 c# iqueryable linq-to-sql
我有一个这样的数据库表:
Entity
---------------------
ID int PK
ParentID int FK
Code varchar
Text text
Run Code Online (Sandbox Code Playgroud)
该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();
}
Run Code Online (Sandbox Code Playgroud)
我知道这是不正确的,因为循环Where内部for只是向当前 Entity而不是父 Entity添加了更多条件,但是我该如何更正呢?换句话说,我希望 for 循环说“父代码必须是 x,父代码的父代码必须是 y,父代码的父代码的父代码必须是 z .... 等等”。除此之外,出于性能原因,我希望它是一个 IQueryable,因此只有 1 个查询会进入数据库。
如何制定 IQueryable 来查询递归数据库表?我希望它是一个 IQueryable,因此只有 1 个查询会进入数据库。
我认为目前使用实体框架无法使用单个翻译查询遍历分层表。原因是您需要实现循环或递归,据我所知,两者都不能转换为 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);
}
Run Code Online (Sandbox Code Playgroud)
根查询是一个特例;这是调用的工作示例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);
}
Run Code Online (Sandbox Code Playgroud)
使用此生成的代码仅查询一次数据库:
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'
Run Code Online (Sandbox Code Playgroud)
虽然我更喜欢原始查询的执行计划(见下文),但这种方法是有效的,也许有用。
更新结束
使用 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;
}
Run Code Online (Sandbox Code Playgroud)
你可以这样使用:
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);
}
Run Code Online (Sandbox Code Playgroud)
这将为每个路径部分执行一个数据库查询,因此如果您希望数据库只查询一次,请改用:
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);
}
Run Code Online (Sandbox Code Playgroud)
一个明显的优化是在遍历之前排除我们路径中不存在的代码:
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);
}
Run Code Online (Sandbox Code Playgroud)
此查询应该足够快,除非您的大多数实体都有类似的代码。但是,如果您绝对需要最高性能,则可以使用原始查询。
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);
}
Run Code Online (Sandbox Code Playgroud)
通过根节点限制数据很容易,并且在性能方面可能非常有用:
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);
}
Run Code Online (Sandbox Code Playgroud)
脚注
所有这些都使用 .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')
Run Code Online (Sandbox Code Playgroud)
我的测试中的所有示例都返回 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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
3293 次 |
| 最近记录: |