332 lines
12 KiB
Markdown
332 lines
12 KiB
Markdown
# 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` 或 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()` 改 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`。
|