Modéliser les organigrammes dans PostgreSQL : listes d'adjacence vs table de clôture
Modélisez les organigrammes dans PostgreSQL en comparant listes d'adjacence et tables de clôture, avec des exemples clairs de filtrage, rapports et vérifications de permissions.

Ce dont un organigramme doit pouvoir s'accommoder
Un organigramme est une cartographie de qui reporte à qui et comment les équipes se rattachent aux départements. Quand vous modélisez des organigrammes dans PostgreSQL, ce n'est pas seulement enregistrer un manager_id pour chaque personne. Il s'agit de soutenir le travail réel : exploration de l'organisation, rapports et règles d'accès.
La plupart des utilisateurs attendent trois choses pour que ça paraisse instantané : parcourir l'organigramme, trouver des personnes, et filtrer les résultats sur « ma zone ». Ils attendent aussi que les mises à jour soient sûres. Quand un manager change, le graphique doit se mettre à jour partout sans casser les rapports ou les permissions.
En pratique, un bon modèle doit répondre à quelques questions récurrentes :
- Quelle est la chaîne de commandement de cette personne (jusqu'en haut) ?
- Qui est sous ce manager (rapports directs et sous-arbre complet) ?
- Comment regroupe-t-on les personnes en équipes et départements pour les tableaux de bord ?
- Comment se déroulent les réorganisations sans accroc ?
- Qui peut voir quoi, en fonction de la structure organisationnelle ?
C'est plus compliqué qu'un simple arbre parce que les organisations changent souvent. Les équipes se déplacent entre départements, les managers échangent des groupes, et certaines vues ne sont pas purement « les gens reportent à des gens ». Par exemple : une personne appartient à une équipe, et les équipes appartiennent à des départements. Les permissions ajoutent une couche : la forme de l'organisation fait partie de votre modèle de sécurité, pas seulement d'un diagramme.
Quelques termes aident à garder les designs clairs :
- Un nœud est un élément de la hiérarchie (une personne, une équipe ou un département).
- Un parent est le nœud directement au-dessus (un manager, ou un département propriétaire d'une équipe).
- Un ancêtre est tout nœud au-dessus à n'importe quelle distance (le manager de votre manager).
- Un descendant est tout nœud en dessous à n'importe quelle distance (toutes les personnes sous vous).
Exemple : si Sales passe sous un nouveau VP, deux choses doivent rester vraies immédiatement. Les tableaux de bord filtrent toujours « tout Sales », et les permissions du nouveau VP couvrent automatiquement Sales.
Décisions à prendre avant de choisir un design de table
Avant de vous engager sur un schéma, soyez clair sur ce que votre application doit répondre au quotidien. « Qui reporte à qui ? » n'est que le début. Beaucoup d'organigrammes doivent aussi montrer qui dirige un département, qui approuve les congés pour une équipe, et qui peut voir un rapport.
Écrivez les questions exactes que vos écrans et vérifications de permissions poseront. Si vous ne pouvez pas nommer les questions, vous aurez un schéma qui semble correct mais difficile à interroger.
Les décisions qui influencent tout :
- Quelles requêtes doivent être rapides : manager direct, chaîne jusqu'au CEO, sous-arbre complet d'un leader, ou « tout le monde dans ce département » ?
- Est-ce un arbre strict (un seul manager) ou une org matricielle (plus d'un manager ou lead) ?
- Les départements sont-ils des nœuds dans la même hiérarchie que les personnes, ou un attribut séparé (comme
department_idsur chaque personne) ? - Quelqu'un peut-il appartenir à plusieurs équipes (services partagés, squads) ?
- Comment les permissions se propagent : vers le bas, vers le haut, ou les deux ?
Ces choix définissent à quoi ressemble des données « correctes ». Si Alex dirige à la fois Support et Onboarding, un seul manager_id ou la règle « un lead par équipe » risque de ne pas convenir. Vous pouvez avoir besoin d'une table de jointure (leader → équipe) ou d'une politique claire comme « une équipe principale, plus des équipes en pointillé ».
Les départements sont une autre bifurcation. Si les départements sont des nœuds, vous pouvez exprimer « Département A contient Équipe B contient Personne C ». Si les départements sont séparés, vous filtrerez avec department_id = X, ce qui est plus simple mais peut échouer quand des équipes couvrent plusieurs départements.
Enfin, définissez les permissions en langage courant. « Un manager peut voir les salaires de tous ceux sous lui, mais pas des pairs » est une règle vers le bas. « N'importe qui peut voir sa chaîne de management » est une règle vers le haut. Décidez cela tôt car cela change quel modèle de hiérarchie semblera naturel et lequel forcera des requêtes coûteuses plus tard.
Liste d'adjacence : un schéma simple pour managers et équipes
Si vous voulez le moins de pièces en mouvement, une liste d'adjacence est le point de départ classique. Chaque personne conserve un pointeur vers son manager direct, et l'arbre se forme en suivant ces pointeurs.
Une configuration minimale ressemble à ceci :
create table departments (
id bigserial primary key,
name text not null unique
);
create table teams (
id bigserial primary key,
department_id bigint not null references departments(id),
name text not null,
unique (department_id, name)
);
create table employees (
id bigserial primary key,
full_name text not null,
team_id bigint references teams(id),
manager_id bigint references employees(id)
);
Vous pouvez aussi sauter les tables séparées et garder department_name et team_name comme colonnes sur employees. C'est plus rapide pour démarrer, mais plus difficile à maintenir propre (typos, renommages d'équipes, rapports incohérents). Les tables séparées facilitent l'expression cohérente des filtres et des règles de permission.
Ajoutez des garde-fous tôt. De mauvaises données hiérarchiques sont pénibles à corriger plus tard. Au minimum, empêchez l'auto-gestion (manager_id <> id). Décidez aussi si un manager peut être en dehors de la même équipe ou département, et si vous avez besoin de suppressions logiques ou d'historique (pour l'audit des lignes de reporting).
Avec les listes d'adjacence, la plupart des changements sont des écritures simples : changer un manager met à jour employees.manager_id, et déplacer une équipe met à jour employees.team_id (souvent en même temps que le manager). Le revers est qu'une petite écriture peut avoir de larges effets en aval. Les agrégations de rapports changent, et toute règle « un manager peut voir tous les rapports » doit maintenant suivre la nouvelle chaîne.
Cette simplicité est la plus grande force de la liste d'adjacence. Sa faiblesse apparaît lorsque vous filtrez fréquemment par « tout le monde sous ce manager », car vous vous appuyez habituellement sur des requêtes récursives pour parcourir l'arbre à chaque fois.
Liste d'adjacence : requêtes courantes pour filtrage et rapports
Avec une liste d'adjacence, beaucoup de questions utiles de l'organigramme se transforment en requêtes récursives. Si vous modélisez des organigrammes dans PostgreSQL de cette façon, ce sont les motifs que vous utiliserez constamment.
Rapports directs (un niveau)
Le cas le plus simple est l'équipe immédiate d'un manager :
SELECT id, full_name, title
FROM employees
WHERE manager_id = $1
ORDER BY full_name;
C'est rapide et lisible, mais ça ne descend qu'un niveau.
Chaîne de commandement (vers le haut)
Pour montrer à qui quelqu'un reporte (manager, manager du manager, etc.), utilisez un CTE récursif :
WITH RECURSIVE chain AS (
SELECT id, full_name, manager_id, 0 AS depth
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.full_name, e.manager_id, c.depth + 1
FROM employees e
JOIN chain c ON e.id = c.manager_id
)
SELECT *
FROM chain
ORDER BY depth;
Cela supporte les approbations, les chemins d'escalade et les « breadcrumbs » de manager.
Sous-arbre complet (vers le bas)
Pour obtenir tout le monde sous un leader (tous niveaux), inversez la récursion :
WITH RECURSIVE subtree AS (
SELECT id, full_name, manager_id, department_id, 0 AS depth
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.full_name, e.manager_id, e.department_id, s.depth + 1
FROM employees e
JOIN subtree s ON e.manager_id = s.id
)
SELECT *
FROM subtree
ORDER BY depth, full_name;
Un rapport courant est « tout le monde dans le département X sous le leader Y » :
WITH RECURSIVE subtree AS (
SELECT id, department_id
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.department_id
FROM employees e
JOIN subtree s ON e.manager_id = s.id
)
SELECT e.*
FROM employees e
JOIN subtree s ON s.id = e.id
WHERE e.department_id = $2;
Les requêtes en liste d'adjacence peuvent devenir risquées pour les permissions parce que les vérifications d'accès dépendent souvent du chemin complet (le visualiseur est-il un ancêtre de cette personne ?). Si un endpoint oublie la récursion ou applique des filtres au mauvais endroit, vous pouvez divulguer des lignes. Surveillez aussi les problèmes de données comme les cycles et les managers manquants. Un mauvais enregistrement peut casser la récursion ou retourner des résultats surprenants, donc les requêtes de permission ont besoin de garde-fous et de bonnes contraintes.
Table de clôture : comment elle stocke toute la hiérarchie
Une table de clôture stocke chaque relation ancêtre-descendant, pas seulement le lien direct de manager. Au lieu de parcourir l'arbre pas à pas, vous pouvez demander : « Qui est sous ce leader ? » et obtenir la réponse complète avec une simple jointure.
On garde généralement deux tables : une pour les nœuds (personnes ou équipes) et une pour les chemins hiérarchiques.
-- nodes
employees (
id bigserial primary key,
name text not null,
manager_id bigint null references employees(id)
)
-- closure
employee_closure (
ancestor_id bigint not null references employees(id),
descendant_id bigint not null references employees(id),
depth int not null,
primary key (ancestor_id, descendant_id)
)
La table de clôture stocke des paires comme (Alice, Bob) signifiant « Alice est un ancêtre de Bob ». Elle stocke aussi une ligne où ancestor_id = descendant_id avec depth = 0. Ce self-row peut sembler étrange au début, mais il simplifie beaucoup de requêtes.
depth indique la distance entre deux nœuds : depth = 1 est un manager direct, depth = 2 est le manager du manager, etc. Cela compte lorsque les rapports directs doivent être traités différemment des rapports indirects.
Le principal avantage est la prévisibilité et la rapidité des lectures :
- Les recherches de sous-arbre entier sont rapides (tout le monde sous un directeur).
- Les chaînes de commandement sont simples (tous les managers au-dessus de quelqu'un).
- Vous pouvez séparer les relations directes et indirectes en utilisant
depth.
Le coût est la maintenance lors des mises à jour. Si Bob change de manager d'Alice à Dana, vous devez reconstruire les lignes de clôture pour Bob et tous ceux sous Bob. L'approche typique est : supprimer les anciens chemins d'ancêtres pour ce sous-arbre, puis insérer de nouveaux chemins en combinant les ancêtres de Dana avec chaque nœud du sous-arbre de Bob et recalculer les profondeurs.
Table de clôture : requêtes courantes pour un filtrage rapide
Une table de clôture stocke à l'avance chaque paire ancêtre-descendant (souvent sous la forme org_closure(ancestor_id, descendant_id, depth)). Cela rend les filtres rapides car la plupart des questions deviennent une seule jointure.
Pour lister tout le monde sous un manager, joignez une fois et filtrez par depth :
-- Descendants (tout le sous-arbre)
SELECT e.*
FROM employees e
JOIN org_closure c
ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
AND c.depth > 0;
-- Rapports directs seulement
SELECT e.*
FROM employees e
JOIN org_closure c
ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
AND c.depth = 1;
Pour la chaîne de commandement (tous les ancêtres d'un employé), inversez la jointure :
SELECT m.*
FROM employees m
JOIN org_closure c
ON c.ancestor_id = m.id
WHERE c.descendant_id = :employee_id
AND c.depth > 0
ORDER BY c.depth;
Le filtrage devient prévisible. Exemple : « toutes les personnes sous le leader X, mais seulement dans le département Y » :
SELECT e.*
FROM employees e
JOIN org_closure c ON c.descendant_id = e.id
WHERE c.ancestor_id = :leader_id
AND e.department_id = :department_id;
Parce que la hiérarchie est pré-calculée, les comptages sont aussi simples (pas de récursion). Cela aide les tableaux de bord et les totaux filtrés par permission, et s'entend bien avec la pagination et la recherche puisqu'on peut appliquer ORDER BY, LIMIT/OFFSET et des filtres directement sur l'ensemble des descendants.
Comment chaque modèle affecte permissions et vérifications d'accès
Une règle d'entreprise commune est simple : un manager peut voir (et parfois modifier) tout ce qui est sous lui. Le schéma choisi change la fréquence à laquelle vous payez le coût de déterminer « qui est sous qui ».
Avec une liste d'adjacence, la vérification de permission nécessite généralement de la récursion. Si un utilisateur ouvre une page listant 200 employés, vous construisez typiquement l'ensemble des descendants avec un CTE récursif et filtrez les lignes cibles par rapport à cet ensemble.
Avec une table de clôture, la même règle peut souvent être vérifiée par un simple test d'existence : « l'utilisateur courant est-il un ancêtre de cet employé ? » Si oui, autoriser.
-- Vérification de permission avec table de clôture (conceptuelle)
SELECT 1
FROM org_closure c
WHERE c.ancestor_id = :viewer_id
AND c.descendant_id = :employee_id
LIMIT 1;
Cette simplicité compte quand vous introduisez la sécurité au niveau des lignes (RLS), où chaque requête inclut automatiquement une règle du type « ne retournez que les lignes que le visiteur peut voir ». Avec les listes d'adjacence, la politique inclut souvent de la récursion et peut être plus difficile à optimiser. Avec une table de clôture, la politique est souvent un EXISTS(...) simple.
Les cas limites sont ceux où la logique de permission casse le plus souvent :
- Reporting en pointillé : une personne a effectivement deux managers.
- Assistants et délégués : l'accès n'est pas basé sur la hiérarchie, stockez des droits explicites (souvent avec une date d'expiration).
- Accès temporaire : les permissions limitées dans le temps ne doivent pas être intégrées dans la structure org.
- Projets cross-team : accordez l'accès par l'appartenance au projet, pas par la chaîne de management.
Si vous construisez ceci dans AppMaster, une table de clôture s'intègre souvent bien à un modèle de données visuel et garde la vérification d'accès simple sur le web et le mobile.
Compromis : vitesse, complexité et maintenance
Le choix principal est ce que vous optimisez : des écritures simples et un schéma minimal, ou des lectures rapides pour « qui est sous ce manager » et des vérifications de permission.
Les listes d'adjacence gardent la table petite et les mises à jour faciles. Le coût apparaît sur les lectures : un sous-arbre complet signifie habituellement de la récursion. Cela peut convenir si votre organisation est petite, si votre UI charge seulement quelques niveaux, ou si les filtres basés sur la hiérarchie sont rares.
Les tables de clôture inversent le compromis. Les lectures deviennent rapides car vous pouvez répondre à « tous les descendants » avec des jointures régulières. Les écritures deviennent plus complexes car un déplacement ou une réorg peut nécessiter d'insérer et de supprimer de nombreuses lignes de relation.
En pratique, le compromis ressemble souvent à ceci :
- Performance en lecture : une liste d'adjacence nécessite de la récursion ; la clôture est surtout des jointures et reste rapide avec la croissance.
- Complexité en écriture : la liste d'adjacence met à jour un seul
parent_id; la clôture met à jour beaucoup de lignes pour un seul déplacement. - Taille des données : la liste d'adjacence croît avec les personnes/équipes ; la clôture croît avec les relations (dans le pire des cas, à peu près N² pour un arbre profond).
L'indexation compte dans les deux modèles, mais l'objectif diffère :
- Liste d'adjacence : indexer le pointeur parent (
manager_id), plus les filtres courants comme un flag « actif ». - Table de clôture : indexer
(ancestor_id, descendant_id)et aussidescendant_idseul pour les recherches fréquentes.
Règle simple : si vous filtrez rarement par hiérarchie et que les vérifications de permission sont seulement « le manager voit ses rapports directs », une liste d'adjacence suffit souvent. Si vous exécutez régulièrement des rapports « tout le monde sous le VP X », filtrez par arbres de département, ou appliquez des permissions hiérarchiques sur de nombreux écrans, les tables de clôture paient souvent l'effort de maintenance supplémentaire.
Pas à pas : passer de la liste d'adjacence à la table de clôture
Vous n'avez pas à choisir le jour 1. Une voie sûre est de conserver votre liste d'adjacence (manager_id ou parent_id) et d'ajouter une table de clôture à côté, puis migrer les lectures progressivement. Cela réduit le risque pendant que vous validez le comportement de la nouvelle hiérarchie dans des requêtes réelles et des vérifications de permission.
Commencez par créer une table de clôture (souvent appelée org_closure) avec des colonnes comme ancestor_id, descendant_id et depth. Gardez-la séparée de vos tables employees ou teams existantes pour pouvoir backfiller et valider sans toucher aux fonctionnalités en production.
Un déploiement pratique :
- Créez la table de clôture et les index en gardant la liste d'adjacence comme source de vérité.
- Backfillez les lignes de clôture à partir des relations de manager actuelles, y compris la self-row (chaque nœud est son propre ancêtre avec depth 0).
- Validez par contrôles ponctuels : choisissez quelques managers et confirmez que le même ensemble de subordonnés apparaît dans les deux modèles.
- Basculez d'abord les chemins de lecture : rapports, filtres et permissions hiérarchiques doivent lire depuis la table de clôture avant de changer les écritures.
- Maintenez la clôture à jour à chaque écriture (re-parent, embauche, déplacement d'équipe). Une fois stable, retirez les requêtes récursives.
Pendant la validation, focalisez-vous sur les cas qui cassent habituellement les règles d'accès : changements de manager, leaders de haut niveau et utilisateurs sans manager.
Si vous construisez dans AppMaster, gardez les anciens endpoints actifs pendant que vous ajoutez des nouveaux qui lisent la table de clôture, puis basculez quand les résultats correspondent.
Erreurs courantes qui cassent les filtres d'organisation ou les permissions
La manière la plus rapide de casser les fonctionnalités d'organisation est de laisser la hiérarchie devenir incohérente. Les données peuvent sembler correctes ligne par ligne, mais de petites erreurs causent des filtres erronés, des pages lentes ou une fuite de permission.
Un problème classique est de créer accidentellement un cycle : A gère B, puis quelqu'un met B comme manager de A (ou une boucle plus longue à travers 3–4 personnes). Les requêtes récursives peuvent boucler indéfiniment, retourner des doublons ou atteindre un timeout. Même avec une table de clôture, les cycles peuvent empoisonner les lignes ancêtre/descendant.
Autre problème fréquent : la dérive de la clôture : vous changez le manager de quelqu'un, mais vous ne mettez à jour que la relation directe et oubliez de reconstruire les lignes de clôture pour le sous-arbre. Alors les filtres comme « tout le monde sous ce VP » retournent un mélange de structure ancienne et nouvelle. C'est difficile à repérer car les pages de profil individuelles semblent encore correctes.
Les organigrammes deviennent aussi confus quand départements et lignes de reporting sont mélangés sans règles claires. Un département est souvent un regroupement administratif, tandis que les lignes de reporting concernent les managers. Si vous les traitez comme le même arbre, vous pouvez obtenir des comportements étranges comme un « déménagement de département » qui change involontairement l'accès.
Les permissions échouent le plus souvent quand les vérifications regardent seulement le manager direct. Si vous autorisez l'accès quand viewer is manager of employee, vous oubliez la chaîne complète. Le résultat est soit un sur-blocage (les managers au-dessus d'un niveau ne voient pas leur org), soit un sur-partage (quelqu'un obtient l'accès en étant placé temporairement comme manager direct).
Les pages longues lentes viennent souvent de recalculer la récursion sur chaque requête (chaque boîte de réception, chaque liste de tickets, chaque recherche d'employé). Si le même filtre est utilisé partout, vous voulez soit un chemin pré-calculé (table de clôture) soit un ensemble mis en cache d'IDs d'employés autorisés.
Quelques garde-fous pratiques :
- Bloquez les cycles avec une validation avant d'enregistrer les changements de manager.
- Décidez ce que « département » signifie et gardez-le séparé du reporting.
- Si vous utilisez une table de clôture, reconstruisez les lignes de descendants lors des changements de manager.
- Écrivez des règles de permission pour la chaîne complète, pas seulement le manager direct.
- Précomputez les scopes d'organisation utilisés par les pages de liste au lieu de recalculer la récursion à chaque fois.
Si vous créez des panneaux d'administration dans AppMaster, traitez « changer le manager » comme un flux sensible : validez, mettez à jour les données de hiérarchie liées, puis laissez cela affecter les filtres et l'accès.
Contrôles rapides avant mise en production
Avant de déclarer votre organigramme « prêt », assurez-vous de pouvoir expliquer l'accès en mots simples. Si quelqu'un demande « Qui peut voir l'employé X, et pourquoi ? », vous devez pouvoir pointer vers une règle et une requête (ou vue) qui le prouve.
La performance est le suivant test de réalité. Avec une liste d'adjacence, « montre-moi tout le monde sous ce manager » devient une requête récursive dont la vitesse dépend de la profondeur et de l'indexation. Avec une table de clôture, les lectures sont généralement rapides, mais vous devez faire confiance à votre chemin d'écriture pour garder la table correcte après chaque changement.
Une courte checklist avant livraison :
- Choisissez un employé et tracez la visibilité de bout en bout : quelle chaîne accorde l'accès et quel rôle le refuse.
- Mesurez une requête sous-arbre de manager en utilisant la taille attendue (par exemple 5 niveaux et 50 000 employés).
- Bloquez les mauvaises écritures : empêchez les cycles, l'auto-gestion et les nœuds orphelins avec des contraintes et des vérifications transactionnelles.
- Testez la sécurité des réorgs : déplacements, fusions, changements de manager et rollback si quelque chose échoue à mi-parcours.
- Ajoutez des tests de permission qui affirment à la fois les accès autorisés et refusés pour des rôles réalistes (RH, manager, team lead, support).
Un scénario pratique à valider : un agent support ne peut voir que les employés de son département assigné, tandis qu'un manager peut voir son sous-arbre complet. Si vous pouvez modéliser les organigrammes dans PostgreSQL et prouver ces deux règles par des tests, vous êtes proche de la mise en production.
Si vous construisez cela comme un outil interne dans AppMaster, gardez ces contrôles comme des tests automatisés autour des endpoints qui retournent des listes d'organisation et des profils employés, pas seulement des requêtes SQL.
Scénario d'exemple et prochaines étapes
Imaginez une entreprise avec trois départements : Sales, Support et Engineering. Chaque département a deux équipes, et chaque équipe a un lead. Le Sales Lead A peut approuver des remises pour son équipe, le Support Lead B peut voir tous les tickets de son département, et le VP Engineering peut tout voir sous Engineering.
Puis une réorg arrive : une équipe Support passe sous Sales, et un nouveau manager est inséré entre le directeur Sales et deux team leads. Le lendemain, quelqu'un demande l'accès : « Permettez à Jamie (analyste Sales) de voir tous les comptes clients du département Sales, mais pas Engineering. »
Si vous modélisez les organigrammes dans PostgreSQL avec une liste d'adjacence, le schéma est simple, mais le travail applicatif se déplace dans vos requêtes et contrôles de permission. Les filtres comme « tout le monde sous Sales » nécessitent généralement de la récursion. Une fois que vous ajoutez des approbations (comme « seuls les managers dans la chaîne peuvent approuver »), les cas limites après une réorg deviennent importants.
Avec une table de clôture, les réorgs impliquent plus de travail en écriture (mettre à jour les lignes ancêtre/descendant), mais la lecture devient simple. Les filtres et permissions deviennent souvent des jointures simples : « cet utilisateur est-il un ancêtre de cet employé ? » ou « cette équipe est-elle à l'intérieur du sous-arbre de ce département ? ».
Cela se voit directement dans les écrans que vous créez : sélecteurs de personnes limités à un département, routage d'approbation vers le manager le plus proche au-dessus d'un demandeur, vues d'administration pour les tableaux de bord départementaux, et audits qui expliquent pourquoi un accès existait à une date donnée.
Prochaines étapes :
- Rédigez les règles de permission en langage courant (qui peut voir quoi, et pourquoi).
- Choisissez un modèle qui correspond aux vérifications les plus courantes (lectures rapides vs écritures simples).
- Construisez un outil d'administration interne qui vous permette de tester les réorgs, les demandes d'accès et les approbations de bout en bout.
Si vous voulez construire rapidement ces panneaux d'administration et portails sensibles à l'organisation, AppMaster (appmaster.io) peut convenir : il vous permet de modéliser des données PostgreSQL, d'implémenter la logique d'approbation dans un Business Process visuel, et de livrer des apps web et mobiles natives depuis le même backend.
FAQ
Utilisez une liste d'adjacence lorsque votre organisation est petite, que les mises à jour sont fréquentes et que la plupart des écrans n'ont besoin que des rapports directs ou de quelques niveaux. Utilisez une table de clôture lorsque vous avez constamment besoin de « tout le monde sous ce leader », de filtres par arbre de département ou de permissions basées sur la hiérarchie sur de nombreuses pages : les lectures deviennent alors de simples jointures et restent prévisibles à mesure que vous grandissez.
Commencez avec employees(manager_id) et récupérez les rapports directs avec une requête simple WHERE manager_id = ?. Ajoutez des requêtes récursives seulement pour les fonctionnalités qui ont réellement besoin de l'ascendance complète ou d'un sous-arbre complet, comme les approbations, les filtres « mon org » ou les tableaux de bord skip-level.
Bloquez l'auto-gestion avec une contrainte comme manager_id <> id, et validez les mises à jour pour ne jamais assigner un manager qui se trouve déjà dans le sous-arbre de l'employé. En pratique, la méthode la plus sûre est de vérifier l'antériorité avant d'enregistrer un changement de manager, car un seul cycle peut casser les requêtes récursives et corrompre la logique de permission.
Un bon choix par défaut est de traiter les départements comme un regroupement administratif et les lignes de reporting comme un arbre de managers séparé. Cela évite qu'un « déménagement de département » modifie par erreur la personne à qui quelqu'un rend compte, et rend les filtres comme « tout le monde dans Sales » plus clairs même lorsque les lignes de reporting ne correspondent pas aux limites du département.
Vous stockez en général un manager principal sur l'employé et vous représentez les relations en pointillé séparément, par exemple avec une relation de manager secondaire ou une table de mapping « team lead ». Cela évite de casser les requêtes hiérarchiques de base tout en permettant des règles spéciales comme l'accès projet ou la délégation d'approbation.
Supprimez les anciens chemins d'ancêtres pour le sous-arbre de l'employé déplacé, puis insérez de nouveaux chemins en combinant les ancêtres du nouveau manager avec chaque nœud du sous-arbre, en recalculant la profondeur. Faites-le dans une transaction pour ne pas laisser la table de clôture à moitié modifiée si quelque chose échoue en cours d'opération.
Pour les listes d'adjacence, indexez employees(manager_id) car presque toute requête d'organigramme commence là, et ajoutez des index pour les filtres courants comme team_id ou department_id. Pour les tables de clôture, les index clés sont la clé primaire (ancestor_id, descendant_id) et un index séparé sur descendant_id pour rendre rapides les contrôles du type « qui peut voir cette ligne ? ».
Un schéma courant consiste en un EXISTS sur la table de clôture : autorisez l'accès lorsque le visiteur est un ancêtre de l'employé ciblé. Cela fonctionne bien avec la sécurité au niveau des lignes (RLS) parce que la base de données peut appliquer la règle de manière cohérente, plutôt que de compter sur chaque endpoint d'API pour se souvenir de la même logique récursive.
Enregistrez l'historique explicitement, généralement avec une table séparée qui conserve les changements de manager avec des dates d'effet, au lieu d'écraser le manager courant et de perdre le passé. Cela vous permet de répondre à « qui reportait à qui à la date X » sans deviner, et de garder les rapports et audits cohérents après des réorganisations.
Gardez votre manager_id existant comme source de vérité, créez la table de clôture à côté et remplissez-la à partir de l'arbre courant. Déplacez d'abord les chemins de lecture (filtres, tableaux de bord, vérifications de permissions), puis faites en sorte que les écritures mettent à jour les deux tables, et ne retirez la récursion qu'une fois que vous avez validé que les résultats correspondent dans des scénarios réels.


