5

次のようなデータベーステーブルがあります。

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

ParentIDフィールドは、同じテーブル内の別のレコードを持つ外部キーです (再帰的) 。したがって、構造はツリーを表します。

このテーブルをクエリし、パスに基づいて 1 つの特定のエンティティを取得するメソッドを作成しようとしています。Codeパスは、エンティティと親エンティティのプロパティを表す文字列になります。したがって、パスの例は、 、親の、および親の の親"foo/bar/baz"である 1 つの特定のエンティティを意味します。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ループ内では、親 Entityではなく現在の Entityforに条件を追加するだけなので、これが正しくないことはわかっていますが、これを修正するにはどうすればよいですか? つまり、for ループで「親のコードは x でなければならず、その親のコードの親は y でなければならず、その親のコードのその親の親は z でなければならない .... など」と言ってほしいと思います。それに加えて、パフォーマンス上の理由から、データベースに送信されるクエリが 1 つだけになるように、1 つの IQueryable にしたいと考えています。

4

3 に答える 3

5

再帰的なデータベース テーブルをクエリするために 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
于 2012-11-27T12:15:30.510 に答える
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;
}
于 2012-11-17T11:40:18.067 に答える
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 オブジェクトを作成します。

編集: 私はあなたの仕事をやろうとしましたが、根本的な問題があると思います.単一のパスを特定できない場合があります. たとえば、2 つのパス "foo/bar/baz" と "foo/bar/baz/bak" があり、"baz" エンティティは異なります。パス「foo/bar/baz」を探している場合は、常に 2 つの一致するパスが見つかります (1 つは 4 つのエンティティ パスの一部です)。「baz」エンティティを正しく取得できますが、これは紛らわしいため、これを再設計します。各エンティティを 1 回しか使用できないように一意の制約を設定するか、「コード」列にフル パスを格納します。

于 2012-11-27T01:53:51.527 に答える