Kiến trúc · Quan hệ module · 07/7/2026

Quan hệ giữa các module & join dữ liệu chéo module

Trả lời câu hỏi: action cần join dữ liệu giữa bảng của nhiều module thì thực hiện thế nào? Phương án rút từ ràng buộc thực của OpenGate + đối chiếu Odoo · ERPNext · Lark. Bản chi tiết: markdown.

0

Bối cảnh OpenGate quyết định lời giải

Đặc điểm OpenGateHệ quả
Mỗi workspace = 1 DB Postgres chung cho mọi moduleJOIN chéo module khả thi về kỹ thuật — vấn đề là kỷ luật, không phải công nghệ
Module cài/gỡ độc lập, không có ORM registry chung kiểu OdooKhông join "tự do" — bảng module khác có thể vắng mặt hoặc đổi schema
Manifest v2 có depends + scopes ('module:feature:readonly')Đã có chỗ khai báo hợp thức quyền đọc chéo — chỉ thiếu quy ước thực thi
Đã chốt: không FK cứng xuyên module — khóa nghiệp vụ (ma_kh, ma_gv, ma_xe, ma_dk…)Join theo khóa nghiệp vụ, không theo id nội bộ
1

Bản đồ quan hệ & 5 loại quan hệ

┌────────── catalog (danh mục — mọi module depends) ──────────┐ ▼ ▼ ▼ ▼ ▼ crm ─convert─▶ student ◀─ma_kh── course teacher vehicle ──seri_dat──▶ dat ▲ ▲ ▲ ▲ ▲ ▲ ▲ │ (ma_dk,ma_kh,ma_csdt) │ └─ma_gv───┘ └ma_xe──┘ │ │ │ │ (enrollment — phân công) │ tuition ────────┘ │ │ │ │ callback: da_thu_dot1 │◀──────── callback: da_bao_so ───────────│ ▼ │ ▼ finance (v1.5) report ◀── đọc chéo: student+course+dat (đối soát) hrm.timesheet-dat (built-in) ◀── giờ dạy hợp lệ (ma_gv) ──────────────────────┘ portal / bi / notify ◀── đọc chéo & nhận yêu cầu gửi từ mọi module
#Loại quan hệVí dụTính chất
Q1Tham chiếu danh mụcmọi module → catalog (hạng, checklist)đọc, rất ổn định
Q2Tham chiếu khóa nghiệp vụstudent.ma_kh → course · vehicle.seri_dat → datđọc, enrich khi hiển thị
Q3Consumer đọc rộngreport, bi, payroll đọc nhiều moduleđọc, khối lượng lớn, theo kỳ
Q4Callback trạng tháituition → student (da_thu_dot1) · report → course (da_bao_so)ghi — phải qua rule nghiệp vụ
Q5Sự kiện / thông báodat.alert → notifyghi một chiều, fire-and-forget
2

Học gì từ Odoo · ERPNext · Lark

HệCách join chéo moduleĐáng họcRủi ro nếu bê nguyên
Odoo
1 DB + 1 ORM registry
Module load vào registry chung → ORM join tự do; depends xếp thứ tự; tích hợp 2 module optional bằng bridge module (vd sale_stock) Bridge module ② đồ thị depends tường minh Join tự do nhờ registry + loader — OpenGate không có → module vỡ ngầm khi module khác nâng version
ERPNext
Frappe · DocType · 1 DB
Link field (tham chiếu theo tên ≈ khóa nghiệp vụ) + fetch_from (copy giá trị lúc lưu); phản ứng chéo qua hooks/doc_events; báo cáo = Query Report read-only ① fetch_from = denorm có chủ đích ② hooks = callback chuẩn hóa ③ báo cáo tách kênh read-only Denorm tràn lan không đánh dấu → không biết cột nào là snapshot
Lark
multi-app, không chung DB
Mọi truy cập chéo qua Open API + scopes cấp cho từng app + event subscription; eventual consistency ① scopes khai báo — khớp field OpenGate đã có ② event một chiều ③ app vắng không làm app khác chết Ép mọi thứ qua API trong khi chung 1 DB → N+1, chậm không cần thiết
Kết luận: OpenGate chung DB như Odoo nhưng vòng đời module rời như Lark → phương án tối ưu là lai: join trong DB cho đọc (Odoo) nhưng qua interface có hợp đồng (tinh thần scopes của Lark); copy-tại-thời-điểm & hooks như ERPNext cho dữ liệu phát sinh và ghi chéo.
3

6 pattern theo loại action

#PatternDùng choCảm hứng
P1Contract view — module chủ publish view read-only <key>_v_*; consumer JOIN vào view, không vào bảng gốc; khai scopes trong manifestQ1, Q2 — enrich list, lookup danh mụcOdoo + Lark
P2Copy tại thời điểm phát sinh — ghi kèm giá trị tham chiếu vào bản ghi sự kiện, không sync ngượcdat_phien_hoc giữ ma_gv/ma_xe lúc phiên chạy; phiếu thu giữ số tiền lúc thuERPNext fetch_from
P3Service API + callback — ghi chéo chỉ qua API module chủ; transition chạy rule + ghi lịch sử tại module chủQ4: tuition→student · report→courseERPNext hooks / Lark API
P4Event một chiều (outbox nhẹ) — bắn sự kiện, không chờ; retry hàng đợiQ5: cảnh báo/nhắc hạn → notifyLark event
P5Bridge module — tính năng cần 2 module optional đặt ở module thứ 3 depends cả haihrm.timesheet-dat = bridge feature (dat + teacher + lms); chip×bookingOdoo bridge
P6Snapshot / materialized view — đọc rộng theo kỳ: chụp lúc lập (report) hoặc MV refresh định kỳ (bi)Q3: báo cáo Sở, BIERPNext Query Report

▸ Cây quyết định nhanh

Action cần dữ liệu module khác? ├─ Chỉ ĐỌC? │ ├─ Hiển thị/enrich list, lookup → P1 contract view (JOIN 1 query) │ ├─ Giá trị gắn với sự kiện quá khứ → P2 copy tại thời điểm (không join lại) │ └─ Đọc rộng theo kỳ (báo cáo/BI) → P6 snapshot / materialized view ├─ Phải GHI / đổi trạng thái module khác → P3 service API + callback (cấm SQL trực tiếp) ├─ Chỉ thông báo cho nơi khác biết → P4 event một chiều └─ Tính năng nối 2 module optional → P5 bridge module
4

Quy tắc cứng

Đọc

  • Không FK cứng xuyên module — khóa nghiệp vụ
  • Không JOIN bảng gốc module khác — chỉ contract view <key>_v_* (publish trong migration module chủ)
  • View = public interface, version như API: thêm cột OK; đổi/bỏ = view mới _v2
  • Contract view chỉ chứa cột định danh + nhãn hiển thị (mã, tên, trạng thái) — dữ liệu nhạy cảm (tiền, lương, scan giấy tờ) không bao giờ vào view; đọc phải qua service API check quyền feature nguồn (vì enrich P1 đi vòng qua quyền: người xem không cần quyền module nguồn — bổ sung 20/7, xem Sơ đồ §0)
  • catalog = stable tier — đọc trực tiếp view; danh mục chỉ thêm bản hiệu lực mới

Ghi

  • Cấm INSERT/UPDATE bảng module khác — luôn qua service API để rule + lịch sử không bị bỏ qua
  • Không distributed transaction — callback lỗi → outbox retry + job đối soát định kỳ
  • Denormalize phải có chủ đích, đánh dấu rõ là snapshot (tên *_luc_<sự kiện> hoặc comment migration; bài học cột ten_kh legacy)

Khai báo & degrade

  • Manifest: depends (cài đặt) + scopes ('student:hoso:readonly') cho đọc chéo
  • Module nguồn vắng mặt → feature degrade (ẩn cột/tab), không crash; check khi mount
5

Áp vào action cụ thể của hệ

ActionPatternThực hiện
List học viên kèm tên khóa, GVP1JOIN course_v_khoa_hoc + teacher_v_giao_vien; manifest khai scopes: ['course:course:readonly','teacher:teacher:readonly']
Đối soát km/giờ DATP1+P2Chuẩn đọc catalog_v_hang_gplx; phân công đọc course_v_enrollment; phiên giữ nguyên ma_gv/ma_xe lúc chạy — lệch → hop_le=false, không "sửa quá khứ"
Thu đủ đợt 1 → da_thu_dot1P3tuition ghi phiếu (bảng mình) → gọi API student transition; student ghi lịch sử. Lỗi → outbox retry + job đối soát "đã thu mà chưa chuyển trạng thái"
Sinh báo cáo Sở XDP6+P3generate = query view các module, snapshot vào file immutable; sau da_gui → callback course "đã báo Sở"
Lương GV theo giờ DATP5+P1hrm.timesheet-dat (built-in, bridge feature depends dat+teacher+lms); đọc dat_v_phien_hop_le group theo ma_gv → hrm.payroll
App học viên xem tiến độP1portal đọc dat_v_doi_soat qua service + record-scope (ma_dk ∈ current_ma_dk_list)
Cảnh báo thiếu km/giờ → ZaloP4dat.alert bắn event vào notify (outbox); notify chọn kênh + log gửi — dat không biết Zalo là gì
BI dashboardP6Materialized view refresh theo lịch; drill-down deep-link sang module chủ
6

Việc cần làm ở platform

Quy ước & SDK

  • Tên view <key>_v_<tên> · CREATE OR REPLACE VIEW trong migration module chủ
  • SDK helper ctx.views.exists(...) cho degrade
  • Enforce scopes khi đọc chéo (hiện mới validate hình dạng)
  • Outbox nhẹ: bảng <key>_outbox + worker retry trong host — đủ cho P3/P4, chưa cần broker

Contract view đợt đầu (MVP)

  • catalog_v_hang_gplx
  • course_v_khoa_hoc · course_v_enrollment
  • teacher_v_giao_vien · vehicle_v_xe
  • dat_v_phien_hop_le · dat_v_doi_soat
  • dat_v_thiet_bi_theo_xe (20/7 — vehicle đọc để xét điều kiện dạy sau khi binding chuyển về dat)
  • student_v_hoso (lọc theo scope caller ở tầng service)

Tài liệu

  • Ghi quy tắc mục 4 vào HUONG_DAN.md của ws_suns (gộp với việc còn mở trong review module §4)
7

Cập nhật 08/7 — built-in thay module kế hoạch

Sau khảo sát 7 built-in module của WS (review core §1b), bản đồ quan hệ đổi như sau:

TrướcNay
Module mới hr, payroll (v1.5)Bỏhrm built-in + feature timesheet-dat (bridge: dat+teacher+lms)
Module mới crm (v1.5)Bỏcrm built-in + feature convert (lead→hồ sơ, P3 callback) + commission (mốc da_thu_dot1)
Danh mục phòng ban/chức danh dự kiến tự tạoorg built-in — nguồn duy nhất; student.phong_phu_trach + data-scope current_dept_id đọc từ đây
GV riêng lẻ trong teacherteacher ↔ hrm.Employee 1-1 (mã NV) — teacher giữ hồ sơ nghiệp vụ dạy
Adapter DAT/HĐĐT/payment/Zalo nằm trong từng moduleworkflow.connector — két adapter + credential một cửa (áp dụng P4 outbox khi gọi)
v1.5 = 7 module mớiv1.5 = 4 module mới (lms, finance, fleet, portal) + 3 mở rộng built-in · workplace disabled ở Vĩnh An

Các pattern P1–P6 và quy tắc mục 4 không đổi — built-in tuân cùng quy ước (contract view hrm_v_*, org_v_*; ghi chéo qua service API).