web-dev-qa-db-ja.com

Entity Frameworkを使用した自己参照ツリーの最も効率的な方法

だから私は基本的にはSQLテーブルを持っています

ID, ParentID, MenuName, [Lineage, Depth]

最後の2つの列は、検索を支援するために自動計算されるため、現時点では無視できます。

複数のカテゴリを持つドロップダウンメニューシステムを作成しています。

残念ながらEFは、1レベル以上の自己参照テーブルでNiceを再生するとは思わない。だから私はいくつかのオプションが残っています

1)クエリを作成し、深さ順に並べてから、C#でカスタムクラスを作成し、一度に1つの深さを設定します。

2)EFでデータを積極的にロードする方法を見つけてください。レベルを無制限に設定することは可能だとは思いません。固定量のみです。

3)他の方法で、私も確信がありません。

任意の入力を歓迎します!

35
John Mitchell

EFを使用して階層データを正常にマッピングしました。

たとえば、Establishmentエンティティを取り上げます。これは、企業、大学、またはより大きな組織構造内の他のユニットを表すことができます。

public class Establishment : Entity
{
    public string Name { get; set; }
    public virtual Establishment Parent { get; set; }
    public virtual ICollection<Establishment> Children { get; set; }
    ...
}

以下に、Parent/Childrenプロパティのマッピング方法を示します。このように、1エンティティの親を設定すると、親エンティティの子コレクションが自動的に更新されます。

// ParentEstablishment 0..1 <---> * ChildEstablishment
HasOptional(d => d.Parent)
    .WithMany(p => p.Children)
    .Map(d => d.MapKey("ParentId"))
    .WillCascadeOnDelete(false); // do not delete children when parent is deleted

これまでのところ、LineageまたはDepthプロパティを含めていないことに注意してください。そうです、EFは上記の関係を持つネストされた階層クエリを生成するのにうまく機能しません。私が最終的に決めたのは、新しいgerundエンティティの追加と、2つの新しいエンティティプロパティです。

public class EstablishmentNode : Entity
{
    public int AncestorId { get; set; }
    public virtual Establishment Ancestor { get; set; }

    public int OffspringId { get; set; }
    public virtual Establishment Offspring { get; set; }

    public int Separation { get; set; }
}

public class Establishment : Entity
{
    ...
    public virtual ICollection<EstablishmentNode> Ancestors { get; set; }
    public virtual ICollection<EstablishmentNode> Offspring { get; set; }

}

これを書いている間、 hazzikはこのアプローチに非常に似た答えを投稿しました ただし、少し異なる代替手段を提供するために、引き続き説明します。私は祖先と子孫の動名詞型を実際のエンティティ型にするのが好きです。なぜなら、祖先と子孫の間の分離(深度と呼ばれるもの)を取得するのに役立つからです。これらのマッピング方法は次のとおりです。

private class EstablishmentNodeOrm : EntityTypeConfiguration<EstablishmentNode>
{
    internal EstablishmentNodeOrm()
    {
        ToTable(typeof(EstablishmentNode).Name);
        HasKey(p => new { p.AncestorId, p.OffspringId });
    }
}

...そして最後に、施設エンティティの識別関係:

// has many ancestors
HasMany(p => p.Ancestors)
    .WithRequired(d => d.Offspring)
    .HasForeignKey(d => d.OffspringId)
    .WillCascadeOnDelete(false);

// has many offspring
HasMany(p => p.Offspring)
    .WithRequired(d => d.Ancestor)
    .HasForeignKey(d => d.AncestorId)
    .WillCascadeOnDelete(false);

また、ノードマッピングの更新にsprocを使用しませんでした。代わりに、Parent&Childrenプロパティに基づいてAncestorsおよびOffspringプロパティを導出/計算する一連の内部コマンドがあります。ただし、最終的には、hazzikの答えと非常によく似たクエリを実行できます。

// load the entity along with all of its offspring
var establishment = dbContext.Establishments
    .Include(x => x.Offspring.Select(y => e.Offspring))
    .SingleOrDefault(x => x.Id == id);

メインエンティティとその祖先/子孫の間のブリッジエンティティの理由は、このエンティティが分離を取得できるためです。また、識別関係として宣言することにより、コレクションでノードを明示的に呼び出すことなくコレクションからノードを削除できます。

// load all entities that are more than 3 levels deep
var establishments = dbContext.Establishments
    .Where(x => x.Ancestors.Any(y => y.Separation > 3));
40
danludwig

サポート階層テーブルを使用して、無制限のレベルのツリーを積極的にロードできます。

そのため、2つのコレクションAncestorsDescendantsを追加する必要があります。両方のコレクションは、サポートテーブルに多対多としてマップする必要があります。

public class Tree 
{
    public virtual Tree Parent { get; set; }
    public virtual ICollection<Tree> Children { get; set; }
    public virtual ICollection<Tree> Ancestors { get; set; }
    public virtual ICollection<Tree> Descendants { get; set; }
}

祖先にはエンティティのすべての祖先(親、祖父母、祖父母など)が含まれ、Descendantsにはすべての子孫(子、孫、孫など)が含まれます。 )エンティティの。

次に、EF Code Firstでマッピングする必要があります。

public class TreeConfiguration : EntityTypeConfiguration<Tree>
{
    public TreeConfiguration()
    {
        HasOptional(x => x.Parent)
            .WithMany(x => x.Children)
            .Map(m => m.MapKey("PARENT_ID"));

        HasMany(x => x.Children)
            .WithOptional(x => x.Parent);

        HasMany(x => x.Ancestors)
            .WithMany(x => x.Descendants)
            .Map(m => m.ToTable("Tree_Hierarchy").MapLeftKey("PARENT_ID").MapRightKey("CHILD_ID"));

        HasMany(x => x.Descendants)
            .WithMany(x => x.Ancestors)
            .Map(m => m.ToTable("Tree_Hierarchy").MapLeftKey("CHILD_ID").MapRightKey("PARENT_ID"));
    }    
}

この構造を使用すると、次のような熱心なフェッチを行うことができます

context.Trees.Include(x => x.Descendants).Where(x => x.Id == id).SingleOrDefault()

このクエリは、エンティティにidおよびそのすべてをロードします。

次のストアドプロシージャを使用して、サポートテーブルを設定できます。

CREATE PROCEDURE [dbo].[FillHierarchy] (@table_name nvarchar(MAX), @hierarchy_name nvarchar(MAX))
AS
BEGIN
    DECLARE @sql nvarchar(MAX), @id_column_name nvarchar(MAX)
    SET @id_column_name = '[' + @table_name + '_ID]'
    SET @table_name = '[' + @table_name + ']'
    SET @hierarchy_name = '[' + @hierarchy_name + ']'

    SET @sql = ''
    SET @sql = @sql + 'WITH Hierachy(CHILD_ID, PARENT_ID) AS ( '
    SET @sql = @sql + 'SELECT ' + @id_column_name + ', [PARENT_ID] FROM ' + @table_name + ' e '
    SET @sql = @sql + 'UNION ALL '
    SET @sql = @sql + 'SELECT e.' + @id_column_name + ', e.[PARENT_ID] FROM ' + @table_name + ' e '
    SET @sql = @sql + 'INNER JOIN Hierachy eh ON e.' + @id_column_name + ' = eh.[PARENT_ID]) '
    SET @sql = @sql + 'INSERT INTO ' + @hierarchy_name + ' ([CHILD_ID], [PARENT_ID]) ( '
    SET @sql = @sql + 'SELECT [CHILD_ID], [PARENT_ID] FROM Hierachy WHERE [PARENT_ID] IS NOT NULL '
    SET @sql = @sql + ') '

    EXECUTE (@sql)
END
GO

または、サポートテーブルをビューにマップすることもできます。

CREATE VIEW [Tree_Hierarchy]
AS
    WITH Hierachy (CHILD_ID, PARENT_ID) 
    AS 
    (
        SELECT [MySuperTree_ID], [PARENT_ID] FROM [MySuperTree] AS e
        UNION ALL
        SELECT e.[MySuperTree_ID], e.[PARENT_ID] FROM [MySuperTree] AS e 
            INNER JOIN Hierachy AS eh ON e.[MySuperTree_ID] = eh.[PARENT_ID]
    )

    SELECT [CHILD_ID], [PARENT_ID] FROM Hierachy WHERE [PARENT_ID] IS NOT NULL
GO
13
hazzik

あなたのソリューションのバグを修正するために、私はすでに長い時間を費やしました。ストアドプロシージャは、実際には子や孫などを生成しません。以下に、固定ストアドプロシージャを示します。

CREATE PROCEDURE dbo.UpdateHierarchy AS
BEGIN
  DECLARE @sql nvarchar(MAX)

  SET @sql = ''
  SET @sql = @sql + 'WITH Hierachy(ChildId, ParentId) AS ( '
  SET @sql = @sql + 'SELECT t.Id, t.ParentId FROM dbo.Tree t '
  SET @sql = @sql + 'UNION ALL '
  SET @sql = @sql + 'SELECT h.ChildId, t.ParentId FROM dbo.Tree t '
  SET @sql = @sql + 'INNER JOIN Hierachy h ON t.Id = h.ParentId) '
  SET @sql = @sql + 'INSERT INTO dbo.TreeHierarchy (ChildId, ParentId) ( '
  SET @sql = @sql + 'SELECT DISTINCT ChildId, ParentId FROM Hierachy WHERE ParentId IS NOT NULL '
  SET @sql = @sql + 'EXCEPT SELECT t.ChildId, t.ParentId FROM dbo.TreeHierarchy t '
  SET @sql = @sql + ') '

  EXECUTE (@sql)
END

間違い:間違った参照。 @hazzikコードの翻訳:

  SET @sql = @sql + 'SELECT t.ChildId, t.ParentId FROM dbo.Tree t '

しかし、する必要があります

  SET @sql = @sql + 'SELECT h.ChildId, t.ParentId FROM dbo.Tree t '

また、データを設定するときだけでなく、TreeHierarchyテーブルを更新できるコードを追加しました。

  SET @sql = @sql + 'EXCEPT SELECT t.ChildId, t.ParentId FROM dbo.TreeHierarchy t '

そして魔法。この手順またはむしろTreeHierarchyを使用すると、先祖(子ではなく子孫ではない)を含めるだけで子をロードできます。

 using (var context = new YourDbContext())
 {
      rootNode = context.Tree
           .Include(x => x.Ancestors)
           .SingleOrDefault(x => x.Id == id);
 } 

これで、YourDbContextは、ロードされた子、rootNameの子(孫)などの子を持つrootNodeを返します。

5
DeXteR

私が最近取り組んだ別の実装オプション...

私のツリーはとてもシンプルです。

public class Node
{
    public int NodeID { get; set; }
    public string Name { get; set; }
    public virtual Node ParentNode { get; set; }
    public int? ParentNodeID { get; set; }
    public virtual ICollection<Node> ChildNodes { get; set; }
    public int? LeafID { get; set; }
    public virtual Leaf Leaf { get; set; }
}
public class Leaf
{
    public int LeafID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Node> Nodes { get; set; }
}

私の要件ではありません。

葉のセットと単一の祖先を指定して、セット内に葉を持つ子孫を持つその祖先の子を表示します

類推は、ディスク上のファイル構造になります。現在のユーザーは、システム上のファイルのサブセットにアクセスできます。ユーザーがファイルシステムツリーでノードを開くとき、最終的にはユーザーが表示できるファイルに導くユーザーノードのみを表示します。アクセスできないファイルへのファイルパスを表示したくない(セキュリティ上の理由から、たとえば特定の種類のドキュメントの存在を漏らす)。

このフィルターをIQueryable<T>として表現できるようにしたいので、任意のノードクエリに適用して、不要な結果を除外できます。

これを行うために、ツリー内のノードの子孫を返すテーブル値関数を作成しました。これは、CTEを介して行われます。

CREATE FUNCTION [dbo].[DescendantsOf]
(   
    @parentId int
)
RETURNS TABLE 
AS
RETURN 
(
    WITH descendants (NodeID, ParentNodeID, LeafID) AS(
        SELECT NodeID, ParentNodeID, LeafID from Nodes where ParentNodeID = @parentId
        UNION ALL
        SELECT n.NodeID, n.ParentNodeID, n.LeafID from Nodes n inner join descendants d on n.ParentNodeID = d.NodeID
    ) SELECT * from descendants
)

今、私はCode Firstを使用しているので、

https://www.nuget.org/packages/EntityFramework.Functions

dbContextに関数を追加するため

[TableValuedFunction("DescendantsOf", "Database", Schema = "dbo")]
public IQueryable<NodeDescendant> DescendantsOf(int parentID)
{
    var param = new ObjectParameter("parentId", parentID);
    return this.ObjectContext().CreateQuery<NodeDescendant>("[DescendantsOf](@parentId)", param);
}

複雑な戻り値の型(Nodeを再利用できず、調べました)

[ComplexType]
public class NodeDescendant
{
    public int NodeID { get; set; }
    public int LeafID { get; set; }
}

それをすべてまとめると、ユーザーがツリー内のノードを展開したときに、子ノードのフィルターされたリストを取得できました。

public static Node[] GetVisibleDescendants(int parentId)
{
    using (var db = new Models.Database())
    {
        int[] visibleLeaves = SuperSecretResourceManager.GetLeavesForCurrentUserLol();

        var targetQuery = db.Nodes as IQueryable<Node>;

        targetQuery = targetQuery.Where(node =>
                node.ParentNodeID == parentId &&
                db.DescendantsOf(node.NodeID).Any(x => 
                                visibleLeaves.Any(y => x.LeafID == y)));

        // Notice, still an IQueryable.  Perform whatever processing is required.
        SortByCurrentUsersSavedSettings(targetQuery);

        return targetQuery.ToArray();
    }
}

関数はアプリケーションではなくサーバー上で実行されることに注意することが重要です。実行されるクエリは次のとおりです

SELECT 
    [Extent1].[NodeID] AS [NodeID], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[ParentNodeID] AS [ParentNodeID], 
    [Extent1].[LeafID] AS [LeafID]
    FROM [dbo].[Nodes] AS [Extent1]
    WHERE ([Extent1].[ParentNodeID] = @p__linq__0) AND ( EXISTS (SELECT 
        1 AS [C1]
        FROM ( SELECT 
            [Extent2].[LeafID] AS [LeafID]
            FROM [dbo].[DescendantsOf]([Extent1].[NodeID]) AS [Extent2]
        )  AS [Project1]
        WHERE  EXISTS (SELECT 
            1 AS [C1]
            FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]
            WHERE [Project1].[LeafID] = 17
        )
    ))

上記のクエリ内の関数呼び出しに注意してください。

2
Will

このソリューションには何か問題があるに違いないことを知っていました。簡単ではありません。 EF6では、このソリューションを使用して、単純なツリー(削除など)を管理するための別のハッキングパッケージが必要です。それでようやく、簡単な解決策を見つけましたが、このアプローチと組み合わせました。

まず、エンティティを単純にします。親と子のリストで十分です。また、マッピングは単純でなければなりません:

 HasOptional(x => x.Parent)
    .WithMany(x => x.Children)
    .Map(m => m.MapKey("ParentId"));

 HasMany(x => x.Children)
    .WithOptional(x => x.Parent);

次に、移行を追加します(最初のコード:移行:パッケージコンソール:移行の追加階層)または他の方法でストアドプロシージャ:

CREATE PROCEDURE [dbo].[Tree_GetChildren] (@Id int) AS
BEGIN
WITH Hierachy(ChildId, ParentId) AS (
    SELECT ts.Id, ts.ParentId 
        FROM med.MedicalTestSteps ts
    UNION ALL 
    SELECT h.ChildId, ts.ParentId 
        FROM med.MedicalTestSteps ts
        INNER JOIN Hierachy h ON ts.Id = h.ParentId
) 
SELECT h.ChildId
    FROM Hierachy h
    WHERE h.ParentId = @Id
END

次に、データベースからツリーノードを受信しようとするときは、次の2つの手順でそれを実行します。

//Get children IDs
var sql = $"EXEC Tree_GetChildren {rootNodeId}";
var children = context.Database.SqlQuery<int>(sql).ToList<int>();

//Get root node and all it's children
var rootNode = _context.TreeNodes
                    .Include(s => s.Children)
                    .Where(s => s.Id == id || children.Any(c => s.Id == c))
                    .ToList() //MUST - get all children from database then get root
                    .FirstOrDefault(s => s.Id == id);

それをすべて。このクエリは、ルートノードを取得し、すべての子をロードするのに役立ちます。祖先と子孫を紹介することなしに。

サブノードを保存しようとするときも覚えておいてください、それから次のようにしてください:

var node = new Node { ParentId = rootNode }; //Or null, if you want node become a root
context.TreeNodess.Add(node);
context.SaveChanges();

ルートノードに子を追加するのではなく、そのようにします。

2
DeXteR

@danludwig答えてくれてありがとう

ノードを更新するための関数をいくつか作成しますが、完璧に機能します。私のコードは良いですか、それとも他の方法で書くべきですか?

    public void Handle(ParentChanged e)
    {
        var categoryGuid = e.CategoryId.Id;
        var category = _context.Categories
            .Include(cat => cat.ParentCategory)
            .First(cat => cat.Id == categoryGuid);

        if (null != e.OldParentCategoryId)
        {
            var oldParentCategoryGuid = e.OldParentCategoryId.Id;
            if (category.ParentCategory.Id == oldParentCategoryGuid)
            {
                throw new Exception("Old Parent Category mismatch.");
            }
        }

        (_context as DbContext).Configuration.LazyLoadingEnabled = true;

        RemoveFromAncestors(category, category.ParentCategory);

        var newParentCategoryGuid = e.NewParentCategoryId.Id;
        var parentCategory = _context.Categories
            .First(cat => cat.Id == newParentCategoryGuid);

        category.ParentCategory = parentCategory;

        AddToAncestors(category, category.ParentCategory, 1);

        _context.Commit();
    }

    private static void RemoveFromAncestors(Model.Category.Category mainCategory, Model.Category.Category ancestorCategory)
    {
        if (null == ancestorCategory)
        {
            return;
        }

        while (true)
        {
            var offspring = ancestorCategory.Offspring;
            offspring?.RemoveAll(node => node.OffspringId == mainCategory.Id);

            if (null != ancestorCategory.ParentCategory)
            {
                ancestorCategory = ancestorCategory.ParentCategory;
                continue;
            }
            break;
        }
    }

    private static int AddToAncestors(Model.Category.Category mainCategory,
        Model.Category.Category ancestorCategory, int deep)
    {
        var offspring = ancestorCategory.Offspring ?? new List<CategoryNode>();
        if (null == ancestorCategory.Ancestors)
        {
            ancestorCategory.Ancestors = new List<CategoryNode>();
        }

        var node = new CategoryNode()
        {
            Ancestor = ancestorCategory,
            Offspring = mainCategory
        };

        offspring.Add(node);

        if (null != ancestorCategory.ParentCategory)
        {
            deep = AddToAncestors(mainCategory, ancestorCategory.ParentCategory, deep + 1);
        }

        node.Separation = deep;

        return deep;
    }
0
SH_SWAT