12 thg 12, 2024·8 phút đọc

Mô hình sơ đồ tổ chức trong PostgreSQL: adjacency lists vs closure

Mô hình sơ đồ tổ chức trong PostgreSQL bằng so sánh adjacency list và closure table, kèm ví dụ rõ ràng về lọc, báo cáo và kiểm tra quyền.

Mô hình sơ đồ tổ chức trong PostgreSQL: adjacency lists vs closure

Những gì một sơ đồ tổ chức cần hỗ trợ

Một sơ đồ tổ chức là bản đồ ai báo cáo cho ai, và cách các đội được gom thành phòng ban. Khi bạn mô hình sơ đồ tổ chức trong PostgreSQL, bạn không chỉ lưu manager_id trên mỗi người. Bạn đang hỗ trợ công việc thực tế: duyệt org, báo cáo và quy tắc truy cập.

Hầu hết người dùng mong đợi ba thứ hoạt động ngay lập tức: khám phá tổ chức, tìm người, và lọc kết quả về “khu vực của tôi”. Họ cũng mong cập nhật phải an toàn. Khi một quản lý thay đổi, sơ đồ nên cập nhật ở mọi nơi mà không phá hỏng báo cáo hay quyền hạn.

Trong thực tế, một mô hình tốt cần trả lời vài câu hỏi lặp lại:

  • Chuỗi chỉ huy của người này là gì (lên đến đỉnh)?
  • Ai thuộc quyền quản lý của người này (báo cáo trực tiếp và toàn bộ cây)?
  • Mọi người được nhóm vào đội và phòng ban ra sao cho dashboard?
  • Tái cơ cấu xảy ra mà không gây trục trặc như thế nào?
  • Ai có thể thấy gì, dựa trên cấu trúc tổ chức?

Nó trở nên phức tạp hơn một cây đơn giản vì tổ chức thay đổi thường xuyên. Đội chuyển giữa các phòng ban, quản lý đổi nhóm, và một số góc nhìn không chỉ là “người báo cáo cho người”. Ví dụ: một người thuộc một đội, và đội thuộc phòng ban. Quyền hạn thêm một lớp nữa: hình dạng tổ chức trở thành một phần của mô hình bảo mật, không chỉ là sơ đồ.

Một vài thuật ngữ giúp thiết kế rõ ràng:

  • Node là một mục trong phân cấp (một người, một đội hoặc một phòng ban).
  • Parent là node trực tiếp ở phía trên (một quản lý, hoặc phòng ban sở hữu một đội).
  • Ancestor là bất kỳ node nào phía trên ở mọi khoảng cách (quản lý của quản lý bạn).
  • Descendant là bất kỳ node nào phía dưới ở mọi khoảng cách (mọi người thuộc quyền bạn).

Ví dụ: nếu Sales chuyển về dưới một VP mới, hai điều nên đúng ngay lập tức. Dashboard vẫn lọc “tất cả Sales”, và quyền của VP mới bao phủ Sales tự động.

Quyết định cần làm trước khi chọn thiết kế bảng

Trước khi định hình schema, hãy rõ ràng về những gì app của bạn phải trả lời hàng ngày. “Ai báo cáo cho ai?” chỉ là khởi đầu. Nhiều sơ đồ tổ chức cũng cần hiển thị ai lãnh đạo một phòng ban, ai phê duyệt nghỉ phép cho một đội, và ai có thể xem một báo cáo.

Ghi lại các câu hỏi chính xác mà màn hình và kiểm tra quyền sẽ hỏi. Nếu bạn không thể đặt tên câu hỏi, bạn sẽ kết thúc với một schema trông đúng nhưng khó truy vấn.

Những quyết định định hình mọi thứ:

  • Truy vấn nào phải nhanh: quản lý trực tiếp, chuỗi đến CEO, toàn bộ subtree dưới một lãnh đạo, hay “mọi người trong phòng ban này”?
  • Có phải là một cây nghiêm ngặt (một quản lý) hay tổ chức ma trận (nhiều hơn một quản lý hoặc lãnh đạo)?
  • Phòng ban có phải là node trong cùng phân cấp với con người, hay là thuộc tính riêng (như department_id trên mỗi người)?
  • Ai đó có thể thuộc nhiều đội không (dịch vụ chia sẻ, squad)?
  • Quyền chảy theo chiều xuống, lên hay cả hai?

Những lựa chọn đó xác định dữ liệu “đúng” trông như thế nào. Nếu Alex lãnh đạo cả Support và Onboarding, một manager_id đơn lẻ hoặc quy tắc “một lãnh đạo cho mỗi đội” có thể không đủ. Bạn có thể cần một bảng join (lãnh đạo-đội) hoặc chính sách rõ ràng như “một đội chính, cộng các đội đường chấm” (dotted-line teams).

Phòng ban là một rẽ khác. Nếu phòng ban là node, bạn có thể diễn đạt “Phòng A chứa Đội B chứa Người C”. Nếu phòng ban tách riêng, bạn sẽ lọc bằng department_id = X, đơn giản hơn nhưng có thể sụp đổ khi đội trải dài qua nhiều phòng ban.

Cuối cùng, định nghĩa quyền bằng ngôn ngữ bình dân. “Một quản lý có thể xem lương của mọi người dưới quyền họ, nhưng không phải đồng cấp” là quy tắc xuống cây. “Bất kỳ ai cũng có thể xem chuỗi quản lý của mình” là quy tắc lên cây. Quyết định này sớm vì nó thay đổi mô hình phân cấp nào sẽ cảm thấy tự nhiên và mô hình nào sẽ khiến truy vấn tốn kém sau này.

Adjacency list: schema đơn giản cho quản lý và đội

Nếu bạn muốn ít thành phần chuyển động nhất, adjacency list là điểm khởi đầu cổ điển. Mỗi người lưu một con trỏ tới quản lý trực tiếp, và cây được tạo bằng cách theo các con trỏ đó.

Một thiết lập tối giản trông như sau:

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

Bạn cũng có thể bỏ qua các bảng riêng và giữ department_nameteam_name như cột trên employees. Cách đó nhanh hơn để bắt đầu, nhưng khó giữ sạch (lỗi đánh máy, đổi tên đội, và báo cáo không nhất quán). Bảng riêng giúp lọc và quy tắc quyền dễ diễn đạt một cách nhất quán.

Thêm các rào chắn sớm. Dữ liệu phân cấp xấu rất đau để sửa sau này. Ít nhất, ngăn tự quản lý (manager_id <> id). Cũng quyết định liệu một quản lý có thể nằm ngoài cùng đội hoặc phòng ban hay không, và liệu bạn cần xóa mềm hoặc thay đổi lịch sử (cho kiểm toán dòng báo cáo).

Với adjacency list, hầu hết thay đổi là ghi đơn giản: thay đổi quản lý cập nhật employees.manager_id, và chuyển đội cập nhật employees.team_id (thường kèm theo quản lý). Điểm trừ là một ghi nhỏ có thể có nhiều hệ quả xuống dòng. Các tổng hợp báo cáo thay đổi, và bất kỳ quy tắc “quản lý có thể thấy mọi báo cáo” phải theo chuỗi mới.

Sự đơn giản này là sức mạnh lớn nhất của adjacency list. Yếu điểm xuất hiện khi bạn thường xuyên lọc theo “mọi người dưới quản lý này”, vì bạn thường phải dựa vào truy vấn đệ quy để duyệt cây mỗi lần.

Adjacency list: các truy vấn thông dụng cho lọc và báo cáo

Với adjacency list, nhiều câu hỏi hữu ích về sơ đồ tổ chức trở thành truy vấn đệ quy. Nếu bạn mô hình trong PostgreSQL theo cách này, đây là các mẫu bạn sẽ dùng liên tục.

Báo cáo trực tiếp (một cấp)

Trường hợp đơn giản nhất là đội trực tiếp của một quản lý:

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

Cái này nhanh và dễ đọc, nhưng chỉ đi xuống một cấp.

Chuỗi chỉ huy (lên trên)

Để hiển thị ai mà một người báo cáo (quản lý, quản lý của quản lý, v.v.), dùng CTE đệ quy:

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;

Điều này hỗ trợ phê duyệt, đường thoát khẩn cấp, và breadcrumb quản lý.

Toàn bộ subtree (xuống dưới)

Để lấy mọi người dưới một lãnh đạo (mọi cấp), đảo ngược đệ quy:

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;

Một báo cáo phổ biến là “mọi người trong phòng ban X dưới lãnh đạo 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;

Truy vấn adjacency list có thể rủi ro cho quyền vì kiểm tra truy cập thường phụ thuộc vào toàn bộ đường dẫn (người xem có phải là ancestor của người này không?). Nếu một endpoint quên đệ quy hoặc áp dụng bộ lọc sai chỗ, bạn có thể lộ hàng. Cũng cần chú ý vấn đề dữ liệu như vòng lặp và quản lý bị thiếu. Một bản ghi xấu có thể phá đệ quy hoặc trả kết quả bất ngờ, nên truy vấn quyền cần biện pháp bảo vệ và ràng buộc tốt.

Closure table: cách nó lưu toàn bộ phân cấp

Xác thực quyền hạn phân cấp
Giữ cho “ai thấy ai” nhất quán trên mọi màn hình với các kiểm tra đơn giản.
Kiểm tra quyền truy cập

Closure table lưu mọi quan hệ ancestor-descendant, không chỉ liên kết quản lý trực tiếp. Thay vì duyệt cây từng bước, bạn có thể hỏi: “Ai nằm dưới lãnh đạo này?” và nhận câu trả lời đầy đủ bằng một join đơn giản.

Thường bạn giữ hai bảng: một cho node (người hoặc đội) và một cho các đường dẫn phân cấp.

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

Closure table lưu các cặp như (Alice, Bob) nghĩa là “Alice là ancestor của Bob”. Nó cũng lưu một hàng nơi ancestor_id = descendant_id với depth = 0. Hàng tự thân này có vẻ lạ lúc đầu, nhưng làm nhiều truy vấn sạch hơn.

depth cho biết hai node cách nhau bao xa: depth = 1 là quản lý trực tiếp, depth = 2 là quản lý của quản lý, v.v. Điều này quan trọng khi báo cáo trực tiếp nên được xử lý khác với gián tiếp.

Lợi ích chính là đọc predictable và nhanh:

  • Tra cứu toàn bộ subtree nhanh (mọi người dưới một director).
  • Chuỗi chỉ huy đơn giản (tất cả quản lý phía trên một người).
  • Bạn có thể tách mối quan hệ trực tiếp và gián tiếp bằng depth.

Chi phí là bảo trì khi cập nhật. Nếu Bob đổi quản lý từ Alice sang Dana, bạn phải xây lại các hàng closure cho Bob và mọi người dưới Bob. Cách phổ biến là: xóa các đường dẫn tổ tiên cũ cho subtree đó, rồi chèn đường dẫn mới bằng cách kết hợp tổ tiên của Dana với mọi node trong subtree của Bob và tính lại depth.

Closure table: các truy vấn thông dụng cho lọc nhanh

Tạo bảng quản trị tái cơ cấu
Tạo luồng "thay đổi quản lý" an toàn với xác thực để tránh vòng lặp.
Xây dựng quản trị

Closure table lưu mọi cặp ancestor-descendant trước (thường là org_closure(ancestor_id, descendant_id, depth)). Điều đó làm cho bộ lọc org nhanh vì hầu hết câu hỏi trở thành một join đơn.

Để liệt kê mọi người dưới một quản lý, join một lần và lọc theo depth:

-- Descendants (mọi người trong 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;

-- Chỉ báo cáo trực tiếp
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;

Cho chuỗi chỉ huy (tất cả ancestors của một nhân viên), đảo 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;

Lọc trở nên dễ dự đoán. Ví dụ: “mọi người dưới lãnh đạo X, nhưng chỉ trong phòng ban 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;

Vì phân cấp được tính trước, việc đếm cũng đơn giản (không cần đệ quy). Điều này giúp dashboard và tổng hợp theo quyền, và hoạt động tốt với phân trang và tìm kiếm vì bạn có thể áp ORDER BY, LIMIT/OFFSET, và bộ lọc trực tiếp trên tập descendants.

Mỗi mô hình ảnh hưởng thế nào đến quyền và kiểm tra truy cập

Một quy tắc org phổ biến là đơn giản: một quản lý có thể xem (và đôi khi chỉnh sửa) mọi thứ dưới quyền họ. Schema bạn chọn thay đổi tần suất bạn phải trả chi phí để xác định “ai dưới ai”.

Với adjacency list, kiểm tra quyền thường cần đệ quy. Nếu một người mở trang liệt kê 200 nhân viên, bạn thường xây tập descendants bằng CTE đệ quy và lọc các hàng mục tiêu theo đó.

Với closure table, cùng quy tắc thường có thể kiểm tra bằng phép tồn tại đơn: “Người xem có phải là ancestor của nhân viên này không?” Nếu có, cho phép.

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

Sự đơn giản này quan trọng khi bạn giới thiệu row-level security (RLS), nơi mọi truy vấn tự động bao gồm một quy tắc như “chỉ trả về các hàng người xem có thể thấy”. Với adjacency list, chính sách thường nhúng đệ quy và khó tinh chỉnh hơn. Với closure table, policy thường là một kiểm tra EXISTS (...) thẳng thắn.

Các trường hợp biên là nơi logic quyền hay gặp lỗi nhất:

  • Dotted-line reporting: một người có hiệu quả hai quản lý.
  • Trợ lý và ủy quyền: truy cập không dựa trên phân cấp, nên lưu cấp phép rõ ràng (thường có thời hạn).
  • Truy cập tạm thời: quyền theo thời gian không nên gói vào cấu trúc org.
  • Dự án xuyên đội: cấp quyền theo thành viên dự án, không theo chuỗi quản lý.

Nếu bạn xây trong AppMaster, closure table thường ánh xạ tốt vào mô hình dữ liệu trực quan và giữ cho kiểm tra truy cập đơn giản trên web và mobile.

Đánh đổi: tốc độ, độ phức tạp và bảo trì

Xây dựng mô hình tổ chức của bạn
Mô hình nhân viên, đội và bảng closure trên PostgreSQL bằng giao diện trực quan.
Dùng thử AppMaster

Lựa chọn lớn nhất là bạn tối ưu cho gì: ghi đơn giản và schema nhỏ, hay đọc nhanh cho câu hỏi “ai dưới quản lý này” và kiểm tra quyền.

Adjacency list giữ bảng nhỏ và cập nhật dễ. Chi phí xuất hiện ở phía đọc: toàn bộ subtree thường cần đệ quy. Điều đó có thể ổn nếu tổ chức nhỏ, UI chỉ tải vài cấp, hoặc bộ lọc dựa trên phân cấp chỉ dùng ở vài nơi.

Closure table đổi ngược lại. Đọc trở nên nhanh vì bạn có thể trả lời “tất cả descendants” bằng joins thường. Ghi phức tạp hơn vì một lần di chuyển hoặc tái cơ cấu có thể yêu cầu chèn/xóa nhiều hàng quan hệ.

Trong công việc thực tế, đánh đổi thường như sau:

  • Hiệu năng đọc: adjacency cần đệ quy; closure chủ yếu là join và nhanh khi org lớn.
  • Độ phức tạp ghi: adjacency cập nhật một parent_id; closure cập nhật nhiều hàng khi di chuyển.
  • Kích thước dữ liệu: adjacency tăng theo số người/đội; closure tăng theo quan hệ (tệ nhất gần như N^2 cho cây sâu).

Việc đánh chỉ mục quan trọng ở cả hai mô hình, nhưng mục tiêu khác nhau:

  • Adjacency list: đánh chỉ mục con trỏ cha (manager_id), cộng các bộ lọc thường dùng như flag active.
  • Closure table: đánh chỉ mục (ancestor_id, descendant_id) và cả descendant_id riêng cho các lookup phổ biến.

Một quy tắc đơn giản: nếu bạn hiếm khi lọc theo phân cấp và kiểm tra quyền chỉ là “quản lý thấy báo cáo trực tiếp”, adjacency list thường đủ. Nếu bạn hay chạy báo cáo “mọi người dưới VP X”, lọc theo cây phòng ban, hoặc thực thi quyền phân cấp trên nhiều màn hình, closure table thường đáng công chi phí bảo trì thêm.

Các bước từng bước: chuyển từ adjacency list sang closure table

Bạn không phải chọn một mô hình ngay ngày đầu. Một con đường an toàn là giữ adjacency list (manager_id hoặc parent_id) và thêm một closure table cạnh đó, rồi di chuyển đường đọc theo thời gian. Điều này giảm rủi ro trong khi bạn kiểm tra mô hình mới với truy vấn và kiểm tra quyền thật.

Bắt đầu bằng tạo bảng closure (thường gọi org_closure) với các cột ancestor_id, descendant_id, và depth. Giữ nó riêng khỏi employees hoặc teams hiện tại để bạn có thể backfill và kiểm tra mà không ảnh hưởng tính năng hiện tại.

Một rollout thực tế:

  • Tạo bảng closure và chỉ mục trong khi giữ adjacency list là nguồn chân lý.
  • Backfill các hàng closure từ quan hệ quản lý hiện tại, bao gồm hàng tự thân (mỗi node là ancestor của chính nó ở depth 0).
  • Xác thực bằng kiểm tra chỗ: chọn vài quản lý và so sánh tập cấp dưới trong hai mô hình.
  • Chuyển đường đọc trước: báo cáo, bộ lọc và quyền phân cấp nên đọc từ closure table trước khi bạn thay đổi ghi.
  • Cập nhật closure trên mỗi ghi (re-parent, hire, chuyển đội). Khi ổn định, bỏ các truy vấn dựa trên đệ quy.

Khi xác thực, tập trung vào các trường hợp thường làm hỏng quyền: thay đổi quản lý, lãnh đạo cấp cao, và người dùng không có quản lý.

Nếu bạn xây trong AppMaster, bạn có thể giữ các endpoint cũ chạy trong khi thêm endpoint mới đọc từ closure table, rồi chuyển khi kết quả khớp.

Lỗi phổ biến làm hỏng lọc org hoặc quyền

Triển khai thư mục sơ đồ tổ chức
Tạo trình duyệt sơ đồ tổ chức trên web với tìm kiếm, bộ lọc và khoanh vùng theo quản lý.
Bắt đầu xây dựng

Cách nhanh nhất để làm hỏng tính năng org là để phân cấp trở nên không nhất quán. Dữ liệu có thể trông ổn theo từng hàng, nhưng vài sai sót nhỏ có thể gây lọc sai, trang chậm, hoặc rò rỉ quyền.

Vấn đề kinh điển là vô tình tạo vòng lặp: A quản lý B, và sau đó ai đó đặt B quản lý A (hoặc một vòng dài hơn qua 3-4 người). Truy vấn đệ quy có thể chạy vô hạn, trả về hàng trùng lặp, hoặc timeout. Ngay cả với closure table, vòng lặp có thể làm ô nhiễm các hàng ancestor/descendant.

Vấn đề phổ biến khác là closure drift: bạn thay đổi quản lý của ai đó, nhưng chỉ cập nhật quan hệ trực tiếp và quên xây lại hàng closure cho subtree. Khi đó bộ lọc như “mọi người dưới VP này” trả về lẫn lộn cấu trúc cũ và mới. Khó phát hiện vì trang profile cá nhân vẫn trông đúng.

Sơ đồ tổ chức cũng lộn xộn khi phòng ban và đường báo cáo bị trộn mà không có quy tắc rõ ràng. Phòng ban thường là nhóm hành chính, trong khi đường báo cáo là về quản lý. Nếu bạn coi chúng cùng một cây, bạn có thể gặp hành vi kỳ lạ như “di chuyển phòng ban” vô tình thay đổi quyền truy cập.

Quyền thất bại thường nhất khi kiểm tra chỉ nhìn quản lý trực tiếp. Nếu bạn cho phép truy cập khi viewer is manager of employee, bạn bỏ qua toàn bộ chuỗi. Kết quả là hoặc chặn quá nhiều (quản lý skip-level không thể thấy org của họ) hoặc chia sẻ quá nhiều (ai đó có quyền bằng cách tạm thời được đặt làm quản lý trực tiếp).

Trang danh sách chậm thường do chạy lọc đệ quy trên mỗi yêu cầu (mỗi inbox, mỗi danh sách ticket, mỗi tìm kiếm nhân viên). Nếu cùng một bộ lọc dùng khắp nơi, bạn nên có đường dẫn tiền tính trước (closure table) hoặc một tập ID nhân viên được cache sẵn.

Một vài biện pháp phòng ngừa thực tế:

  • Chặn vòng lặp bằng xác thực trước khi lưu thay đổi quản lý.
  • Quyết định “phòng ban” nghĩa là gì, và giữ nó tách biệt khỏi đường báo cáo.
  • Nếu dùng closure table, rebuild các hàng descendant khi thay đổi quản lý.
  • Viết quy tắc quyền cho toàn bộ chuỗi, không chỉ quản lý trực tiếp.
  • Tiền tính toán phạm vi org dùng cho trang danh sách thay vì tính đệ quy mỗi lần.

Nếu bạn xây bảng quản trị trong AppMaster, coi “thay đổi quản lý” là workflow nhạy cảm: xác thực, cập nhật dữ liệu phân cấp liên quan, rồi mới cho phép thay đổi ảnh hưởng tới bộ lọc và quyền.

Kiểm tra nhanh trước khi phát hành

Sở hữu mã nguồn sinh ra
Giữ tùy chọn xuất mã nguồn thật khi bạn cần kiểm soát hoàn toàn.
Xuất mã

Trước khi coi sơ đồ tổ chức là “hoàn thành”, hãy chắc bạn có thể giải thích truy cập bằng lời thường. Nếu ai đó hỏi, “Ai có thể xem nhân viên X, và tại sao?”, bạn nên chỉ ra một quy tắc và một truy vấn (hoặc view) chứng minh điều đó.

Hiệu năng là kiểm tra thực tế tiếp theo. Với adjacency list, “cho tôi mọi người dưới quản lý này” trở thành truy vấn đệ quy mà tốc độ phụ thuộc vào độ sâu và chỉ mục. Với closure table, đọc thường nhanh, nhưng bạn phải tin vào đường ghi để giữ bảng đúng sau mỗi thay đổi.

Danh sách kiểm tra trước khi phát hành ngắn:

  • Chọn một nhân viên và truy vết quyền nhìn từ đầu đến cuối: chuỗi nào cấp quyền và vai trò nào từ chối.
  • Benchmark truy vấn subtree của một quản lý theo kích thước mong đợi (ví dụ 5 cấp sâu và 50.000 nhân viên).
  • Chặn ghi xấu: ngăn vòng lặp, tự quản lý, và node mồ côi bằng ràng buộc và kiểm tra transaction.
  • Test an toàn tái cơ cấu: di chuyển, gộp, thay đổi quản lý, và rollback khi có lỗi giữa chừng.
  • Thêm test quyền xác nhận cả cho truy cập được phép và bị từ chối cho vai trò thực tế (HR, manager, team lead, support).

Một kịch bản thực tế để xác thực: một agent support chỉ xem được nhân viên trong phòng ban được gán, trong khi một quản lý xem toàn bộ subtree của họ. Nếu bạn có thể mô hình và chứng minh cả hai quy tắc trong PostgreSQL bằng test, bạn gần sẵn sàng phát hành.

Nếu bạn xây nội bộ trong AppMaster, giữ các kiểm tra này tự động quanh endpoint trả danh sách org và profile nhân viên, không chỉ truy vấn DB.

Ví dụ kịch bản và bước tiếp theo

Hãy tưởng tượng một công ty có ba phòng ban: Sales, Support và Engineering. Mỗi phòng có hai đội, và mỗi đội có một lead. Sales Lead A phê duyệt chiết khấu cho đội họ, Support Lead B xem mọi ticket cho phòng họ, và VP Engineering thấy tất cả dưới Engineering.

Rồi có một tái cơ cấu: một đội Support chuyển về Sales, và một quản lý mới được thêm giữa Giám đốc Sales và hai team lead. Ngày hôm sau, ai đó yêu cầu quyền: “Cho Jamie (analyst Sales) xem tất cả tài khoản khách hàng cho phòng Sales, nhưng không phải Engineering.”

Nếu bạn mô hình bằng adjacency list, schema đơn giản, nhưng gánh nặng mảng app dịch sang truy vấn và kiểm tra quyền. Bộ lọc như “mọi người dưới Sales” thường cần đệ quy. Khi thêm phê duyệt (như “chỉ quản lý trong chuỗi mới được phê duyệt”), các trường hợp biên sau tái cơ cấu bắt đầu quan trọng.

Với closure table, tái cơ cấu tốn nhiều công ghi hơn (cập nhật hàng ancestor/descendant), nhưng phía đọc trở nên thẳng thắn. Lọc và quyền thường là join đơn giản: “người này có phải ancestor của nhân viên kia?” hoặc “đội này có nằm trong subtree phòng ban này không?”.

Điều này hiện trực tiếp trên các màn hình: picker người được khoanh vùng theo phòng ban, routing phê duyệt tới quản lý gần nhất phía trên người yêu cầu, view admin cho dashboard phòng ban, và audit giải thích vì sao quyền tồn tại vào một ngày nhất định.

Bước tiếp theo:

  1. Viết quy tắc quyền bằng ngôn ngữ thường (ai thấy gì và vì sao).
  2. Chọn mô hình phù hợp với các kiểm tra phổ biến nhất (đọc nhanh vs ghi đơn giản).
  3. Xây công cụ admin nội bộ để test tái cơ cấu, yêu cầu truy cập và phê duyệt đầu cuối.

Nếu bạn muốn nhanh tạo các admin panel và cổng nhận biết org, AppMaster (appmaster.io) có thể phù hợp: cho phép bạn mô hình dữ liệu hậu trường trên PostgreSQL, triển khai logic phê duyệt bằng Business Process trực quan, và xuất web lẫn app native từ cùng backend.

Câu hỏi thường gặp

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

Sử dụng adjacency list khi tổ chức nhỏ, cập nhật thường xuyên và hầu hết màn hình chỉ cần báo cáo trực tiếp hoặc vài cấp. Dùng closure table khi bạn liên tục cần “mọi người dưới lãnh đạo X”, lọc theo cây phòng ban, hoặc quyền truy cập dựa trên phân cấp trên nhiều màn hình — vì đọc sẽ trở nên đơn giản hơn bằng join và dễ mở rộng.

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

Bắt đầu với employees(manager_id) và lấy báo cáo trực tiếp bằng truy vấn đơn giản WHERE manager_id = ?. Thêm truy vấn đệ quy chỉ cho các tính năng thật sự cần toàn bộ chuỗi tổ tiên hoặc toàn bộ cây, như phê duyệt, bộ lọc “tổ chức của tôi” hoặc dashboard bỏ qua cấp.

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

Chặn tự quản lý bằng kiểm tra như manager_id <> id, và xác thực cập nhật để không gán một quản lý vốn đã là hậu duệ của người được gán. Thực tế, cách an toàn nhất là kiểm tra tính tổ tiên trước khi lưu thay đổi quản lý, vì một vòng lặp có thể làm hỏng truy vấn đệ quy và logic quyền hạn.

Should departments be nodes in the same hierarchy as people?

Một mặc định tốt là coi phòng ban là nhóm hành chính và đường báo cáo là một cây quản lý riêng. Cách này ngăn việc “di chuyển phòng ban” thay đổi ai đó báo cáo cho ai, và giúp các bộ lọc như “mọi người trong Sales” rõ ràng hơn ngay cả khi đường báo cáo không trùng với ranh giới phòng ban.

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

Thường thì lưu quản lý chính trên bản ghi nhân viên và biểu diễn quan hệ dotted-line (hai quản lý) riêng, chẳng hạn bảng liên kết quản lý phụ hoặc ánh xạ “team lead”. Cách này giữ cho truy vấn cây cơ bản không bị hỏng đồng thời vẫn cho phép các quy tắc đặc biệt như phân quyền dự án hoặc ủy quyền phê duyệt.

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

Xóa các đường dẫn tổ tiên cũ cho subtree của người bị di chuyển, sau đó chèn các đường dẫn mới bằng cách kết hợp các tổ tiên của quản lý mới với mọi node trong subtree, tính lại depth. Thực hiện trong một transaction để tránh đóng bảng closure ở trạng thái nửa cập nhật nếu có lỗi xảy ra giữa chừng.

What indexes matter most for org chart queries?

Với adjacency list, index employees(manager_id) vì hầu hết truy vấn bắt đầu từ đó, và thêm các chỉ mục cho bộ lọc phổ biến như team_id hoặc department_id. Với closure table, chỉ mục quan trọng là khóa chính (ancestor_id, descendant_id) và một chỉ mục riêng trên descendant_id để kiểm tra “ai có thể thấy bản ghi này?” nhanh.

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

Một mẫu phổ biến là dùng EXISTS trên bảng closure: cho phép truy cập khi viewer là một ancestor của employee mục tiêu. Cách này hoạt động tốt với row-level security vì cơ sở dữ liệu có thể áp dụng quy tắc nhất quán, thay vì để mọi endpoint API phải nhớ cùng một logic đệ quy.

How do I handle reorg history and audit trails?

Lưu lịch sử rõ ràng, thường bằng một bảng riêng ghi lại thay đổi quản lý cùng ngày hiệu lực, thay vì ghi đè trường manager hiện tại và mất quá khứ. Cách này cho phép trả lời “ai báo cáo cho ai vào ngày X” mà không phải phỏng đoán, và giữ báo cáo cùng audit nhất quán sau các tái cơ cấu.

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

Giữ manager_id hiện tại làm nguồn dữ liệu, tạo bảng closure song song và backfill từ cây hiện tại. Chuyển đường đọc trước (bộ lọc, dashboard, kiểm tra quyền), rồi làm cho các thao tác ghi cập nhật cả hai, và chỉ bỏ truy vấn đệ quy khi bạn đã xác thực kết quả trùng khớp trong kịch bản thực tế.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu
Mô hình sơ đồ tổ chức trong PostgreSQL: adjacency lists vs closure | AppMaster