Files
sys-analysis-design/.claude/skills/sa-2-architecture/templates/physical-data-model.md
2026-09-22 13:46:36 +07:00

181 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PDM — Physical Data Model — <PROJECT>
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | YYYY-MM-DD |
| **Author** | <SA> (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 `<bảng>_id` · index `ix_<bảng>_<cột>` · unique `ux_…` · FK constraint `fk_<bảng>_<cột>` |
| **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` · `<schema>.<bảng>` ← `<Entity>`
*(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/<PROJECT>/02-architecture/schema/<kho>/`
```
schema/<kho>/
├── V001__init_<schema>.sql ← MIG-001: toàn bộ bảng §2, chạy được từ CSDL rỗng
├── V001__init_<schema>.down.sql ← rollback tương ứng (bắt buộc)
├── V002__<thay-doi>.sql ← mỗi thay đổi sau baseline một cặp up/down
├── V002__<thay-doi>.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 | `<tool> migrate` trên container CSDL rỗng | ☐ |
| Down toàn bộ rồi up lại | `<tool> migrate down --all && <tool> 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 |
|---|---|---|---|---|---|