|
|
||
|---|---|---|
| .. | ||
| README.md | ||
| config.go | ||
| const.go | ||
| custom_mongo_decimal.go | ||
| doc-db-with-cache.go | ||
| doc-db.go | ||
| option.go | ||
| uri.go | ||
| uri_test.go | ||
| usecase.go | ||
README.md
MongoDB(DocumentDB + Cache)
Gateway 的 MongoDB 存取層:在 go-zero mon 之上封裝 collection 操作,並整合 Redis cache-aside。業務模組(internal/model/{module}/repository)應透過本包存取 DB,而不是直接 new driver client。
更上層的分層與 API 流程見:
- 專案 README — HTTP、錯誤碼、驗證、目錄總覽
- docs/model.md — entity / repository / usecase 規範
流程
1. 執行期:一筆 API 怎麼走到 Mongo / Redis
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. 開發期:新功能從零到上線(建議順序)
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. 啟動期:連線與健康檢查
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 讀寫(本包內部)
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 |
Collection CRUD、breaker、慢查詢 log | 與 Gateway 同框架,連線共用、可觀測 |
| mongo-driver v2 | 官方 Go driver(由 mon 使用) |
現行維護版本;go-zero 1.10 已遷移 v2 |
go-zero cache |
Redis cache-aside、TakeCtx、SingleFlight |
與 go-zero 生態一致;ErrNotFound 不當成 cache 穿透錯誤 |
| 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 建構時建立):
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}/)
- 在
entity/定義 document +CollectionName()。 - 在
redis.go定義 cache key,例如GetAccountRedisKey(id)。 - key 規則要與 讀取
FindOne(ctx, key, ...)的key參數一致。
3. Repository 實作(repository/)
- struct 持有
mongo.DocumentDBWithCacheUseCase。 NewXxxRepository內MustDocumentDBWithCache(啟動失敗panic)。- CRUD 範例:
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
}
- 錯誤在 usecase 轉成
gateway/internal/library/errors(見 errors README)。 - 啟動或 migration 呼叫
PopulateIndex/IndexUP(回傳 error 要處理)。
詳見 docs/model.md 第 4、11 節。
4. UseCase 與 Logic
- UseCase 只依賴
repositoryinterface,不 import 本包。 - Logic 只呼叫 usecase,做
types↔ DTO 映射。
5. 新增對外 API(可選)
generate/api/*.api→make gen-api- 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 分開。
常用指令
# 在專案根目錄
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。