template-monorepo/internal/library/mongo/README.md

332 lines
12 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.

# MongoDBDocumentDB + 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 |
| 78 | 業務規則、不直接碰 mongo 包 | `usecase/` |
| 9 | `ServiceContext.MemberUC` 等 | `internal/svc/` |
| 1011 | 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 JSONNoSQL 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` 或 srv 的 host |
| `User` / `Password` | 會經 URL 編碼,勿手拼 URI |
| `Database` | 傳入 `mon.NewModel` 的 db 名稱 |
| `AuthSource` | 查詢參數 `authSource` |
| `ReplicaName` | 查詢參數 `replicaSet` |
| `TLS` | 查詢參數 `tls=true` |
| `MaxPoolSize` / `MinPoolSize` / `MaxConnIdleTime` | client pool |
| `Compressors` | 預設 `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()` 改 DBcache **不會**自動失效。
---
## 介面一覽
- `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`。