add lib mongo
This commit is contained in:
parent
fb5ac4b09f
commit
04077a0fcb
|
|
@ -100,6 +100,7 @@ gateway/
|
||||||
│ ├── config/ # go-zero RestConf 等服務設定
|
│ ├── config/ # go-zero RestConf 等服務設定
|
||||||
│ ├── library/errors/ # 全專案唯一結構化錯誤(8 碼 SSCCCDDD)
|
│ ├── library/errors/ # 全專案唯一結構化錯誤(8 碼 SSCCCDDD)
|
||||||
│ ├── library/validate/ # struct 驗證(go-playground/validator + 翻譯)
|
│ ├── library/validate/ # struct 驗證(go-playground/validator + 翻譯)
|
||||||
|
│ ├── library/mongo/ # DocumentDB + Redis cache(mongo-driver v2)
|
||||||
│ ├── library/errlog/ # 可選 slog 日誌輔助
|
│ ├── library/errlog/ # 可選 slog 日誌輔助
|
||||||
│ └── model/ # 業務模型根目錄(見下方)
|
│ └── model/ # 業務模型根目錄(見下方)
|
||||||
│ └── {module}/ # 例如 member/、order/
|
│ └── {module}/ # 例如 member/、order/
|
||||||
|
|
@ -157,6 +158,7 @@ HTTP Request
|
||||||
- [generate/api/README.md](generate/api/README.md) — `.api` 與 `@respdoc` 約定
|
- [generate/api/README.md](generate/api/README.md) — `.api` 與 `@respdoc` 約定
|
||||||
- [internal/response/README.md](internal/response/README.md) — Handler / Logic 分工
|
- [internal/response/README.md](internal/response/README.md) — Handler / Logic 分工
|
||||||
- [internal/library/errors/README.md](internal/library/errors/README.md) — 錯誤碼與 HTTP 對照
|
- [internal/library/errors/README.md](internal/library/errors/README.md) — 錯誤碼與 HTTP 對照
|
||||||
|
- [internal/library/mongo/README.md](internal/library/mongo/README.md) — MongoDB / Redis cache 流程與用法
|
||||||
- [docs/model.md](docs/model.md) — `internal/model/{module}` 分層(entity / repository / usecase)
|
- [docs/model.md](docs/model.md) — `internal/model/{module}` 分層(entity / repository / usecase)
|
||||||
|
|
||||||
## 開發約定
|
## 開發約定
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ func (p Platform) ToString() string { /* map lookup */ }
|
||||||
- 方法第一個參數固定為 `context.Context`。
|
- 方法第一個參數固定為 `context.Context`。
|
||||||
- 參數 / 回傳值使用 `entity` 型別,不暴露 driver 細節(除 index migration 等必要場景)。
|
- 參數 / 回傳值使用 `entity` 型別,不暴露 driver 細節(除 index migration 等必要場景)。
|
||||||
- Index migration 以獨立 interface 嵌入,命名 `{Entity}IndexUP`,方法名含版本號,如 `Index20241226001UP`。
|
- Index migration 以獨立 interface 嵌入,命名 `{Entity}IndexUP`,方法名含版本號,如 `Index20241226001UP`。
|
||||||
- 介面檔案不含實作、不含 import 基礎設施 package(`mon`、`mgo` 等僅在實作檔出現)。
|
- 介面檔案不含實作、不含 import 基礎設施 package(`mon`、`mongo` 實作層等僅在 repository 實作出現)。
|
||||||
|
|
||||||
**範例:**
|
**範例:**
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ type AccountIndexUP interface {
|
||||||
- struct 名稱 `{Entity}Repository`,建構子 `New{Entity}Repository(param {Entity}RepositoryParam)`。
|
- struct 名稱 `{Entity}Repository`,建構子 `New{Entity}Repository(param {Entity}RepositoryParam)`。
|
||||||
- Param struct 集中注入 `Conf`、`CacheConf`、`DBOpts`、`CacheOpts`。
|
- Param struct 集中注入 `Conf`、`CacheConf`、`DBOpts`、`CacheOpts`。
|
||||||
- 建構時以 entity 的 `CollectionName()` 初始化 DocumentDB;失敗時 `panic`(啟動期錯誤)。
|
- 建構時以 entity 的 `CollectionName()` 初始化 DocumentDB;失敗時 `panic`(啟動期錯誤)。
|
||||||
- CRUD 透過 `mgo.DocumentDBWithCacheUseCase` 操作,搭配模組 `redis.go` 的 key helper。
|
- CRUD 透過 `mongo.DocumentDBWithCacheUseCase`(`gateway/internal/library/mongo`)操作,搭配模組 `redis.go` 的 key helper。
|
||||||
- `Insert`:ID 為 zero 時自動產生 ObjectID 並寫入 `CreateAt` / `UpdateAt`。
|
- `Insert`:ID 為 zero 時自動產生 ObjectID 並寫入 `CreateAt` / `UpdateAt`。
|
||||||
- `Update`:自動更新 `UpdateAt`。
|
- `Update`:自動更新 `UpdateAt`。
|
||||||
- `FindOne` / `Delete`:無效 ObjectID → `*errs.Error`(`ResInvalidMeasureID`)或模組 `ErrInvalidObjectID`;查無資料 → 模組 `ErrNotFound`(見第 7 節錯誤)。
|
- `FindOne` / `Delete`:無效 ObjectID → `*errs.Error`(`ResInvalidMeasureID`)或模組 `ErrInvalidObjectID`;查無資料 → 模組 `ErrNotFound`(見第 7 節錯誤)。
|
||||||
|
|
@ -162,19 +162,19 @@ type AccountIndexUP interface {
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type AccountRepositoryParam struct {
|
type AccountRepositoryParam struct {
|
||||||
Conf *mgo.Conf
|
Conf *mongo.Conf
|
||||||
CacheConf cache.CacheConf
|
CacheConf cache.CacheConf
|
||||||
DBOpts []mon.Option
|
DBOpts []mon.Option
|
||||||
CacheOpts []cache.Option
|
CacheOpts []cache.Option
|
||||||
}
|
}
|
||||||
|
|
||||||
type accountRepository struct {
|
type accountRepository struct {
|
||||||
DB mgo.DocumentDBWithCacheUseCase
|
DB mongo.DocumentDBWithCacheUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccountRepository(param AccountRepositoryParam) AccountRepository {
|
func NewAccountRepository(param AccountRepositoryParam) AccountRepository {
|
||||||
e := entity.Account{}
|
e := entity.Account{}
|
||||||
documentDB, err := mgo.MustDocumentDBWithCache(
|
documentDB, err := mongo.MustDocumentDBWithCache(
|
||||||
param.Conf, e.CollectionName(), param.CacheConf, param.DBOpts, param.CacheOpts,
|
param.Conf, e.CollectionName(), param.CacheConf, param.DBOpts, param.CacheOpts,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
11
go.mod
11
go.mod
|
|
@ -6,8 +6,10 @@ require (
|
||||||
github.com/go-playground/locales v0.14.1
|
github.com/go-playground/locales v0.14.1
|
||||||
github.com/go-playground/universal-translator v0.18.1
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.30.2
|
github.com/go-playground/validator/v10 v10.30.2
|
||||||
|
github.com/shopspring/decimal v1.4.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/zeromicro/go-zero v1.10.1
|
github.com/zeromicro/go-zero v1.10.1
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||||
google.golang.org/grpc v1.79.3
|
google.golang.org/grpc v1.79.3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16,6 +18,7 @@ require (
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
|
@ -37,8 +40,13 @@ require (
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/titanous/json5 v1.0.0 // indirect
|
github.com/titanous/json5 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||||
|
|
@ -50,10 +58,13 @@ require (
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
golang.org/x/crypto v0.49.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
|
|
|
||||||
58
go.sum
58
go.sum
|
|
@ -1,11 +1,19 @@
|
||||||
|
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
|
|
@ -41,6 +49,8 @@ github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslC
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
|
@ -72,10 +82,14 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
|
||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
|
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
|
||||||
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
|
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
|
@ -84,8 +98,23 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s=
|
github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s=
|
||||||
github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c=
|
github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
github.com/zeromicro/go-zero v1.10.1 h1:1nM3ilvYx97GUqyaNH2IQPtfNyK7tp5JvN63c7m6QKU=
|
github.com/zeromicro/go-zero v1.10.1 h1:1nM3ilvYx97GUqyaNH2IQPtfNyK7tp5JvN63c7m6QKU=
|
||||||
github.com/zeromicro/go-zero v1.10.1/go.mod h1:z41DXmO6gx/Se7Ow5UIwPxcUmpVj3ebhoNCcZ1gfp5k=
|
github.com/zeromicro/go-zero v1.10.1/go.mod h1:z41DXmO6gx/Se7Ow5UIwPxcUmpVj3ebhoNCcZ1gfp5k=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
|
|
@ -110,22 +139,51 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,331 @@
|
||||||
|
# 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`。
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Conf is MongoDB client configuration for DocumentDB helpers.
|
||||||
|
type Conf struct {
|
||||||
|
Schema string
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
Host string
|
||||||
|
Database string
|
||||||
|
AuthSource string
|
||||||
|
ReplicaName string
|
||||||
|
TLS bool
|
||||||
|
MaxPoolSize uint64
|
||||||
|
MinPoolSize uint64
|
||||||
|
MaxConnIdleTime time.Duration
|
||||||
|
Compressors []string
|
||||||
|
ConnectTimeoutMs int64
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||||
|
"github.com/zeromicro/go-zero/core/syncx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotFound matches go-zero mon / mongo driver "no documents".
|
||||||
|
ErrNotFound = mon.ErrNotFound
|
||||||
|
|
||||||
|
singleFlight = syncx.NewSingleFlight()
|
||||||
|
stats = cache.NewStat("mongoc")
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MgoDecimal struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ bson.ValueEncoder = &MgoDecimal{}
|
||||||
|
_ bson.ValueDecoder = &MgoDecimal{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (dc *MgoDecimal) EncodeValue(_ bson.EncodeContext, vw bson.ValueWriter, value reflect.Value) error {
|
||||||
|
dec, ok := value.Interface().(decimal.Decimal)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("value %v is not decimal.Decimal", value.Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
primDec, err := bson.ParseDecimal128(dec.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decimal %v to Decimal128: %w", dec, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vw.WriteDecimal128(primDec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *MgoDecimal) DecodeValue(_ bson.DecodeContext, vr bson.ValueReader, value reflect.Value) error {
|
||||||
|
primDec, err := vr.ReadDecimal128()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read Decimal128: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dec, err := decimal.NewFromString(primDec.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Decimal128 %v to decimal: %w", primDec, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Set(reflect.ValueOf(dec))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentDBWithCache struct {
|
||||||
|
DocumentDBUseCase
|
||||||
|
Cache cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustDocumentDBWithCache(
|
||||||
|
conf *Conf,
|
||||||
|
collection string,
|
||||||
|
cacheConf cache.CacheConf,
|
||||||
|
dbOpts []mon.Option,
|
||||||
|
cacheOpts []cache.Option,
|
||||||
|
) (DocumentDBWithCacheUseCase, error) {
|
||||||
|
documentDB, err := NewDocumentDB(conf, collection, dbOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mongo: document db: %w", err)
|
||||||
|
}
|
||||||
|
return &DocumentDBWithCache{
|
||||||
|
DocumentDBUseCase: documentDB,
|
||||||
|
Cache: MustModelCache(cacheConf, cacheOpts...),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) DelCache(ctx context.Context, keys ...string) error {
|
||||||
|
return dc.Cache.DelCtx(ctx, keys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) GetCache(ctx context.Context, key string, v any) error {
|
||||||
|
return dc.Cache.GetCtx(ctx, key, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) SetCache(ctx context.Context, key string, v any) error {
|
||||||
|
return dc.Cache.SetCtx(ctx, key, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) DeleteOne(ctx context.Context, key string, filter any, opts ...options.Lister[options.DeleteOneOptions]) (int64, error) {
|
||||||
|
val, err := dc.GetClient().DeleteOne(ctx, filter, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
dc.delCacheBestEffort(ctx, key)
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) FindOne(ctx context.Context, key string, v, filter any, opts ...options.Lister[options.FindOneOptions]) error {
|
||||||
|
return dc.Cache.TakeCtx(ctx, v, key, func(v any) error {
|
||||||
|
return dc.GetClient().FindOne(ctx, v, filter, opts...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) FindOneAndDelete(ctx context.Context, key string, v, filter any, opts ...options.Lister[options.FindOneAndDeleteOptions]) error {
|
||||||
|
if err := dc.GetClient().FindOneAndDelete(ctx, v, filter, opts...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dc.delCacheBestEffort(ctx, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) FindOneAndReplace(ctx context.Context, key string, v, filter, replacement any, opts ...options.Lister[options.FindOneAndReplaceOptions]) error {
|
||||||
|
if err := dc.GetClient().FindOneAndReplace(ctx, v, filter, replacement, opts...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dc.delCacheBestEffort(ctx, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) InsertOne(ctx context.Context, key string, document any, opts ...options.Lister[options.InsertOneOptions]) (*mongo.InsertOneResult, error) {
|
||||||
|
res, err := dc.GetClient().InsertOne(ctx, document, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dc.delCacheBestEffort(ctx, key)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) UpdateByID(ctx context.Context, key string, id, update any, opts ...options.Lister[options.UpdateOneOptions]) (*mongo.UpdateResult, error) {
|
||||||
|
res, err := dc.GetClient().UpdateByID(ctx, id, update, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dc.delCacheBestEffort(ctx, key)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) UpdateMany(ctx context.Context, keys []string, filter, update any, opts ...options.Lister[options.UpdateManyOptions]) (*mongo.UpdateResult, error) {
|
||||||
|
res, err := dc.GetClient().UpdateMany(ctx, filter, update, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dc.delCacheBestEffort(ctx, keys...)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) UpdateOne(ctx context.Context, key string, filter, update any, opts ...options.Lister[options.UpdateOneOptions]) (*mongo.UpdateResult, error) {
|
||||||
|
res, err := dc.GetClient().UpdateOne(ctx, filter, update, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dc.delCacheBestEffort(ctx, key)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DocumentDBWithCache) delCacheBestEffort(ctx context.Context, keys ...string) {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dc.DelCache(ctx, keys...); err != nil {
|
||||||
|
logx.WithContext(ctx).Errorf("[DocumentDBWithCache] del cache keys=%v: %v", keys, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustModelCache(conf cache.CacheConf, opts ...cache.Option) cache.Cache {
|
||||||
|
return cache.New(conf, singleFlight, stats, ErrNotFound, opts...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentDB struct {
|
||||||
|
Mon *mon.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentDB(config *Conf, collection string, opts ...mon.Option) (DocumentDBUseCase, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, fmt.Errorf("mongo: config is nil")
|
||||||
|
}
|
||||||
|
if collection == "" {
|
||||||
|
return nil, fmt.Errorf("mongo: collection is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionURI, err := buildConnectionURI(*config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, InitMongoOptions(*config))
|
||||||
|
logx.Infof("[DocumentDB] connecting to %s db=%s coll=%s", redactConnectionURI(connectionURI), config.Database, collection)
|
||||||
|
|
||||||
|
client, err := mon.NewModel(connectionURI, config.Database, collection, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mongo: new model: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.Duration(config.ConnectTimeoutMs) * time.Millisecond
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := client.Collection.Database().Client().Ping(ctx, readpref.Primary()); err != nil {
|
||||||
|
return nil, fmt.Errorf("mongo: ping primary: %w", err)
|
||||||
|
}
|
||||||
|
logx.Infof("[DocumentDB] connected")
|
||||||
|
|
||||||
|
return &DocumentDB{Mon: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (document *DocumentDB) PopulateIndex(ctx context.Context, key string, sort int32, unique bool) error {
|
||||||
|
return document.createIndex(ctx, []string{key}, []int32{sort}, unique, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (document *DocumentDB) PopulateTTLIndex(ctx context.Context, key string, sort int32, unique bool, ttl int32) error {
|
||||||
|
return document.createIndex(ctx, []string{key}, []int32{sort}, unique, options.Index().SetExpireAfterSeconds(ttl))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (document *DocumentDB) PopulateMultiIndex(ctx context.Context, keys []string, sorts []int32, unique bool) error {
|
||||||
|
if len(keys) != len(sorts) {
|
||||||
|
return fmt.Errorf("mongo: keys and sorts length mismatch")
|
||||||
|
}
|
||||||
|
return document.createIndex(ctx, keys, sorts, unique, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (document *DocumentDB) createIndex(
|
||||||
|
ctx context.Context,
|
||||||
|
keys []string,
|
||||||
|
sorts []int32,
|
||||||
|
unique bool,
|
||||||
|
indexOpt *options.IndexOptionsBuilder,
|
||||||
|
) error {
|
||||||
|
c := document.Mon.Collection
|
||||||
|
index := yieldIndexModel(keys, sorts, unique, indexOpt)
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_, err := c.Indexes().CreateOne(ctx, index)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mongo: create index: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (document *DocumentDB) GetClient() *mon.Model {
|
||||||
|
return document.Mon
|
||||||
|
}
|
||||||
|
|
||||||
|
func yieldIndexModel(keys []string, sorts []int32, unique bool, indexOpt *options.IndexOptionsBuilder) mongo.IndexModel {
|
||||||
|
setKeysDoc := bson.D{}
|
||||||
|
for i, key := range keys {
|
||||||
|
setKeysDoc = append(setKeysDoc, bson.E{Key: key, Value: sorts[i]})
|
||||||
|
}
|
||||||
|
if indexOpt == nil {
|
||||||
|
indexOpt = options.Index()
|
||||||
|
}
|
||||||
|
indexOpt.SetUnique(unique)
|
||||||
|
return mongo.IndexModel{
|
||||||
|
Keys: setKeysDoc,
|
||||||
|
Options: indexOpt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetCustomDecimalType registers decimal.Decimal <-> BSON Decimal128 conversion.
|
||||||
|
func SetCustomDecimalType() mon.Option {
|
||||||
|
return mon.WithTypeCodec(mon.TypeCodec{
|
||||||
|
ValueType: reflect.TypeOf(decimal.Decimal{}),
|
||||||
|
Encoder: &MgoDecimal{},
|
||||||
|
Decoder: &MgoDecimal{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitMongoOptions applies pool / compressor settings from Conf.
|
||||||
|
func InitMongoOptions(cfg Conf) mon.Option {
|
||||||
|
return func(opts *options.ClientOptions) {
|
||||||
|
if cfg.MaxPoolSize > 0 {
|
||||||
|
opts.SetMaxPoolSize(cfg.MaxPoolSize)
|
||||||
|
}
|
||||||
|
if cfg.MinPoolSize > 0 {
|
||||||
|
opts.SetMinPoolSize(cfg.MinPoolSize)
|
||||||
|
}
|
||||||
|
if cfg.MaxConnIdleTime > 0 {
|
||||||
|
opts.SetMaxConnIdleTime(cfg.MaxConnIdleTime)
|
||||||
|
}
|
||||||
|
opts.SetCompressors(defaultCompressors(cfg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildConnectionURI(c Conf) (string, error) {
|
||||||
|
scheme := c.Schema
|
||||||
|
if scheme == "" {
|
||||||
|
scheme = "mongodb"
|
||||||
|
}
|
||||||
|
if c.Host == "" {
|
||||||
|
return "", fmt.Errorf("mongo: host is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: scheme,
|
||||||
|
Host: c.Host,
|
||||||
|
}
|
||||||
|
if c.User != "" {
|
||||||
|
u.User = url.UserPassword(c.User, c.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := url.Values{}
|
||||||
|
if c.AuthSource != "" {
|
||||||
|
q.Set("authSource", c.AuthSource)
|
||||||
|
}
|
||||||
|
if c.ReplicaName != "" {
|
||||||
|
q.Set("replicaSet", c.ReplicaName)
|
||||||
|
}
|
||||||
|
if c.TLS {
|
||||||
|
q.Set("tls", "true")
|
||||||
|
}
|
||||||
|
if len(q) > 0 {
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactConnectionURI(uri string) string {
|
||||||
|
u, err := url.Parse(uri)
|
||||||
|
if err != nil || u.User == nil {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
if _, hasPassword := u.User.Password(); !hasPassword {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
u.User = url.UserPassword(u.User.Username(), "*****")
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultCompressors(c Conf) []string {
|
||||||
|
if len(c.Compressors) > 0 {
|
||||||
|
return c.Compressors
|
||||||
|
}
|
||||||
|
return []string{"zstd", "snappy"}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBuildConnectionURI_escapesPassword(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
uri, err := buildConnectionURI(Conf{
|
||||||
|
Schema: "mongodb",
|
||||||
|
Host: "127.0.0.1:27017",
|
||||||
|
User: "user",
|
||||||
|
Password: "p@ss:word",
|
||||||
|
AuthSource: "admin",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if uri != "mongodb://user:p%40ss%3Aword@127.0.0.1:27017?authSource=admin" {
|
||||||
|
t.Fatalf("uri = %q", uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedactConnectionURI(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
raw := "mongodb://user:secret@127.0.0.1:27017"
|
||||||
|
got := redactConnectionURI(raw)
|
||||||
|
if got == raw {
|
||||||
|
t.Fatalf("expected redacted uri, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentDBUseCase interface {
|
||||||
|
PopulateIndex(ctx context.Context, key string, sort int32, unique bool) error
|
||||||
|
PopulateTTLIndex(ctx context.Context, key string, sort int32, unique bool, ttl int32) error
|
||||||
|
PopulateMultiIndex(ctx context.Context, keys []string, sorts []int32, unique bool) error
|
||||||
|
GetClient() *mon.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentDBWithCacheUseCase interface {
|
||||||
|
DocumentDBUseCase
|
||||||
|
CacheUseCase
|
||||||
|
DeleteOne(ctx context.Context, key string, filter any, opts ...options.Lister[options.DeleteOneOptions]) (int64, error)
|
||||||
|
FindOne(ctx context.Context, key string, v, filter any, opts ...options.Lister[options.FindOneOptions]) error
|
||||||
|
FindOneAndDelete(ctx context.Context, key string, v, filter any, opts ...options.Lister[options.FindOneAndDeleteOptions]) error
|
||||||
|
FindOneAndReplace(ctx context.Context, key string, v, filter, replacement any, opts ...options.Lister[options.FindOneAndReplaceOptions]) error
|
||||||
|
InsertOne(ctx context.Context, key string, document any, opts ...options.Lister[options.InsertOneOptions]) (*mongo.InsertOneResult, error)
|
||||||
|
UpdateByID(ctx context.Context, key string, id, update any, opts ...options.Lister[options.UpdateOneOptions]) (*mongo.UpdateResult, error)
|
||||||
|
UpdateMany(ctx context.Context, keys []string, filter, update any, opts ...options.Lister[options.UpdateManyOptions]) (*mongo.UpdateResult, error)
|
||||||
|
UpdateOne(ctx context.Context, key string, filter, update any, opts ...options.Lister[options.UpdateOneOptions]) (*mongo.UpdateResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CacheUseCase interface {
|
||||||
|
DelCache(ctx context.Context, keys ...string) error
|
||||||
|
GetCache(ctx context.Context, key string, v any) error
|
||||||
|
SetCache(ctx context.Context, key string, v any) error
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue