12 gru 2024·7 min czytania

Modelowanie organigramów w PostgreSQL: lista sąsiedztwa kontra tabela zamknięcia

Modeluj organigramy w PostgreSQL, porównując listę sąsiedztwa i tabelę closure, z jasnymi przykładami filtrowania, raportowania i sprawdzeń uprawnień.

Modelowanie organigramów w PostgreSQL: lista sąsiedztwa kontra tabela zamknięcia

Czego potrzebuje organigram

Organigram to mapa kto komu podlega i jak zespoły łączą się w działy. Modelując organigramy w PostgreSQL, nie chodzi tylko o zapisanie manager_id przy każdej osobie. Chcesz obsłużyć realne potrzeby: przeglądanie struktury, raportowanie i reguły dostępu.

Większość użytkowników oczekuje natychmiastowych odpowiedzi na trzy rzeczy: eksplorowanie organizacji, odnajdywanie ludzi i filtrowanie wyników do „mojego obszaru”. Oczekują też bezpiecznych aktualizacji. Gdy menedżer się zmienia, diagram powinien zaktualizować się wszędzie bez łamania raportów czy uprawnień.

W praktyce dobry model musi odpowiadać na kilka powtarzających się pytań:

  • Jaki jest łańcuch dowodzenia tej osoby (aż na szczyt)?
  • Kto podlega temu menedżerowi (bezpośrednie raporty i pełne poddrzewo)?
  • Jak ludzie grupują się w zespoły i działy dla pulpitów menedżerskich?
  • Jak przeprowadzane są reorganizacje bez zakłóceń?
  • Kto może co zobaczyć, bazując na strukturze organizacji?

To robi się trudniejsze niż proste drzewo, bo organizacje często się zmieniają. Zespoły przechodzą między działami, menedżerowie zamieniają grupy, a niektóre widoki nie są czysto „osoba raportuje do osoby”. Na przykład: osoba należy do zespołu, a zespoły należą do działów. Uprawnienia dodają jeszcze jedną warstwę: kształt organizacji staje się częścią modelu bezpieczeństwa, nie tylko diagramem.

Kilka pojęć, które ułatwią projektowanie:

  • Węzeł (node) to jeden element hierarchii (osoba, zespół lub dział).
  • Rodzic (parent) to węzeł bezpośrednio nad nim (menedżer lub dział, który ma dany zespół).
  • Przodek (ancestor) to każdy węzeł nad nim w dowolnej odległości (menedżer menedżera).
  • Potomek (descendant) to każdy węzeł poniżej w dowolnej odległości (wszyscy pod tobą).

Przykład: jeśli dział Sprzedaży przechodzi pod nowego VP, dwie rzeczy powinny od razu się zgadzać. Pulpity nadal filtrują „całą Sprzedaż”, a uprawnienia nowego VP automatycznie obejmują Sprzedaż.

Decyzje przed wyborem projektu tabeli

Zanim wybierzesz schemat, ustal, na jakie pytania twoja aplikacja musi odpowiadać codziennie. „Kto komu podlega?” to dopiero początek. Wiele organigramów musi też pokazywać, kto prowadzi dział, kto zatwierdza urlopy dla zespołu i kto może zobaczyć raport.

Wypisz dokładne pytania, które będą zadawać ekrany i sprawdzenia uprawnień. Jeśli nie potrafisz tych pytań nazwać, skończysz ze schematem, który wygląda dobrze, ale jest trudny do zapytania.

Decyzje, które wszystko kształtują:

  • Które zapytania muszą być szybkie: bezpośredni menedżer, łańcuch aż do CEO, pełne poddrzewo pod liderem, czy „wszyscy w tym dziale”?
  • Czy to ścisłe drzewo (jeden menedżer), czy organizacja macierzowa (więcej niż jeden menedżer lub lider)?
  • Czy działy są węzłami w tej samej hierarchii co ludzie, czy osobnym atrybutem (np. department_id na osobie)?
  • Czy ktoś może należeć do wielu zespołów (usługi dzielone, zespoły projektowe)?
  • Jak przepływają uprawnienia: w dół drzewa, w górę, czy w obie strony?

Te wybory definiują, jak wygląda „prawidłowe” dane. Jeśli Alex prowadzi jednocześnie Support i Onboarding, pojedynczy manager_id albo zasada „jeden lider na zespół” może nie wystarczyć. Może być potrzebna tabela łącząca (lider→zespół) albo jasna polityka typu „jeden zespół podstawowy plus zespoły przerywane”.

Działy to kolejne rozwidlenie. Jeśli działy są węzłami, możesz wyrazić „Dział A zawiera Zespół B zawiera Osobę C”. Jeśli działy są oddzielne, będziesz filtrować przez department_id = X, co jest prostsze, ale może się rozpaść, gdy zespoły rozciągają się między działami.

Na koniec, zdefiniuj uprawnienia w zwykłym języku. „Menedżer może zobaczyć płace wszystkich pod sobą, ale nie współpracowników” to reguła w dół drzewa. „Każdy może zobaczyć swój łańcuch nad sobą” to reguła w górę. Zdecyduj to wcześnie, bo to zmienia, który model hierarchii będzie naturalny, a który zmusi do drogich zapytań później.

Lista sąsiedztwa: prosty schemat dla menedżerów i zespołów

Jeśli chcesz jak najmniej skomplikowane rozwiązanie, lista sąsiedztwa to klasyczny punkt startowy. Każda osoba przechowuje wskaźnik do bezpośredniego menedżera, a drzewo tworzy się śledząc te wskaźniki.

Minimalna konfiguracja wygląda tak:

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)
);

Możesz też pominąć oddzielne tabele i trzymać department_name i team_name jako kolumny w employees. To szybsze na start, ale trudniejsze do utrzymania (błędy literowe, zmiany nazw zespołów, niespójne raportowanie). Oddzielne tabele ułatwiają spójne wyrażanie filtrów i reguł uprawnień.

Dodaj ograniczenia od razu. Złe dane hierarchiczne trudno naprawić później. Przynajmniej zapobiegaj samodzielnemu zarządzaniu (manager_id <> id). Zdecyduj też, czy menedżer może być poza tym samym zespołem lub działem, oraz czy potrzebujesz miękkiego usuwania lub historii zmian (dla audytu linii raportowania).

W modelu listy sąsiedztwa większość zmian to proste zapisy: zmiana menedżera aktualizuje employees.manager_id, a przeniesienie zespołu aktualizuje employees.team_id (często wraz z menedżerem). Minusem jest to, że mały zapis może mieć duże skutki uboczne. Sumy raportów się zmieniają, a każda reguła „menedżer może zobaczyć wszystkie raporty” musi teraz podążać za nowym łańcuchem.

Ta prostota to największa zaleta listy sąsiedztwa. Słabość ujawnia się, gdy często filtrujesz „wszyscy pod tym menedżerem”, bo zwykle polegasz na zapytaniach rekurencyjnych, które przechodzą drzewo za każdym razem.

Lista sąsiedztwa: typowe zapytania do filtrowania i raportów

W modelu listy sąsiedztwa wiele użytecznych pytań o organigram sprowadza się do zapytań rekurencyjnych. Jeśli tak modelujesz organigramy w PostgreSQL, to są wzorce, które będziesz stosował często.

Bezpośrednie raporty (jedno poziom)

Najprostszy przypadek to bezpośredni zespół menedżera:

SELECT id, full_name, title
FROM employees
WHERE manager_id = $1
ORDER BY full_name;

To jest szybkie i czytelne, ale obejmuje tylko jeden poziom.

Łańcuch dowodzenia (do góry)

Aby pokazać, komu ktoś podlega (menedżer, menedżer menedżera itd.), użyj rekurencyjnego CTE:

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;

To obsługuje zatwierdzenia, ścieżki eskalacji i okruszki nawigacyjne menedżera.

Pełne poddrzewo (w dół)

Aby pobrać wszystkich pod liderem (wszystkie poziomy), odwróć rekursję:

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;

Częstym raportem jest „wszyscy w dziale X pod liderem 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;

Zapytania listy sąsiedztwa mogą być ryzykowne dla uprawnień, bo sprawdzenia dostępu często zależą od całej ścieżki (czy oglądający jest przodkiem tej osoby?). Jeśli jakiś endpoint zapomni rekurencję lub zastosuje filtry w złym miejscu, możesz ujawnić wiersze. Zwracaj też uwagę na problemy z danymi, jak cykle i brakujący menedżerowie. Jeden zły rekord może zepsuć rekurencję lub zwrócić zaskakujące wyniki, więc zapytania uprawnień wymagają zabezpieczeń i dobrych ograniczeń.

Tabela closure: jak przechowuje całą hierarchię

Posiadaj wygenerowany kod
Zachowaj opcję eksportu realnego kodu źródłowego, gdy potrzebujesz pełnej kontroli.
Eksportuj kod

Tabela closure przechowuje każdą relację przodek–potomek, nie tylko bezpośredni link do menedżera. Zamiast przechodzić drzewo krok po kroku, możesz zapytać: „kto jest pod tym liderem?” i otrzymać pełną odpowiedź za pomocą prostego joinu.

Zwykle trzymasz dwie tabele: jedną dla węzłów (ludzie lub zespoły) i jedną dla ścieżek hierarchii.

-- 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)
)

Tabela closure przechowuje pary jak (Alice, Bob) oznaczające „Alice jest przodkiem Boba”. Przechowuje też wiersz, w którym ancestor_id = descendant_id z depth = 0. Ten wiersz wygląda dziwnie na pierwszy rzut oka, ale sprawia, że wiele zapytań jest czyściejszych.

depth mówi, jak daleko od siebie są dwa węzły: depth = 1 to bezpośredni menedżer, depth = 2 to menedżer menedżera itd. To ma znaczenie, gdy bezpośrednie raporty powinny być traktowane inaczej niż pośrednie.

Główna zaleta to przewidywalne, szybkie odczyty:

  • Wyszukiwanie całego poddrzewa jest szybkie (wszyscy pod dyrektorem).
  • Łańcuchy dowodzenia są proste (wszyscy menedżerowie nad kimś).
  • Możesz rozdzielić relacje bezpośrednie i pośrednie przy pomocy depth.

Kosztem jest utrzymanie przy aktualizacjach. Jeśli Bob zmienia menedżera z Alice na Dana, musisz przebudować wiersze closure dla Boba i wszystkich poniżej Boba. Typowe podejście: usuń stare ścieżki przodków dla tego poddrzewa, potem wstaw nowe ścieżki łącząc przodków Dany z każdym węzłem w poddrzewie Boba i przeliczając depth.

Tabela closure: typowe zapytania dla szybkiego filtrowania

Uruchom portal oparty na rolach
Dostarcz portal pracowniczy, w którym widoki są automatycznie ograniczone do „mojej organizacji”.
Zbuduj portal

Tabela closure przechowuje każdą parę przodek–potomek z góry (często jako org_closure(ancestor_id, descendant_id, depth)). Dzięki temu filtry organizacyjne są szybkie, bo większość pytań sprowadza się do jednego joinu.

Aby wypisać wszystkich pod menedżerem, dołącz raz i filtruj po depth:

-- Descendants (everyone in the subtree)
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;

-- Direct reports only
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;

Dla łańcucha dowodzenia (wszyscy przodkowie jednego pracownika) odwróć join:

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;

Filtrowanie staje się przewidywalne. Przykład: „wszyscy pod liderem X, ale tylko w dziale 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;

Ponieważ hierarchia jest wcześniej obliczona, zliczenia są proste (bez rekurencji). To pomaga pulpitom, sumom uprawnień ograniczonych zakresem i dobrze współpracuje z paginacją i wyszukiwaniem, bo możesz zastosować ORDER BY, LIMIT/OFFSET i filtry bezpośrednio na zestawie potomków.

Jak każdy model wpływa na uprawnienia i sprawdzenia dostępu

Typową regułą organizacyjną jest: menedżer może zobaczyć (a czasem edytować) wszystko pod sobą. Wybrany schemat zmienia, jak często płacisz koszt ustalenia „kto jest pod kim”.

W liście sąsiedztwa sprawdzenie uprawnień zwykle wymaga rekurencji. Jeśli użytkownik otwiera stronę pokazującą 200 pracowników, zwykle budujesz zestaw potomków za pomocą rekurencyjnego CTE i filtrujesz cele względem niego.

W tabeli closure ta sama reguła często może być sprawdzona prostym testem istnienia: „czy bieżący użytkownik jest przodkiem tego pracownika?” Jeśli tak — pozwól.

-- Closure table permission check (conceptual)
SELECT 1
FROM org_closure c
WHERE c.ancestor_id = :viewer_id
  AND c.descendant_id = :employee_id
LIMIT 1;

Ta prostota ma znaczenie, gdy wprowadzasz row-level security (RLS), gdzie każde zapytanie automatycznie zawiera regułę typu „zwróć tylko wiersze, które widzi przeglądający”. W liście sąsiedztwa reguła często osadza rekurencję i może być trudniejsza do strojenia. W tabeli closure polityka jest często prostym EXISTS(...).

Najczęstsze krawędzie, gdzie logika uprawnień zawodzi:

  • Dotted-line reporting: jedna osoba ma dwóch menedżerów.
  • Asystenci i pełnomocnicy: dostęp nie zawsze opiera się na hierarchii, więc zapisuj jawne uprawnienia (często z datą wygaśnięcia).
  • Dostęp tymczasowy: uprawnienia czasowe nie powinny być wbudowane w strukturę organizacji.
  • Projekty międzyzespołowe: przyznawaj dostęp przez członkostwo w projekcie, nie przez łańcuch menedżerski.

Jeśli budujesz to w AppMaster, tabela closure często dobrze odwzorowuje wizualny model danych i utrzymuje sprawdzenie dostępu proste między aplikacją webową i mobilną.

Kompromisy: szybkość, złożoność i utrzymanie

Generuj API dla organigramu
Zamień strukturę organizacyjną w produkcyjne API bez ręcznego kodowania endpointów.
Generuj backend

Największy wybór to za co optymalizujesz: proste zapisy i mały schemat, czy szybkie odczyty dla „kto jest pod tym menedżerem” i sprawdzeń uprawnień.

Listy sąsiedztwa trzymają tabelę małą i aktualizacje łatwe. Koszt pojawia się przy odczytach: całe poddrzewo zwykle oznacza rekurencję. To może być w porządku, jeśli twoja organizacja jest mała, UI ładuje tylko kilka poziomów, albo filtry hierarchiczne są używane w niewielu miejscach.

Tabele closure odwracają kompromis. Odczyty stają się szybkie, bo „wszyscy potomkowie” to zwykłe joiny. Zapis staje się bardziej skomplikowany, bo przeniesienie jednego węzła może wymagać usunięcia i wstawienia wielu relacji.

W praktyce kompromis wygląda zwykle tak:

  • Wydajność odczytów: lista sąsiedztwa potrzebuje rekurencji; closure to głównie joiny i pozostaje szybka wraz ze wzrostem organizacji.
  • Złożoność zapisów: lista sąsiedztwa aktualizuje jedno parent_id; closure aktualizuje wiele wierszy przy przeniesieniu.
  • Rozmiar danych: lista rośnie z liczbą osób/zespołów; closure rośnie z relacjami (w najgorszym przypadku ~N^2 dla głębokiego drzewa).

Indeksowanie ma znaczenie w obu modelach, ale cel indeksów się różni:

  • Lista sąsiedztwa: indeksuj wskaźnik rodzica (manager_id) oraz typowe filtry jak flaga active.
  • Tabela closure: indeksuj (ancestor_id, descendant_id) oraz osobno descendant_id dla częstych zapytań.

Prosta zasada: jeśli rzadko filtrujesz po hierarchii, a uprawnienia to jedynie „menedżer widzi bezpośrednie raporty”, lista sąsiedztwa zwykle wystarczy. Jeśli regularnie uruchamiasz raporty „wszyscy pod VP X”, filtrujesz według drzew działów lub egzekwujesz uprawnienia hierarchiczne w wielu ekranach, tabela closure zwykle się opłaca mimo dodatkowego kosztu utrzymania.

Krok po kroku: przejście z listy sąsiedztwa do tabeli closure

Nie musisz wybierać tylko jednego modelu pierwszego dnia. Bezpieczna droga to zachować listę sąsiedztwa (manager_id lub parent_id) i dodać obok tabelę closure, potem przenosić odczyty stopniowo. To zmniejsza ryzyko, gdy walidujesz, jak nowa hierarchia zachowuje się w rzeczywistych zapytaniach i sprawdzeniach uprawnień.

Zacznij od utworzenia tabeli closure (często org_closure) z kolumnami ancestor_id, descendant_id i depth. Trzymaj ją oddzielnie od istniejącej tabeli employees lub teams, żeby móc jej backfillować i walidować bez dotykania bieżących funkcji.

Praktyczny plan wdrożenia:

  • Utwórz tabelę closure i indeksy, zachowując listę sąsiedztwa jako źródło prawdy.
  • Backfilluj wiersze closure z aktualnych relacji menedżerskich, włączając wiersz samodzielny (depth = 0).
  • Waliduj losowo: wybierz kilku menedżerów i potwierdź, że zestaw podwładnych jest ten sam w obu modelach.
  • Najpierw przełącz ścieżki odczytu: raporty, filtry i uprawnienia hierarchiczne powinny czytać z tabeli closure, zanim zmienisz zapisy.
  • Aktualizuj closure przy każdym zapisie (zmiana rodzica, zatrudnienie, przeniesienie zespołu). Gdy będzie stabilnie, wycofaj zapytania rekurencyjne.

Przy walidacji skup się na przypadkach, które zwykle psują reguły dostępu: zmiany menedżera, liderzy najwyższego szczebla i użytkownicy bez menedżera.

Jeśli tworzysz to w AppMaster, możesz uruchomić stare endpointy równolegle z nowymi czytającymi z tabeli closure, a potem przełączyć się, gdy wyniki będą zgodne.

Częste błędy, które psują filtrowanie i uprawnienia

Prototypuj oba modele hierarchii
Wypróbuj najpierw listę sąsiedztwa, a potem dodaj tabelę closure, gdy potrzebujesz szybszych odczytów.
Prototypuj teraz

Najszybszy sposób zepsuć funkcje organizacyjne to pozwolić, by hierarchia stała się niespójna. Dane mogą wyglądać poprawnie wiersz po wierszu, ale małe pomyłki powodują błędne filtry, wolne strony lub wycieki uprawnień.

Klasyczny problem to przypadkowe stworzenie cyklu: A zarządza B, a potem ktoś ustawia B jako menedżera A (lub dłuższa pętla przez 3–4 osoby). Zapytania rekurencyjne mogą wtedy biegać w nieskończoność, zwracać duplikaty lub przekraczać limit czasu. Nawet z tabelą closure cykle mogą zepsuć wiersze przodek/potomek.

Inny częsty problem to dryf closure: zmieniasz menedżera, ale aktualizujesz tylko bezpośrednią relację i zapominas odbudować wiersze closure dla poddrzewa. Wtedy filtry „wszyscy pod tym VP” zwracają mieszankę starej i nowej struktury. Trudno to zauważyć, bo profile indywidualne nadal wyglądają poprawnie.

Organigramy robią się też bałaganem, gdy działy i linie raportowania są mieszane bez jasnych zasad. Dział to często grupowanie administracyjne, a linie raportów dotyczą menedżerów. Jeśli traktujesz je jako to samo drzewo, możesz uzyskać dziwne zachowania, np. „przeniesienie działu” przypadkowo zmienia dostęp.

Uprawnienia najczęściej zawodzą, gdy sprawdzenia patrzą tylko na bezpośredniego menedżera. Jeśli zezwalasz, gdy viewer is manager of employee, tracisz pełny łańcuch. Efekt to nadmierne blokowanie (menedżer na pominiętym poziomie nie widzi swojej organizacji) lub nadmierne udostępnianie (ktoś zyskuje dostęp, bo zostaje ustawiony jako tymczasowy bezpośredni menedżer).

Wolne listy często wynikają z uruchamiania rekurencyjnego filtrowania przy każdym żądaniu (inboxy, listy ticketów, wyszukiwanie pracowników). Jeśli ten sam filtr jest używany wszędzie, chcesz albo precomputed path (tabela closure), albo cache zestawu dozwolonych identyfikatorów pracowników.

Kilka praktycznych zabezpieczeń:

  • Blokuj cykle walidacją przed zapisem zmiany menedżera.
  • Zdecyduj, co oznacza „dział” i trzymaj to oddzielnie od linii raportów.
  • Jeśli używasz tabeli closure, przebudowuj wiersze potomków przy zmianach menedżera.
  • Pisz reguły uprawnień dla całego łańcucha, nie tylko bezpośredniego menedżera.
  • Precompute’uj zakresy organizacyjne używane przez strony list, zamiast liczyć rekurencję za każdym razem.

Jeśli tworzysz panele admina w AppMaster, traktuj „zmień menedżera” jako wrażliwy przepływ: waliduj go, aktualizuj powiązane dane hierarchiczne i dopiero potem pozwól, by wpłynął na filtry i dostęp.

Szybkie kontrole przed wdrożeniem

Zweryfikuj uprawnienia hierarchii
Utrzymaj spójność „kto kogo widzi” na wszystkich ekranach dzięki prostym sprawdzeniom.
Testuj dostęp

Zanim uznasz organigram za „gotowy”, upewnij się, że potrafisz w prostych słowach wyjaśnić dostęp. Jeśli ktoś zapyta „kto może zobaczyć pracownika X i dlaczego?”, powinieneś wskazać jedną regułę i jedno zapytanie (lub widok), które to udowodni.

Wydajność to następny test rzeczywistości. W liście sąsiedztwa „pokaż mi wszystkich pod tym menedżerem” to zapytanie rekurencyjne, którego szybkość zależy od głębokości i indeksowania. W tabeli closure odczyty są zwykle szybkie, ale musisz zaufać ścieżce zapisu, że tabela będzie poprawna po każdej zmianie.

Krótka lista kontrolna przed wdrożeniem:

  • Wybierz jednego pracownika i prześledź widoczność od początku do końca: który łańcuch daje dostęp i która rola go odmawia.
  • Przeprowadź benchmark zapytania poddrzewa menedżera dla oczekiwanego rozmiaru (np. 5 poziomów i 50 000 pracowników).
  • Zablokuj złe zapisy: zapobiegaj cyklom, samodzielnemu zarządzaniu i osieroconym węzłom za pomocą ograniczeń i kontroli transakcyjnych.
  • Przetestuj bezpieczeństwo reorganizacji: przenoszenia, łączenia, zmiany menedżerów i wycofania, gdy coś nie powiedzie się w połowie.
  • Dodaj testy uprawnień, które asercjonują zarówno dozwolony, jak i zabroniony dostęp dla realistycznych ról (HR, menedżer, lead zespołu, wsparcie).

Praktyczny scenariusz do walidacji: agent wsparcia widzi tylko pracowników w przypisanym dziale, podczas gdy menedżer widzi całe swoje poddrzewo. Jeśli potrafisz zamodelować organigramy w PostgreSQL i udowodnić obie reguły testami, jesteś blisko wdrożenia.

Jeśli budujesz to jako wewnętrzne narzędzie w AppMaster, trzymaj te kontrole jako testy automatyczne wokół endpointów zwracających listy organizacji i profile pracowników, nie tylko zapytań do bazy.

Przykładowy scenariusz i kolejne kroki

Wyobraź sobie firmę z trzema działami: Sprzedaż, Wsparcie i Inżynieria. Każdy dział ma dwa zespoły, a każdy zespół ma lidera. Lider sprzedaży A zatwierdza rabaty dla swojego zespołu, lider wsparcia B widzi wszystkie zgłoszenia dla swojego działu, a VP Inżynierii widzi wszystko pod Inżynierią.

Potem następuje reorganizacja: jeden zespół Wsparcia przechodzi pod Sprzedaż, a nowy menedżer zostaje dodany między Dyrektorem Sprzedaży a dwoma liderami zespołów. Następnego dnia ktoś prosi o dostęp: „Pozwól Jamie (analitykowi sprzedaży) widzieć wszystkie konta klientów działu Sprzedaż, ale nie Inżynierię.”

Modelując organigramy w PostgreSQL listą sąsiedztwa, schemat jest prosty, ale praca aplikacji przesuwa się do zapytań i reguł uprawnień. Filtry typu „wszyscy w Sprzedaży” zwykle potrzebują rekurencji. Po dodaniu zatwierdzeń (np. „tylko menedżerowie w łańcuchu mogą zatwierdzać”) przypadki brzegowe po reorganizacji zaczynają mieć znaczenie.

W tabeli closure reorganizacje oznaczają więcej pracy przy zapisie (aktualizacja wierszy przodków/potomków), ale strona odczytu staje się prostsza. Filtrowanie i uprawnienia często sprowadzają się do prostych joinów: „czy ten użytkownik jest przodkiem tego pracownika?” lub „czy ten zespół mieści się w poddrzewie tego działu?”.

To bezpośrednio wpływa na ekrany, które budujesz: selektory osób ograniczone do działu, routing zatwierdzeń do najbliższego menedżera nad wnioskodawcą, panele administracyjne działów i audyty wyjaśniające, dlaczego dostęp istniał w danym dniu.

Kolejne kroki:

  1. Spisz reguły uprawnień prostym językiem (kto może co widzieć i dlaczego).
  2. Wybierz model, który pasuje do najczęstszych sprawdzeń (szybsze odczyty vs prostsze zapisy).
  3. Zbuduj wewnętrzne narzędzie administracyjne, które pozwoli testować reorganizacje, żądania dostępu i zatwierdzenia end-to-end.

Jeśli chcesz szybko zbudować panele administracyjne i portale świadome organizacji, AppMaster (appmaster.io) może być praktycznym wyborem: pozwala modelować dane PostgreSQL, implementować logikę zatwierdzeń w wizualnym Business Process i dostarczać webowe oraz natywne aplikacje mobilne z tego samego backendu.

FAQ

When should I use an adjacency list vs a closure table for an org chart?

Użyj listy sąsiedztwa, gdy twoja organizacja jest niewielka, aktualizacje są częste, a większość ekranów potrzebuje tylko bezpośrednich raportów lub kilku poziomów. Użyj tabeli closure, gdy często potrzebujesz „wszystkich pod tym liderem”, filtrów zakresu działu lub uprawnień opartych na hierarchii na wielu stronach — odczyty wtedy stają się prostymi joinami i pozostają przewidywalne wraz ze wzrostem.

What’s the simplest way to store “who reports to whom” in PostgreSQL?

Zacznij od pola manager_id w tabeli employees i pobieraj bezpośrednie raporty za pomocą prostego zapytania WHERE manager_id = ?. Dodaj zapytania rekurencyjne tylko do funkcji, które naprawdę potrzebują pełnego łańcucha przodków lub całego poddrzewa, jak zatwierdzenia, filtry „moja organizacja” czy pulpity pomijające poziomy.

How do I prevent cycles (A manages B and B manages A)?

Zablokuj samodzielne raportowanie za pomocą sprawdzenia jak manager_id <> id i waliduj aktualizacje, aby nigdy nie przypisać menedżera, który już należy do poddrzewa pracownika. Najbezpieczniej jest sprawdzić pokrewieństwo (ancestry) przed zapisaniem zmiany menedżera, bo jeden cykl może zepsuć rekurencję i skomplikować logikę uprawnień.

Should departments be nodes in the same hierarchy as people?

Dobrym domyślnym podejściem jest traktować działy jako grupowanie administracyjne, a linie raportowania jako oddzielne drzewo menedżerów. Dzięki temu „przeniesienie działu” nie zmienia przypadkowo, komu ktoś podlega, a filtry typu „wszyscy w Sales” są czytelniejsze, nawet gdy linie raportowania nie pokrywają się z granicami działu.

How do I model a matrix org where someone has two managers?

Zazwyczaj zapisuje się głównego menedżera w polu pracownika, a „kropkowane” relacje (dotted-line) reprezentuje osobno, np. jako relację drugiego menedżera lub mapowanie „team lead”. To nie łamie podstawowych zapytań hierarchicznych, a jednocześnie pozwala wdrożyć reguły specjalne, jak dostęp do projektów czy delegacje zatwierdzeń.

What do I need to update in a closure table when someone changes manager?

Usuń stare ścieżki przodków dla poddrzewa przenoszonego pracownika, a następnie wstaw nowe ścieżki łącząc przodków nowego menedżera z każdym węzłem w poddrzewie i przeliczając depth. Wykonaj to w transakcji, żeby nie skończyć z częściowo zaktualizowaną tabelą closure w razie błędu.

What indexes matter most for org chart queries?

Dla listy sąsiedztwa zaindeksuj employees(manager_id), ponieważ prawie każde zapytanie organizacyjne zaczyna się od tego pola, oraz dodaj indeksy dla częstych filtrów jak team_id czy department_id. Dla tabeli closure kluczowe są indeksy na (ancestor_id, descendant_id) (klucz główny) oraz osobny indeks na descendant_id, aby przyśpieszyć sprawdzenia „kto widzi ten wiersz?”.

How can I implement “a manager can see everyone under them” safely?

Popularnym wzorcem jest EXISTS na tabeli closure: pozwól na dostęp, kiedy przeglądający jest przodkiem docelowego pracownika. To dobrze współgra z row-level security, bo baza danych może konsekwentnie stosować regułę, zamiast polegać na tym, żeby każdy endpoint API pamiętał tę samą rekurencyjną logikę.

How do I handle reorg history and audit trails?

Przechowuj historię jawnie, najczęściej w osobnej tabeli, która rejestruje zmiany menedżera z datami obowiązywania, zamiast nadpisywać aktualnego menedżera i tracić przeszłość. Dzięki temu możesz odpowiedzieć „kto komu podlegał w dniu X” bez zgadywania, a raporty i audyty pozostaną spójne po reorganizacjach.

How do I migrate from an adjacency list to a closure table without breaking the app?

Zachowaj manager_id jako źródło prawdy, utwórz równoległą tabelę closure i wypełnij ją danymi z istniejącego drzewa. Przenieś ścieżki odczytu najpierw (filtry, pulpity, reguły uprawnień), potem spraw, by zapis aktualizował oba miejsca, a dopiero gdy wyniki będą zgodne, zrezygnuj z rekurencyjnych zapytań.

Łatwy do uruchomienia
Stworzyć coś niesamowitego

Eksperymentuj z AppMaster z darmowym planem.
Kiedy będziesz gotowy, możesz wybrać odpowiednią subskrypcję.

Rozpocznij