# PDM — Physical Data Model — | | | |---|---| | **Version** | 1.0 | | **Date** | YYYY-MM-DD | | **Author** | (skill sa-2-architecture) | | **Status** | 🟡 Draft | | **Approved by** | Tech Lead: — · DBA: — · BE Lead: — | | **Source** | DAT_… v1.0 · DOM_… v1.0 · SRS_… §2.3.3 (bảng field) · BR_… của BA · SEC_… §5 | | **Scope** | *(kho nào — một PDM mỗi kho vật lý, hoặc một file nhiều §2.x)* | | **Confidence** | 🟡 | ## Change Log | Version | Date | Người sửa | Thay đổi | ADR / MIG | |---|---|---|---|---| | 1.0 | | | Bản đầu | `MIG-001` | > **Thẩm quyền:** file DDL trong `02-architecture/schema/` **thắng** bảng ở đây khi lệch — bảng là > bản đọc cho người, DDL là thứ chạy. Lệch ⇒ lỗi tài liệu, sửa bảng. > > **Vì sao có tài liệu này:** `DAT` dừng ở "thực thể, quan hệ, ai sở hữu". Không có `PDM`, mỗi dev > tự đặt kiểu cột, độ dài, nullable, index — và bảng field của BA (`FLD-…`) chặn ở tầng UI nhưng > không chặn ở tầng DB, hoặc ngược lại. --- ## 1. Kho và quy ước | | | |---|---| | **Kho** | *(tên · loại · phiên bản, ví dụ PostgreSQL 16 · RDS)* — quyết định ở `DAT` §3 / `ADR-nnn` | | **Schema / namespace** | *(schema-per-module theo `ADR-nnn`: `identity`, `catalog`, `cart_order`…)* | | **Công cụ migration** | *(Flyway / Liquibase / Prisma Migrate / Alembic / golang-migrate…)* — `ADR-nnn` | | **Đặt tên** | bảng `snake_case` số ít · PK `id` · FK `_id` · index `ix__` · unique `ux_…` · FK constraint `fk__` | | **Kiểu id** | `uuid v7` / `bigint identity` — `ADR-nnn`; **truyền qua API dạng string** (`ICD` §1) | | **Thời gian** | `timestamptz`, lưu UTC; cột `created_at`, `updated_at` bắt buộc mọi bảng | | **Tiền** | `numeric(18,2)` + cột `currency char(3)` hoặc value object cố định VND — theo `DOM` §2 `Money` | | **Xoá** | soft delete (`deleted_at`) cho bảng có yêu cầu retention ở `DAT` §5; hard delete cho bảng còn lại — ghi từng bảng ở §2 | | **Ký tự** | `UTF-8`; độ dài `varchar(n)` tính theo **code point**, khớp bảng field của SRS (W3) | --- ## 2. Bảng — `TBL-nnn` *Một mục mỗi bảng. Mỗi bảng phải truy về một thực thể `DAT` §1 và một class `DOM` §2; bảng không truy được là bảng thừa hoặc thiếu mô hình.* ### 2.1 `TBL-001` · `cart_order.order` ← `Order` (`DAT` §1.3 · `DOM` §2.1) | | | |---|---| | **Chủ sở hữu** | `CMP-04` — chỉ component này được ghi (`DAT` §1) | | **Ước lượng** | … bản ghi/năm 1 · … năm 3 (`DAT` §2.2) · tăng …/tháng | | **Xoá** | soft delete · retention … năm (`DAT` §5) | | **Phân mảnh** | không / theo `placed_at` tháng (`DAT` §7.4 · `ADR-nnn`) | **Cột** | Cột | Kiểu | Null | Default | Ràng buộc | PII (`SEC` §5) | Nguồn (`FLD`/`BR`) | Ghi chú | |---|---|---|---|---|---|---|---| | `id` | `uuid` | ✗ | `gen_uuid_v7()` | PK | — | — | | | `order_number` | `varchar(20)` | ✗ | — | `ux_order_order_number` | — | `BR-010` định dạng | khoá nghiệp vụ | | `customer_id` | `uuid` | ✓ | `NULL` | *(không FK vật lý — khác schema, `DAT` §1)* | 🔶 gián tiếp | `FLD-SCR04-01` | null = Guest | | `total_amount` | `numeric(18,2)` | ✗ | — | `CHECK (total_amount >= 0)` | — | `BR-014` | = Σ `order_seller.subtotal_amount` | | `status` | `varchar(32)` | ✗ | `'pending'` | `CHECK (status IN (…))` theo `BR` §3 | — | `BR-021` | enum ở tầng app, CHECK ở DB | | `idempotency_key` | `varchar(64)` | ✗ | — | `ux_order_idempotency_key` | — | `ADR-004` | khoá của `POST /v1/checkout` | | `placed_at` | `timestamptz` | ✗ | `now()` | | — | | | | `created_at` / `updated_at` | `timestamptz` | ✗ | `now()` | | — | quy ước §1 | | | `deleted_at` | `timestamptz` | ✓ | `NULL` | | — | quy ước §1 | soft delete | 🔴 **Bốn ô không được trống ở bất kỳ cột nào**: kiểu có độ dài/độ chính xác · Null · Default · Nguồn. Trùng nguyên tắc "bảng ràng buộc cụ thể tới mức không cần hỏi lại" của SRS PART 2. **Index và khoá ngoại** | Tên | Loại | Cột | Vì sao (`QAS`/truy vấn `DAT` §7.1) | Kích cỡ ước lượng | |---|---|---|---|---| | `ix_order_customer_placed` | btree | `(customer_id, placed_at DESC)` | lịch sử đơn · `QAS-006` | | | `fk_order_seller_order` | FK | `order_seller.order_id → order.id` | `ON DELETE RESTRICT` | | 🔴 Index không truy về truy vấn nào ⇒ bỏ. Truy vấn trong `DAT` §7.1 không có index ⇒ thiếu. ### 2.2 `TBL-002` · `.` ← `` *(cùng cấu trúc)* --- ## 3. Ánh xạ thực thể → bảng *Bảng đối chiếu bắt buộc, `sa-conformance` đọc bảng này.* | Thực thể (`DAT` §1) | Class (`DOM` §2) | Bảng (`TBL`) | Quan hệ 1 thực thể → n bảng? | Ghi chú | |---|---|---|---|---| | `Order` | `Order` | `TBL-001` | 1→1 | | | `Order` × `Seller` | `OrderSeller` | `TBL-002` | tách theo `ADR-003` | | | `Cart` (Redis) | `Cart` | *(không phải bảng — key schema §5)* | | | Thực thể không có dòng ⇒ 🔴 chặn AG2. Bảng không có thực thể ⇒ bảng thừa hoặc thiếu mô hình. --- ## 4. DDL và migration — `MIG-nnn` **Vị trí:** `sa-output//02-architecture/schema//` ``` schema// ├── V001__init_.sql ← MIG-001: toàn bộ bảng §2, chạy được từ CSDL rỗng ├── V001__init_.down.sql ← rollback tương ứng (bắt buộc) ├── V002__.sql ← mỗi thay đổi sau baseline một cặp up/down ├── V002__.down.sql └── seed/ └── R__reference_data.sql ← dữ liệu tham chiếu (enum bảng, cấu hình) — idempotent ``` | `MIG` | File | Nội dung | Rollback | Dữ liệu phát sinh trong lúc chạy | Thời lượng ước lượng | Chạy trên | |---|---|---|---|---|---|---| | `MIG-001` | `V001__init_cart_order.sql` | tạo `TBL-001..0nn` | `V001__….down.sql` drop ngược thứ tự FK | không (CSDL rỗng) | < 1 phút | dev/stg/prod | | `MIG-002` | | | | | | | 🔴 **Migration không có down là migration một chiều** (`DAT` §8). Với migration đổi dữ liệu (không chỉ DDL) phải ghi thêm: cách đối chiếu sau khi chạy, ngưỡng chênh lệch chấp nhận, ai duyệt. **Quy ước file SQL:** một câu lệnh một dòng logic · comment đầu file ghi `MIG-nnn`, `TBL` liên quan, `ADR` · không dùng lệnh phụ thuộc quyền superuser · `CREATE … IF NOT EXISTS` chỉ trong seed. **Kiểm bằng máy** *(người điều phối chạy, ghi kết quả vào đây)*: | Kiểm | Lệnh | Kết quả | |---|---|---| | Up từ rỗng | ` migrate` trên container CSDL rỗng | ☐ | | Down toàn bộ rồi up lại | ` migrate down --all && migrate` | ☐ | | Mọi bảng §2 có trong DDL | `grep -c "CREATE TABLE" V001*.sql` = số `TBL` | ☐ | | Mọi cột PII có cờ ở `SEC` §5 | đối chiếu cột 🔶/✅ PII với `SEC` §5 | ☐ | --- ## 5. Kho không quan hệ *(nếu có — cache, document, search)* | Kho | Key / collection | Cấu trúc value | TTL | Invalidation (`DAT` §7.4) | Kích cỡ ước lượng | |---|---|---|---|---|---| | Redis `CMP-15` | `cart:{sessionId}` | JSON `CartSnapshot` (`DOM` §2) | 7 ngày | write-through khi `PATCH`/`DELETE` | ≤ 50 dòng × … | --- ## 6. Dữ liệu tham chiếu và seed | Bảng | Nguồn dữ liệu | Ai duy trì | Cách nạp | Idempotent | |---|---|---|---|---| --- ## 7. Đối chiếu | Kiểm | Kết quả | Hành động | |---|---|---| | `DAT` thực thể → `TBL` (§3) | n/n | | | `FLD-*` trong SRS PART 2 → cột (kiểu/độ dài/null khớp) | n/n | lệch ⇒ `OQ`, BA cập nhật SRS hoặc PDM sửa | | Cột PII → `SEC` §5 có dòng | n/n | | | Truy vấn `DAT` §7.1 → index | n/n | | | Mọi `MIG` có down | n/n | | ## 8. Giả định & Ngoài phạm vi **Giả định:** | ID | Giả định | Cách xác minh | Nếu sai | |---|---|---|---| **Ngoài phạm vi:** - *(ví dụ: không thiết kế kho phân tích/BI — ngoài `CON-…`)* ## 9. Open Questions | ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược | |---|---|---|---|---|---|