# 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`。