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

12 KiB
Raw Permalink Blame History

MongoDBDocumentDB + Cache

Gateway 的 MongoDB 存取層:在 go-zero mon 之上封裝 collection 操作,並整合 Redis cache-aside。業務模組(internal/model/{module}/repository)應透過本包存取 DB而不是直接 new driver client。

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


流程

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
78 業務規則、不直接碰 mongo 包 usecase/
9 ServiceContext.MemberUC internal/svc/
1011 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 drivermon 使用) 現行維護版本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 轉換) 本包只提供 ErrNotFoundHTTP 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 InitMongoOptionsSetCustomDecimalTypemon.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 ErrNotFoundsingleFlight、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 keyinternal/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. NewXxxRepositoryMustDocumentDBWithCache(啟動失敗 panic)。
  3. 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
}
  1. 錯誤在 usecase 轉成 gateway/internal/library/errors(見 errors README)。
  2. 啟動或 migration 呼叫 PopulateIndex / IndexUP(回傳 error 要處理)。

詳見 docs/model.md 第 4、11 節。

4. UseCase 與 Logic

  • UseCase 只依賴 repository interface不 import 本包。
  • Logic 只呼叫 usecasetypes ↔ DTO 映射。

5. 新增對外 API可選

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

設定說明(Conf

欄位 說明
Schema mongodbmongodb+srvAtlas
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"]);勿寫單一字串。省略時程式預設 zstdsnappy
ConnectTimeoutMs 啟動 Ping 逾時(預設 10s

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


Cache 語意(必讀)

方法 行為
FindOne cache.TakeCtxmiss 時查 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 應使用此介面
  • CacheUseCaseDelCache / GetCache / SetCache(皆帶 context

ErrNotFoundmon.ErrNotFound / mongo.ErrNoDocuments 相同,供 repository 判斷「無資料」。


測試

類型 內容 何時跑
單元 uri_test.goURI 編碼、遮蔽) 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/mongov1請改為 v2 路徑:go.mongodb.org/mongo-driver/v2/mongo