J'ai donc une table SQL qui est fondamentalement
ID, ParentID, MenuName, [Lineage, Depth]
Les deux dernières colonnes sont automatiquement calculées pour aider à la recherche afin que nous puissions les ignorer pour le moment.
Je crée un système de menu déroulant avec plusieurs catégories.
Malheureusement, EF ne joue pas bien avec les tables de référencement auto-référencées de plus d'un niveau. Donc je suis parti avec quelques options
1) Créez une requête, triez par profondeur, puis créez une classe personnalisée en C #, en la renseignant une profondeur à la fois.
2) Trouvez un moyen de charger les données avec impatience dans EF. Je ne pense pas que ce soit possible pour un nombre illimité de niveaux, seulement un montant fixe.
3) D'une autre manière, je ne suis même pas sûr de.
Toutes les contributions seraient les bienvenues!
J'ai réussi à mapper des données hiérarchiques en utilisant EF.
Prenons par exemple une entité Establishment
. Cela peut représenter une entreprise, une université ou une autre unité au sein d’une structure organisationnelle plus vaste:
public class Establishment : Entity
{
public string Name { get; set; }
public virtual Establishment Parent { get; set; }
public virtual ICollection<Establishment> Children { get; set; }
...
}
Voici comment les propriétés Parent/Children sont mappées. Ainsi, lorsque vous définissez l'entité Parent of 1, la collection Children de l'entité Parent est automatiquement mise à jour:
// 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
Notez que jusqu'à présent, je n'ai pas inclus vos propriétés de lignage ou de profondeur. Vous avez raison, EF ne fonctionne pas bien pour générer des requêtes hiérarchiques imbriquées avec les relations ci-dessus. Ce que j’ai finalement choisi, c’est l’ajout d’une nouvelle entité gérondif, ainsi que de 2 nouvelles propriétés d’entité:
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; }
}
En écrivant cela, hazzik a posté une réponse qui ressemble beaucoup à cette approche . Je continuerai cependant à vous proposer une alternative légèrement différente. J'aime faire de mes types d'entité réels mes types gerund Ancestor et Offspring car cela m'aide à obtenir la séparation entre l'ancêtre et la progéniture (ce que vous avez appelé profondeur). Voici comment je les ai cartographiées:
private class EstablishmentNodeOrm : EntityTypeConfiguration<EstablishmentNode>
{
internal EstablishmentNodeOrm()
{
ToTable(typeof(EstablishmentNode).Name);
HasKey(p => new { p.AncestorId, p.OffspringId });
}
}
... et enfin, les relations d'identification dans l'entité Établissement:
// 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);
De plus, je n'ai pas utilisé de sproc pour mettre à jour les mappages de nœuds. À la place, nous avons un ensemble de commandes internes qui dériveront/calculeront les propriétés Ancestors et Offspring en fonction des propriétés Parent & Children. Cependant, vous finissez par être capable de faire des requêtes très similaires à celles de la réponse de 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);
La raison de l'entité de pont entre l'entité principale et ses ancêtres/descendants est encore une fois parce que cette entité vous permet d'obtenir la séparation. De plus, en le déclarant comme une relation d'identification, vous pouvez supprimer des nœuds de la collection sans avoir à appeler explicitement DbContext.Delete ().
// load all entities that are more than 3 levels deep
var establishments = dbContext.Establishments
.Where(x => x.Ancestors.Any(y => y.Separation > 3));
Vous pouvez utiliser une table hiérarchique pour effectuer un chargement rapide de niveaux illimités d’arbres.
Donc, vous devez ajouter deux collections Ancestors
et Descendants
, les deux collections doivent être mappées plusieurs à plusieurs dans la table de support.
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; }
}
Les ancêtres contiendront tous les ancêtres (parent, grand-parent, grand-grand-parent, etc.) de l'entité et Descendants
contiendra tous les descendants (enfants, petits-enfants, petits-enfants, etc.) de l'entité.
Maintenant, vous devez le mapper avec 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"));
}
}
Maintenant, avec cette structure, vous pouvez faire chercher chercher comme suit
context.Trees.Include(x => x.Descendants).Where(x => x.Id == id).SingleOrDefault()
Cette requête chargera l'entité avec id
et tous ses descendants.
Vous pouvez remplir la table de support avec la procédure stockée suivante:
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
Vous pouvez même mapper une table de support sur une vue:
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
J'ai déjà passé un certain temps à essayer de corriger un bogue dans votre solution. La procédure stockée ne génère pas vraiment d'enfants, de petits-enfants, etc., ci-dessous vous trouverez une procédure stockée fixe:
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
Erreur: mauvaise référence. Traduire le code @hazzik c'était:
SET @sql = @sql + 'SELECT t.ChildId, t.ParentId FROM dbo.Tree t '
mais devrait être
SET @sql = @sql + 'SELECT h.ChildId, t.ParentId FROM dbo.Tree t '
j'ai également ajouté du code qui vous permet de mettre à jour la table TreeHierarchy non seulement lorsque vous la peuplerez.
SET @sql = @sql + 'EXCEPT SELECT t.ChildId, t.ParentId FROM dbo.TreeHierarchy t '
Et la magie. Cette procédure ou plutôt TreeHierarchy vous permet de charger des enfants simplement en incluant des ancêtres (pas des enfants et pas des descendants).
using (var context = new YourDbContext())
{
rootNode = context.Tree
.Include(x => x.Ancestors)
.SingleOrDefault(x => x.Id == id);
}
Maintenant, YourDbContext renvoie un rootNode avec des enfants chargés, des enfants des enfants de rootName (petits-enfants), etc.
Une autre option d'implémentation sur laquelle j'ai récemment travaillé ...
Mon arbre est très simple.
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; }
}
Mes exigences, pas tellement.
À partir d’un ensemble de feuilles et d’un seul ancêtre, montrez aux enfants de cet ancêtre dont les descendants ont des feuilles dans l’ensemble.
Une analogie serait une structure de fichier sur disque. L'utilisateur actuel a accès à un sous-ensemble de fichiers sur le système. Lorsque l'utilisateur ouvre des nœuds dans l'arborescence du système de fichiers, nous souhaitons uniquement montrer que les nœuds de l'utilisateur les mèneront éventuellement aux fichiers qu'ils peuvent voir. Nous ne voulons pas leur montrer les chemins d'accès aux fichiers auxquels ils n'ont pas accès (pour des raisons de sécurité, par exemple, en laissant filtrer l'existence d'un document d'un certain type).
Nous voulons pouvoir exprimer ce filtre sous la forme IQueryable<T>
afin de pouvoir l'appliquer à n'importe quelle requête de nœud, en filtrant les résultats indésirables.
Pour ce faire, j'ai créé une fonction Table Valued qui renvoie les descendants d'un nœud de l'arborescence. Cela se fait via un 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
)
Maintenant, j'utilise le code d'abord, alors je devais utiliser
https://www.nuget.org/packages/EntityFramework.Functions
afin d'ajouter la fonction à mon 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);
}
avec un type de retour complexe (impossible de réutiliser Node en examinant cela)
[ComplexType]
public class NodeDescendant
{
public int NodeID { get; set; }
public int LeafID { get; set; }
}
En réunissant tous les éléments, cela m'a permis, lorsque l'utilisateur développe un nœud dans l'arborescence, d'obtenir la liste filtrée des nœuds enfants.
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();
}
}
Il est important de noter que la fonction est exécutée sur le serveur, pas dans l'application. Voici la requête qui est exécutée
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
)
))
Notez l'appel de fonction dans la requête ci-dessus.
Je savais qu'il devait y avoir quelque chose qui n'allait pas avec cette solution. Ce n'est pas simple En utilisant cette solution, EF6 nécessite un autre paquet de hacks pour gérer un simple arbre (par exemple des suppressions). J'ai donc finalement trouvé une solution simple mais combinée à cette approche.
Tout d’abord, laissez l’entité simple: il suffit d’un parent et d’une liste d’enfants. Aussi, la cartographie devrait être simple:
HasOptional(x => x.Parent)
.WithMany(x => x.Children)
.Map(m => m.MapKey("ParentId"));
HasMany(x => x.Children)
.WithOptional(x => x.Parent);
Ajoutez ensuite la migration (code d'abord: migrations: console du package: hiérarchie Add-Migration) ou, d'une autre manière, une procédure stockée:
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
Ensuite, lorsque vous essayez de recevoir vos noeuds d’arbre de la base de données, faites-le en deux étapes:
//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);
Tout ça. Cette requête vous aide à obtenir un nœud racine et à charger tous les enfants. Sans jouer avec l'introduction d'ancêtres et de descendants.
Rappelez-vous également quand vous allez essayer de sauvegarder un sous-noeud, puis procédez comme suit:
var node = new Node { ParentId = rootNode }; //Or null, if you want node become a root
context.TreeNodess.Add(node);
context.SaveChanges();
Faites-le de cette façon, pas en ajoutant des enfants au nœud racine.
@danludwig merci pour votre réponse
J'écris une fonction pour la mise à jour Node, ça marche parfaitement. Mon code est-il bon ou devrais-je l'écrire autrement?
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;
}