再帰的なデータベース テーブルをクエリするために IQueryable を作成する方法は? データベースに送信されるクエリが 1 つだけになるように、1 つの 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);
}
DB は、この生成されたコードで 1 回だけ照会されます。
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);
}
これにより、パス部分ごとに 1 つの DB クエリが実行されるため、DB を 1 回だけクエリする場合は、代わりにこれを使用します。
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