# MongoDB（DocumentDB + Cache）

Gateway 的 **MongoDB 存取層**：在 [go-zero `mon`](https://go-zero.dev/docs/tutorials/mongo/mongo-connections) 之上封裝 collection 操作，並整合 **Redis cache-aside**。業務模組（`internal/model/{module}/repository`）應透過本包存取 DB，而不是直接 new driver client。

更上層的分層與 API 流程見：

- [專案 README](../../README.md) — HTTP、錯誤碼、驗證、目錄總覽
- [docs/model.md](../../docs/model.md) — entity / repository / usecase 規範

---

## 流程

### 1. 執行期：一筆 API 怎麼走到 Mongo / Redis

```mermaid
sequenceDiagram
    participant C as Client
    participant H as handler
    participant L as logic
    participant UC as usecase
    participant R as repository
    participant M as library/mongo
    participant Redis as Redis
    participant Mongo as MongoDB

    C->>H: HTTP
    H->>H: httpx.Parse + ValidateAll
    H->>L: types req
    L->>UC: usecase DTO
    UC->>R: 業務方法
  alt 讀取 FindOne
    R->>M: FindOne(ctx, cacheKey, doc, filter)
    M->>Redis: TakeCtx 查 cache
    alt cache hit
      Redis-->>M: doc
    else cache miss
      M->>Mongo: FindOne
      Mongo-->>M: doc
      M->>Redis: 寫入 cache
    end
    M-->>R: err / nil
  else 寫入 Update/Insert/Delete
    R->>M: Update*/Insert*/Delete*
    M->>Mongo: 寫入
    M->>Redis: DelCache(key) 失效
  end
    R-->>UC: entity / error
    UC-->>L: DTO / *errs.Error
    L-->>H: types data / err
    H-->>C: Status JSON
```

### 2. 開發期：新功能從零到上線（建議順序）

```mermaid
flowchart TD
    A[1. 定義 entity + CollectionName] --> B[2. redis.go 定義 cache key]
    B --> C[3. repository interface]
    C --> D[4. NewRepository: MustDocumentDBWithCache]
    D --> E[5. 實作 CRUD 呼叫本包]
    E --> F[6. PopulateIndex / IndexUP]
    F --> G[7. usecase interface + DTO]
    G --> H[8. usecase 實作 + 錯誤轉 errs]
    H --> I[9. svc 組裝注入 UseCase]
    I --> J[10. generate/api + make gen-api]
    J --> K[11. logic: types 映射]
    K --> L[12. make check]
```

| 步驟 | 做什麼 | 在哪裡 |
|:----:|--------|--------|
| 1 | Document struct、`CollectionName()` | `internal/model/{module}/entity/` |
| 2 | `GetXxxRedisKey()`，與 `FindOne` 的 key 一致 | `internal/model/{module}/redis.go` |
| 3 | `XxxRepository` interface | `internal/model/{module}/repository/` |
| 4 | `mongo.Conf` + `cache.CacheConf` + `mon.Option` | `svc` 或 repository `New*` |
| 5 | `FindOne` / `InsertOne` / … | 同 repository 實作檔 |
| 6 | 建立索引（失敗要處理 error） | 啟動或 migration |
| 7–8 | 業務規則、不直接碰 mongo 包 | `usecase/` |
| 9 | `ServiceContext.MemberUC` 等 | `internal/svc/` |
| 10–11 | HTTP 契約與編排 | `generate/api/`、`internal/logic/` |
| 12 | fmt + lint + test | `make check` |

### 3. 啟動期：連線與健康檢查

```mermaid
flowchart LR
    A[NewDocumentDB / MustDocumentDBWithCache] --> B[buildConnectionURI]
    B --> C[mon.NewModel]
    C --> D[InitMongoOptions + 可選 SetCustomDecimalType]
    D --> E[Ping Primary]
    E --> F{成功?}
    F -->|是| G[注入 repository]
    F -->|否| H[啟動失敗 error]
```

### 4. Cache 讀寫（本包內部）

```mermaid
flowchart TD
    subgraph 讀取 FindOne
        R1[TakeCtx] --> R2{Redis 有 key?}
        R2 -->|有| R3[回傳快取]
        R2 -->|無| R4[Mongo FindOne]
        R4 --> R5[寫入 Redis]
    end
    subgraph 寫入 Insert/Update/Delete
        W1[Mongo 寫入] --> W2[DelCache keys]
        W2 --> W3{Redis 刪除失敗?}
        W3 -->|是| W4[只 log，仍回成功]
        W3 -->|否| W5[完成]
    end
```

---

## 為什麼需要這一層？

| 問題 | 本包做法 |
|------|----------|
| 各服務自己拼 Mongo URI、各自 cache | 統一 `Conf` + `buildConnectionURI`（密碼 URL 編碼、TLS/query） |
| go-zero 只提供 `mon.Model`，沒有「依 cache key 讀寫」 | `DocumentDBWithCache`：寫入後失效、讀取走 `TakeCtx` + SingleFlight |
| `shopspring/decimal` 與 BSON Decimal128 不一致 | `SetCustomDecimalType()` 註冊 codec |
| 索引建立散落、失敗只 log | `PopulateIndex*` 回傳 `error`，由啟動或 migration 處理 |
| driver v1 / v2 與 go-zero 1.10 不一致 | 全專案對齊 **mongo-driver v2**（與 go-zero 1.10 `mon` 相同） |

---

## 用到的技術與原因

| 元件 | 用途 | 為什麼選它 |
|------|------|------------|
| **[go-zero `mon`](https://github.com/zeromicro/go-zero/tree/master/core/stores/mon)** | Collection CRUD、breaker、慢查詢 log | 與 Gateway 同框架，連線共用、可觀測 |
| **[mongo-driver v2](https://www.mongodb.com/docs/drivers/go/current/)** | 官方 Go driver（由 `mon` 使用） | 現行維護版本；go-zero 1.10 已遷移 v2 |
| **[go-zero `cache`](https://github.com/zeromicro/go-zero/tree/master/core/stores/cache)** | Redis cache-aside、`TakeCtx`、SingleFlight | 與 go-zero 生態一致；`ErrNotFound` 不當成 cache 穿透錯誤 |
| **[shopspring/decimal](https://github.com/shopspring/decimal)** | 業務金額精度 | 避免 `float64`；透過自訂 codec 存成 Decimal128 |
| **`gateway/internal/library/errors`** | 對外 API 錯誤（在 repository 轉換） | 本包只提供 `ErrNotFound`；HTTP 8 碼在上一層處理 |

---

## 在整體架構中的位置

與上方 **§1 執行期流程** 相同；本包只出現在 **repository** 層。

**禁止：**

- `logic` / `handler` 直接 import 本包或操作 collection
- repository 繞過 `DocumentDBWithCache` 改 DB 卻不處理 cache
- 在業務 struct 上用 `bson.M` 直接吃未驗證的 HTTP JSON（NoSQL operator injection）

---

## 目錄與職責

| 檔案 | 職責 |
|------|------|
| `config.go` | `Conf`：連線、pool、壓縮、逾時 |
| `uri.go` | 組裝 / 遮蔽連線 URI |
| `option.go` | `InitMongoOptions`、`SetCustomDecimalType`（`mon.Option`） |
| `doc-db.go` | `NewDocumentDB`、Ping、建立索引 |
| `doc-db-with-cache.go` | 帶 cache 的 CRUD 封裝 |
| `usecase.go` | `DocumentDBUseCase` / `DocumentDBWithCacheUseCase` 介面 |
| `custom_mongo_decimal.go` | Decimal ↔ Decimal128 codec |
| `const.go` | `ErrNotFound`、`singleFlight`、cache stat |
| `uri_test.go` | URI 單元測試 |

---

## 開發步驟詳解

以下對應 **§2 開發期流程** 各步驟的程式範例。

### 1. 設定與組裝（`internal/svc`）

在 `ServiceContext` 建立各 collection 的 DB 實例（或 repository 建構時建立）：

```go
import (
    "gateway/internal/library/mongo"
    "github.com/zeromicro/go-zero/core/stores/cache"
    "github.com/zeromicro/go-zero/core/stores/mon"
)

mongoConf := &mongo.Conf{
    Schema:     "mongodb",
    Host:       "127.0.0.1:27017",
    Database:   "gateway",
    User:       "app",
    Password:   "secret",
    AuthSource: "admin",
}

dbOpts := []mon.Option{
    mongo.InitMongoOptions(*mongoConf),
    mongo.SetCustomDecimalType(), // 有 decimal 欄位才需要
}

accountDB, err := mongo.MustDocumentDBWithCache(
    mongoConf,
    "account",           // collection 名稱，與 entity.CollectionName() 一致
    cache.CacheConf{ /* Host, Pass, ... */ },
    dbOpts,
    nil,
)
```

**為什麼在 svc / repository 建構？** 連線與 cache 屬基礎設施，生命週期應與 process 相同；業務 usecase 只依賴 interface。

### 2. Entity 與 Redis key（`internal/model/{module}/`）

1. 在 `entity/` 定義 document + `CollectionName()`。
2. 在 `redis.go` 定義 cache key，例如 `GetAccountRedisKey(id)`。
3. key 規則要與 **讀取** `FindOne(ctx, key, ...)` 的 `key` 參數一致。

### 3. Repository 實作（`repository/`）

1. struct 持有 `mongo.DocumentDBWithCacheUseCase`。
2. `NewXxxRepository` 內 `MustDocumentDBWithCache`（啟動失敗 `panic`）。
3. CRUD 範例：

```go
func (r *accountRepository) FindOne(ctx context.Context, id string) (*entity.Account, error) {
    var doc entity.Account
    filter := bson.M{"_id": oid}
    err := r.DB.FindOne(ctx, member.GetAccountRedisKey(id), &doc, filter)
    if err != nil {
        if errors.Is(err, mongo.ErrNotFound) {
            return nil, member.ErrNotFound
        }
        return nil, err
    }
    return &doc, nil
}
```

4. 錯誤在 usecase 轉成 `gateway/internal/library/errors`（見 [errors README](../errors/README.md)）。
5. 啟動或 migration 呼叫 `PopulateIndex` / `IndexUP`（回傳 error 要處理）。

詳見 [docs/model.md](../../docs/model.md) 第 4、11 節。

### 4. UseCase 與 Logic

- UseCase 只依賴 `repository` interface，不 import 本包。
- Logic 只呼叫 usecase，做 `types` ↔ DTO 映射。

### 5. 新增對外 API（可選）

1. `generate/api/*.api` → `make gen-api`
2. logic 實作 → `make check`

---

## 設定說明（`Conf`）

| 欄位 | 說明 |
|------|------|
| `Schema` | `mongodb` 或 `mongodb+srv`（Atlas） |
| `Host` | `host:port`；也可只寫 host 並用 `Port`（整數） |
| `Port` | 選用；`Host` 未含埠時會組成 `host:port` |
| `User` / `Password` | 會經 URL 編碼，勿手拼 URI |
| `Database` | 傳入 `mon.NewModel` 的 db 名稱 |
| `AuthSource` | 查詢參數 `authSource` |
| `ReplicaName` | 查詢參數 `replicaSet` |
| `TLS` | 查詢參數 `tls=true` |
| `MaxPoolSize` / `MinPoolSize` / `MaxConnIdleTime` | client pool |
| `Compressors` | 選用 YAML **陣列**（`["zstd","snappy"]`）；勿寫單一字串。省略時程式預設 `zstd`、`snappy` |
| `ConnectTimeoutMs` | 啟動 Ping 逾時（預設 10s） |

尚未接到 `etc/gateway.yaml` 時，可在 `ServiceContext` 從環境變數或本地 yaml 填入 `Conf`。

---

## Cache 語意（必讀）

| 方法 | 行為 |
|------|------|
| `FindOne` | `cache.TakeCtx`：miss 時查 Mongo，並寫入 cache |
| `InsertOne` / `Update*` / `DeleteOne` | DB 成功後 **刪除** 傳入的 cache key(s) |
| `FindOneAndDelete` / `FindOneAndReplace` | DB 成功後刪除 key |

**寫入後刪 cache 失敗**：只記 error log，**不回傳錯誤**（避免 DB 已寫入卻對外報錯）；可能短暫讀到舊快取，需接受或改為強一致策略。

**未封裝的操作**：`Find`、列表、aggregate 沒有 cache；若直接 `GetClient()` 改 DB，cache **不會**自動失效。

---

## 介面一覽

- `DocumentDBUseCase`：連線、索引、`GetClient() *mon.Model`（進階或繞過 cache 時慎用）
- `DocumentDBWithCacheUseCase`：業務 repository 應使用此介面
- `CacheUseCase`：`DelCache` / `GetCache` / `SetCache`（皆帶 `context`）

`ErrNotFound` 與 `mon.ErrNotFound` / `mongo.ErrNoDocuments` 相同，供 repository 判斷「無資料」。

---

## 測試

| 類型 | 內容 | 何時跑 |
|------|------|--------|
| 單元 | `uri_test.go`（URI 編碼、遮蔽） | `make test` / `make check` |
| 建議補 | `custom_mongo_decimal` 往返測試 | 有金額欄位時 |
| 整合 | testcontainers + miniredis，測 repository CRUD + cache | 第一個 repository 完成時；可標 `//go:build integration` |

本包 **不強制** testcontainers；**業務 repository 的整合測試** 才是上線前主要門禁。

---

## 維運注意

- 啟動時 Ping **Primary**；讀取若要走 secondary 請在 URI / client 設定 read preference。
- 多 collection = 多次 `MustDocumentDBWithCache`（每個 collection 一個實例）。
- 程序關閉：目前未封裝 `Disconnect`；依 go-zero client manager 生命週期，必要時可在 shutdown hook 補強。
- Health API 建議另做 Mongo Ping（給 K8s probe），與啟動 Ping 分開。

---

## 常用指令

```bash
# 在專案根目錄
make test ./internal/library/mongo/...
make check   # 改 Go 後建議全跑
```

---

## 版本與遷移備註

- 2023 舊版曾使用獨立 module `library-go/mongo` + driver **v1**；已併入 `gateway` 並升級 **v2**。
- `SetCustomDecimalType` 使用 go-zero 內建 `mon.WithTypeCodec`，勿再維護重複的 registry 註冊邏輯。
- Repository 介面若仍 import `go.mongodb.org/mongo-driver/mongo`（v1），請改為 **v2** 路徑：`go.mongodb.org/mongo-driver/v2/mongo`。
