diff --git a/database/mongodb/README.md b/database/mongodb/README.md new file mode 100644 index 0000000..1c2b3db --- /dev/null +++ b/database/mongodb/README.md @@ -0,0 +1,51 @@ +# MongoDB + +## 概念对比 + +| MongoDB存储结构 | RDBMS存储结构 | +|-------------|-------------| +| database | database | +| collection | table | +| document | row | +| field | column | +| index | 索引 | +| primary key | primary key | + +## Docker部署 + +下载镜像: + +```bash +docker pull bitnami/mongodb:latest +docker pull bitnami/mongodb-exporter:latest +``` + +带密码安装: + +```bash +docker run -itd \ + --name mongodb-server \ + -p 27017:27017 \ + -e MONGODB_ROOT_USER=root \ + -e MONGODB_ROOT_PASSWORD=123456 \ + -e MONGODB_USERNAME=test \ + -e MONGODB_PASSWORD=123456 \ + -e MONGODB_DATABASE=finances \ + bitnami/mongodb:latest +``` + +不带密码安装: + +```bash +docker run -itd \ + --name mongodb-server \ + -p 27017:27017 \ + -e ALLOW_EMPTY_PASSWORD=yes \ + bitnami/mongodb:latest +``` + +有两点需要注意: + +1. 如果需要映射数据卷,需要把本地路径的所有权改到1001:`sudo chown -R 1001:1001 data/db`,否则会报错: + `‘mkdir: cannot create directory ‘/bitnami/mongodb’: Permission denied’`; +2. 从MongoDB 5.0开始,有些机器运行会报错:`Illegal instruction`,这是因为机器硬件不支持 **AVX 指令集** 的缘故,没办法,MongoDB降级吧。 diff --git a/database/mongodb/client.go b/database/mongodb/client.go index 8cae505..7d8a239 100644 --- a/database/mongodb/client.go +++ b/database/mongodb/client.go @@ -2,35 +2,172 @@ package mongodb import ( "context" - "fmt" + "time" "github.com/go-kratos/kratos/v2/log" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + mongoV2 "go.mongodb.org/mongo-driver/v2/mongo" + optionsV2 "go.mongodb.org/mongo-driver/v2/mongo/options" conf "github.com/tx7do/kratos-bootstrap/api/gen/go/conf/v1" ) -// NewMongoClient 创建MongoDB客户端 -func NewMongoClient(ctx context.Context, cfg *conf.Bootstrap, l *log.Helper) *mongo.Client { - if cfg.Data == nil || cfg.Data.Mongodb == nil { - l.Warn("Mongodb config is nil") - return nil - } +type Client struct { + log *log.Helper - var opts []*options.ClientOptions - - uri := fmt.Sprintf("mongodb://%s:%s@%s", - cfg.Data.Mongodb.Username, cfg.Data.Mongodb.Password, cfg.Data.Mongodb.Address, - ) - opts = append(opts, options.Client().ApplyURI(uri)) - - cli, err := mongo.Connect(ctx, opts...) - if err != nil { - l.Fatalf("failed opening connection to mongodb: %v", err) - return nil - } - - return cli + cli *mongoV2.Client + database string + timeout time.Duration +} + +func NewClient(logger log.Logger, cfg *conf.Bootstrap) (*Client, error) { + c := &Client{ + log: log.NewHelper(log.With(logger, "module", "mongodb-client")), + } + + if err := c.createMongodbClient(cfg); err != nil { + return nil, err + } + + return c, nil +} + +// createMongodbClient 创建MongoDB客户端 +func (c *Client) createMongodbClient(cfg *conf.Bootstrap) error { + if cfg.Data == nil || cfg.Data.Mongodb == nil { + return nil + } + + var opts []*optionsV2.ClientOptions + + if cfg.Data.Mongodb.GetUri() != "" { + opts = append(opts, optionsV2.Client().ApplyURI(cfg.Data.Mongodb.GetUri())) + } + if cfg.Data.Mongodb.GetUsername() != "" && cfg.Data.Mongodb.GetPassword() != "" { + credential := optionsV2.Credential{ + Username: cfg.Data.Mongodb.GetUsername(), + Password: cfg.Data.Mongodb.GetPassword(), + } + + if cfg.Data.Mongodb.GetPassword() != "" { + credential.PasswordSet = true + } + + opts = append(opts, optionsV2.Client().SetAuth(credential)) + } + if cfg.Data.Mongodb.ConnectTimeout != nil { + opts = append(opts, optionsV2.Client().SetConnectTimeout(cfg.Data.Mongodb.GetConnectTimeout().AsDuration())) + } + if cfg.Data.Mongodb.ServerSelectionTimeout != nil { + opts = append(opts, optionsV2.Client().SetServerSelectionTimeout(cfg.Data.Mongodb.GetServerSelectionTimeout().AsDuration())) + } + if cfg.Data.Mongodb.Timeout != nil { + opts = append(opts, optionsV2.Client().SetTimeout(cfg.Data.Mongodb.GetTimeout().AsDuration())) + } + + opts = append(opts, optionsV2.Client().SetBSONOptions(&optionsV2.BSONOptions{ + UseJSONStructTags: true, // 使用JSON结构标签 + })) + + cli, err := mongoV2.Connect(opts...) + if err != nil { + c.log.Errorf("failed to create mongodb client: %v", err) + return err + } + + c.database = cfg.Data.Mongodb.GetDatabase() + if cfg.Data.Mongodb.GetTimeout() != nil { + c.timeout = cfg.Data.Mongodb.GetTimeout().AsDuration() + } else { + c.timeout = 10 * time.Second // 默认超时时间 + } + + c.cli = cli + + return nil +} + +// Close 关闭MongoDB客户端 +func (c *Client) Close() { + if c.cli == nil { + c.log.Warn("mongodb client is already closed or not initialized") + return + } + + if err := c.cli.Disconnect(context.Background()); err != nil { + c.log.Errorf("failed to disconnect mongodb client: %v", err) + } else { + c.log.Info("mongodb client disconnected successfully") + } +} + +// CheckConnect 检查MongoDB连接状态 +func (c *Client) CheckConnect() { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + if err := c.cli.Ping(ctx, nil); err != nil { + c.log.Errorf("failed to ping mongodb: %v", err) + } else { + c.log.Info("mongodb client is connected") + } +} + +// InsertOne 插入单个文档 +func (c *Client) InsertOne(ctx context.Context, collection string, document interface{}) (*mongoV2.InsertOneResult, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + + return c.cli.Database(c.database).Collection(collection).InsertOne(ctx, document) +} + +// InsertMany 插入多个文档 +func (c *Client) InsertMany(ctx context.Context, collection string, documents []interface{}) (*mongoV2.InsertManyResult, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + + return c.cli.Database(c.database).Collection(collection).InsertMany(ctx, documents) +} + +// FindOne 查询单个文档 +func (c *Client) FindOne(ctx context.Context, collection string, filter interface{}, result interface{}) error { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + + return c.cli.Database(c.database).Collection(collection).FindOne(ctx, filter).Decode(result) +} + +// Find 查询多个文档 +func (c *Client) Find(ctx context.Context, collection string, filter interface{}, results interface{}) error { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + + cursor, err := c.cli.Database(c.database).Collection(collection).Find(ctx, filter) + if err != nil { + c.log.Errorf("failed to find documents in collection %s: %v", collection, err) + return err + } + defer func(cursor *mongoV2.Cursor, ctx context.Context) { + if err = cursor.Close(ctx); err != nil { + c.log.Errorf("failed to close cursor: %v", err) + } + }(cursor, ctx) + + return cursor.All(ctx, results) +} + +// UpdateOne 更新单个文档 +func (c *Client) UpdateOne(ctx context.Context, collection string, filter, update interface{}) (*mongoV2.UpdateResult, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + + return c.cli.Database(c.database).Collection(collection).UpdateOne(ctx, filter, update) +} + +// DeleteOne 删除单个文档 +func (c *Client) DeleteOne(ctx context.Context, collection string, filter interface{}) (*mongoV2.DeleteResult, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + + return c.cli.Database(c.database).Collection(collection).DeleteOne(ctx, filter) } diff --git a/database/mongodb/client_test.go b/database/mongodb/client_test.go new file mode 100644 index 0000000..5a479f2 --- /dev/null +++ b/database/mongodb/client_test.go @@ -0,0 +1,66 @@ +package mongodb + +import ( + "context" + "testing" + "time" + + "github.com/go-kratos/kratos/v2/log" + "github.com/stretchr/testify/assert" + "github.com/tx7do/go-utils/trans" + conf "github.com/tx7do/kratos-bootstrap/api/gen/go/conf/v1" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type Candle struct { + Symbol *string `json:"s"` + Open *float64 `json:"o"` + High *float64 `json:"h"` + Low *float64 `json:"l"` + Close *float64 `json:"c"` + Volume *float64 `json:"v"` + StartTime *timestamppb.Timestamp `json:"st"` + EndTime *timestamppb.Timestamp `json:"et"` +} + +func createTestClient() *Client { + cli, _ := NewClient( + log.DefaultLogger, + &conf.Bootstrap{ + Data: &conf.Data{ + Mongodb: &conf.Data_MongoDB{ + Uri: "mongodb://root:123456@127.0.0.1:27017/?compressors=snappy,zlib,zstd", + Database: trans.Ptr("finances"), + }, + }, + }, + ) + return cli +} + +func TestNewClient(t *testing.T) { + client := createTestClient() + assert.NotNil(t, client) + + client.CheckConnect() +} + +func TestInsertOne(t *testing.T) { + client := createTestClient() + assert.NotNil(t, client) + + ctx := context.Background() + + candle := Candle{ + StartTime: timestamppb.New(time.Now()), + Symbol: trans.Ptr("AAPL"), + Open: trans.Ptr(1.0), + High: trans.Ptr(2.0), + Low: trans.Ptr(3.0), + Close: trans.Ptr(4.0), + Volume: trans.Ptr(1000.0), + } + + _, err := client.InsertOne(ctx, "candles", candle) + assert.NoError(t, err) +} diff --git a/database/mongodb/consts.go b/database/mongodb/consts.go new file mode 100644 index 0000000..41a78ef --- /dev/null +++ b/database/mongodb/consts.go @@ -0,0 +1,95 @@ +package mongodb + +const ( + // 比较操作符 + + OperatorEq = "$eq" // 等于 + OperatorNe = "$ne" // 不等于 + OperatorGt = "$gt" // 大于 + OperatorGte = "$gte" // 大于等于 + OperatorLt = "$lt" // 小于 + OperatorLte = "$lte" // 小于等于 + + // 逻辑操作符 + + OperatorAnd = "$and" // 与 + OperatorOr = "$or" // 或 + OperatorNot = "$not" // 非 + OperatorNor = "$nor" // 非或 + + // 元素操作符 + + OperatorExists = "$exists" // 是否存在 + OperatorType = "$type" // 类型 + + // 评估操作符 + + OperatorExpr = "$expr" // 表达式 + OperatorJsonSchema = "$jsonSchema" // JSON Schema 验证 + OperatorMod = "$mod" // 取模 + OperatorRegex = "$regex" // 正则表达式 + OperatorText = "$text" // 文本搜索 + OperatorWhere = "$where" // JavaScript 表达式 + OperatorSearch = "$search" // 文本搜索 + + // 数组操作符 + + OperatorAll = "$all" // 匹配所有 + OperatorElemMatch = "$elemMatch" // 匹配数组中的元素 + OperatorSize = "$size" // 数组大小 + + // 集合操作符 + + OperatorIn = "$in" // 包含 + OperatorNin = "$nin" // 不包含 + + // 更新操作符 + + OperatorSet = "$set" // 设置字段值 + OperatorUnset = "$unset" // 删除字段 + OperatorInc = "$inc" // 增加值 + OperatorMul = "$mul" // 乘法 + OperatorRename = "$rename" // 重命名字段 + OperatorMin = "$min" // 设置最小值 + OperatorMax = "$max" // 设置最大值 + OperatorCurrentDate = "$currentDate" // 设置当前日期 + OperatorAddToSet = "$addToSet" // 添加到集合 + OperatorPop = "$pop" // 删除数组中的元素 + OperatorPull = "$pull" // 删除匹配的数组元素 + OperatorPush = "$push" // 添加数组元素 + OperatorEach = "$each" // 批量添加数组元素 + OperatorSlice = "$slice" // 截取数组 + OperatorSort = "$sort" // 排序数组 + OperatorPosition = "$position" // 指定数组位置 + + // 聚合操作符 + + OperatorGroup = "$group" // 分组 + OperatorMatch = "$match" // 匹配 + OperatorProject = "$project" // 投影 + OperatorSortAgg = "$sort" // 排序 + OperatorLimit = "$limit" // 限制 + OperatorSkip = "$skip" // 跳过 + OperatorUnwind = "$unwind" // 拆分数组 + OperatorLookup = "$lookup" // 关联查询 + OperatorAddFields = "$addFields" // 添加字段 + OperatorReplaceRoot = "$replaceRoot" // 替换根字段 + OperatorCount = "$count" // 计数 + OperatorFacet = "$facet" // 多面查询 + OperatorBucket = "$bucket" // 分桶 + OperatorBucketAuto = "$bucketAuto" // 自动分桶 + OperatorIndexStats = "$indexStats" // 索引统计 + OperatorOut = "$out" // 输出 + OperatorMerge = "$merge" // 合并 + + // 地理空间操作符 + + OperatorNear = "$near" // 查询距离某点最近的文档 + OperatorNearSphere = "$nearSphere" // 查询距离某点最近的文档(球面距离) + OperatorGeoWithin = "$geoWithin" // 地理范围查询 + OperatorGeoIntersects = "$geoIntersects" // 地理相交查询 + + OperatorGeometry = "$geometry" // 几何图形 + OperatorMaxDistance = "$maxDistance" // 最大距离 + OperatorMinDistance = "$minDistance" // 最小距离 +) diff --git a/database/mongodb/errors.go b/database/mongodb/errors.go new file mode 100644 index 0000000..6e59891 --- /dev/null +++ b/database/mongodb/errors.go @@ -0,0 +1 @@ +package mongodb diff --git a/database/mongodb/go.mod b/database/mongodb/go.mod index b089a87..40066c4 100644 --- a/database/mongodb/go.mod +++ b/database/mongodb/go.mod @@ -8,20 +8,26 @@ replace github.com/tx7do/kratos-bootstrap/api => ../../api require ( github.com/go-kratos/kratos/v2 v2.8.4 - github.com/tx7do/kratos-bootstrap/api v0.0.21 - go.mongodb.org/mongo-driver v1.17.3 + github.com/stretchr/testify v1.10.0 + github.com/tx7do/go-utils v1.1.29 + github.com/tx7do/kratos-bootstrap/api v0.0.26 + go.mongodb.org/mongo-driver/v2 v2.2.2 + google.golang.org/protobuf v1.36.6 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/text v0.25.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/text v0.26.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/database/mongodb/go.sum b/database/mongodb/go.sum index 278fd59..d6fa25d 100644 --- a/database/mongodb/go.sum +++ b/database/mongodb/go.sum @@ -1,3 +1,4 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs= @@ -6,10 +7,22 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tx7do/go-utils v1.1.29 h1:kO1JDMVX++ZY4+aXGk3pOtDz5WBPDA3LxhIWkzXkvH8= +github.com/tx7do/go-utils v1.1.29/go.mod h1:bmt7c85QmHURtd7h6QOu7k0QKOJTwjJ+cFP29nljdSw= 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.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -19,20 +32,20 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi 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= -go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= -go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver/v2 v2.2.2 h1:9cYuS3fl1Xhqwpfazso10V7BHQD58kCgtzhfAmJYz9c= +go.mongodb.org/mongo-driver/v2 v2.2.2/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps= 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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= 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/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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= @@ -44,11 +57,16 @@ 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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 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= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/database/mongodb/query.go b/database/mongodb/query.go new file mode 100644 index 0000000..7708fd4 --- /dev/null +++ b/database/mongodb/query.go @@ -0,0 +1,217 @@ +package mongodb + +import ( + bsonV2 "go.mongodb.org/mongo-driver/v2/bson" + optionsV2 "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +type QueryBuilder struct { + filter bsonV2.M + opts *optionsV2.FindOptions +} + +func NewQuery() *QueryBuilder { + return &QueryBuilder{ + filter: bsonV2.M{}, + opts: &optionsV2.FindOptions{}, + } +} + +// SetFilter 设置查询过滤条件 +func (qb *QueryBuilder) SetFilter(filter bsonV2.M) *QueryBuilder { + qb.filter = filter + return qb +} + +// SetOr 设置多个条件的逻辑或 +func (qb *QueryBuilder) SetOr(conditions []bsonV2.M) *QueryBuilder { + qb.filter[OperatorOr] = conditions + return qb +} + +// SetAnd 设置多个条件的逻辑与 +func (qb *QueryBuilder) SetAnd(conditions []bsonV2.M) *QueryBuilder { + qb.filter[OperatorAnd] = conditions + return qb +} + +// SetNotEqual 设置字段的不等于条件 +func (qb *QueryBuilder) SetNotEqual(field string, value interface{}) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorNe: value} + return qb +} + +// SetGreaterThan 设置字段的大于条件 +func (qb *QueryBuilder) SetGreaterThan(field string, value interface{}) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorGt: value} + return qb +} + +// SetLessThan 设置字段的小于条件 +func (qb *QueryBuilder) SetLessThan(field string, value interface{}) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorLt: value} + return qb +} + +// SetExists 设置字段是否存在条件 +func (qb *QueryBuilder) SetExists(field string, exists bool) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorExists: exists} + return qb +} + +// SetType 设置字段的类型条件 +func (qb *QueryBuilder) SetType(field string, typeValue interface{}) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorType: typeValue} + return qb +} + +// SetBetween 设置字段的范围查询条件 +func (qb *QueryBuilder) SetBetween(field string, start, end interface{}) *QueryBuilder { + qb.filter[field] = bsonV2.M{ + OperatorGte: start, + OperatorLte: end, + } + return qb +} + +// SetIn 设置字段的包含条件 +func (qb *QueryBuilder) SetIn(field string, values []interface{}) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorIn: values} + return qb +} + +// SetNotIn 设置字段的排除条件 +func (qb *QueryBuilder) SetNotIn(field string, values []interface{}) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorNin: values} + return qb +} + +// SetElemMatch 设置数组字段的匹配条件 +func (qb *QueryBuilder) SetElemMatch(field string, match bsonV2.M) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorElemMatch: match} + return qb +} + +// SetAll 设置字段必须包含所有指定值的条件 +func (qb *QueryBuilder) SetAll(field string, values []interface{}) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorAll: values} + return qb +} + +// SetSize 设置数组字段的大小条件 +func (qb *QueryBuilder) SetSize(field string, size int) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorSize: size} + return qb +} + +// SetCurrentDate 设置字段为当前日期 +func (qb *QueryBuilder) SetCurrentDate(field string) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorCurrentDate: true} + return qb +} + +// SetTextSearch 设置文本搜索条件 +func (qb *QueryBuilder) SetTextSearch(search string) *QueryBuilder { + qb.filter[OperatorText] = bsonV2.M{OperatorSearch: search} + return qb +} + +// SetMod 设置字段的模运算条件 +func (qb *QueryBuilder) SetMod(field string, divisor, remainder int) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorMod: []int{divisor, remainder}} + return qb +} + +// SetRegex 设置正则表达式查询条件 +func (qb *QueryBuilder) SetRegex(field string, pattern string) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorRegex: pattern} + return qb +} + +// SetGeoWithin 设置地理位置范围查询条件 +func (qb *QueryBuilder) SetGeoWithin(field string, geometry bsonV2.M) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorGeoWithin: geometry} + return qb +} + +// SetGeoIntersects 设置地理位置相交查询条件 +func (qb *QueryBuilder) SetGeoIntersects(field string, geometry bsonV2.M) *QueryBuilder { + qb.filter[field] = bsonV2.M{OperatorGeoIntersects: geometry} + return qb +} + +// SetNear 设置地理位置附近查询条件 +func (qb *QueryBuilder) SetNear(field string, point bsonV2.M, maxDistance, minDistance float64) *QueryBuilder { + qb.filter[field] = bsonV2.M{ + OperatorNear: bsonV2.M{ + OperatorGeometry: point, + OperatorMaxDistance: maxDistance, + OperatorMinDistance: minDistance, + }, + } + return qb +} + +// SetNearSphere 设置球面距离附近查询条件 +func (qb *QueryBuilder) SetNearSphere(field string, point bsonV2.M, maxDistance, minDistance float64) *QueryBuilder { + qb.filter[field] = bsonV2.M{ + OperatorNearSphere: bsonV2.M{ + OperatorGeometry: point, + OperatorMaxDistance: maxDistance, + OperatorMinDistance: minDistance, + }, + } + return qb +} + +// SetLimit 设置查询结果的限制数量 +func (qb *QueryBuilder) SetLimit(limit int64) *QueryBuilder { + if qb.opts == nil { + qb.opts = &optionsV2.FindOptions{} + } + qb.opts.Limit = &limit + return qb +} + +// SetSort 设置查询结果的排序条件 +func (qb *QueryBuilder) SetSort(sort bsonV2.D) *QueryBuilder { + if qb.opts == nil { + qb.opts = &optionsV2.FindOptions{} + } + qb.opts.Sort = sort + return qb +} + +// SetSortWithPriority 设置查询结果的排序条件,并指定优先级 +func (qb *QueryBuilder) SetSortWithPriority(sortFields []bsonV2.E) *QueryBuilder { + if qb.opts == nil { + qb.opts = &optionsV2.FindOptions{} + } + qb.opts.Sort = bsonV2.D(sortFields) + return qb +} + +// SetProjection 设置查询结果的字段投影 +func (qb *QueryBuilder) SetProjection(projection bsonV2.M) *QueryBuilder { + qb.opts.Projection = projection + return qb +} + +// SetSkip 设置查询结果的跳过数量 +func (qb *QueryBuilder) SetSkip(skip int64) *QueryBuilder { + qb.opts.Skip = &skip + return qb +} + +// SetPage 设置分页功能,page 从 1 开始,size 为每页的文档数量 +func (qb *QueryBuilder) SetPage(page, size int64) *QueryBuilder { + offset := (page - 1) * size + qb.opts.Skip = &offset + qb.opts.Limit = &size + return qb +} + +// Build 返回最终的过滤条件和查询选项 +func (qb *QueryBuilder) Build() (bsonV2.M, *optionsV2.FindOptions) { + return qb.filter, qb.opts +} diff --git a/database/mongodb/query_test.go b/database/mongodb/query_test.go new file mode 100644 index 0000000..5b4ab5c --- /dev/null +++ b/database/mongodb/query_test.go @@ -0,0 +1,219 @@ +package mongodb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + bsonV2 "go.mongodb.org/mongo-driver/v2/bson" +) + +func TestQueryBuilder(t *testing.T) { + // 创建 QueryBuilder 实例 + qb := NewQuery() + + // 测试 SetFilter + filter := bsonV2.M{"name": "test"} + qb.SetFilter(filter) + assert.Equal(t, filter, qb.filter) + + // 测试 SetLimit + limit := int64(10) + qb.SetLimit(limit) + assert.NotNil(t, qb.opts.Limit) + assert.Equal(t, &limit, qb.opts.Limit) + + // 测试 SetSort + sort := bsonV2.D{{Key: "name", Value: 1}} + qb.SetSort(sort) + assert.NotNil(t, qb.opts.Sort) + assert.Equal(t, sort, qb.opts.Sort) + + // 测试 Build + finalFilter, finalOpts := qb.Build() + assert.Equal(t, filter, finalFilter) + assert.Equal(t, qb.opts, finalOpts) +} + +func TestQueryBuilderMethods(t *testing.T) { + qb := NewQuery() + + // 测试 SetFilter + filter := bsonV2.M{"name": "test"} + qb.SetFilter(filter) + assert.Equal(t, filter, qb.filter) + + // 测试 SetNotEqual + qb.SetNotEqual("status", "inactive") + assert.Equal(t, bsonV2.M{OperatorNe: "inactive"}, qb.filter["status"]) + + // 测试 SetGreaterThan + qb.SetGreaterThan("age", 18) + assert.Equal(t, bsonV2.M{OperatorGt: 18}, qb.filter["age"]) + + // 测试 SetLessThan + qb.SetLessThan("age", 30) + assert.Equal(t, bsonV2.M{OperatorLt: 30}, qb.filter["age"]) + + // 测试 SetExists + qb.SetExists("email", true) + assert.Equal(t, bsonV2.M{OperatorExists: true}, qb.filter["email"]) + + // 测试 SetType + qb.SetType("age", "int") + assert.Equal(t, bsonV2.M{OperatorType: "int"}, qb.filter["age"]) + + // 测试 SetBetween + qb.SetBetween("price", 10, 100) + assert.Equal(t, bsonV2.M{OperatorGte: 10, OperatorLte: 100}, qb.filter["price"]) + + // 测试 SetOr + orConditions := []bsonV2.M{ + {"status": "active"}, + {"status": "pending"}, + } + qb.SetOr(orConditions) + assert.Equal(t, orConditions, qb.filter[OperatorOr]) + + // 测试 SetAnd + andConditions := []bsonV2.M{ + {"age": bsonV2.M{OperatorGt: 18}}, + {"status": "active"}, + } + qb.SetAnd(andConditions) + assert.Equal(t, andConditions, qb.filter[OperatorAnd]) + + // 测试 SetLimit + limit := int64(10) + qb.SetLimit(limit) + assert.NotNil(t, qb.opts.Limit) + assert.Equal(t, &limit, qb.opts.Limit) + + // 测试 SetSort + sort := bsonV2.D{{Key: "name", Value: 1}} + qb.SetSort(sort) + assert.NotNil(t, qb.opts.Sort) + assert.Equal(t, sort, qb.opts.Sort) + + // 测试 SetSortWithPriority + sortWithPriority := []bsonV2.E{{Key: "priority", Value: -1}, {Key: "name", Value: 1}} + qb.SetSortWithPriority(sortWithPriority) + assert.Equal(t, bsonV2.D(sortWithPriority), qb.opts.Sort) + + // 测试 SetProjection + projection := bsonV2.M{"name": 1, "age": 1} + qb.SetProjection(projection) + assert.Equal(t, projection, qb.opts.Projection) + + // 测试 SetSkip + skip := int64(5) + qb.SetSkip(skip) + assert.NotNil(t, qb.opts.Skip) + assert.Equal(t, &skip, qb.opts.Skip) + + // 测试 SetPage + page, size := int64(2), int64(10) + qb.SetPage(page, size) + assert.Equal(t, &size, qb.opts.Limit) + assert.Equal(t, int64(10), *qb.opts.Limit) + assert.Equal(t, int64(10), *qb.opts.Skip) + + // 测试 SetRegex + qb.SetRegex("name", "^test") + assert.Equal(t, bsonV2.M{OperatorRegex: "^test"}, qb.filter["name"]) + + // 测试 SetIn + qb.SetIn("tags", []interface{}{"tag1", "tag2"}) + assert.Equal(t, bsonV2.M{OperatorIn: []interface{}{"tag1", "tag2"}}, qb.filter["tags"]) + + // 测试 Build + finalFilter, finalOpts := qb.Build() + assert.Equal(t, qb.filter, finalFilter) + assert.Equal(t, qb.opts, finalOpts) +} + +func TestSetGeoWithin(t *testing.T) { + qb := NewQuery() + + field := "location" + geometry := bsonV2.M{"type": "Polygon", "coordinates": []interface{}{ + []interface{}{ + []float64{40.0, -70.0}, + []float64{41.0, -70.0}, + []float64{41.0, -71.0}, + []float64{40.0, -71.0}, + []float64{40.0, -70.0}, + }, + }} + + qb.SetGeoWithin(field, geometry) + + expected := bsonV2.M{ + OperatorGeoWithin: bsonV2.M{ + OperatorGeometry: geometry, + }, + } + + assert.Equal(t, expected, qb.filter[field]) +} + +func TestSetGeoIntersects(t *testing.T) { + qb := NewQuery() + + field := "location" + geometry := bsonV2.M{"type": "LineString", "coordinates": [][]float64{ + {40.0, -70.0}, + {41.0, -71.0}, + }} + + qb.SetGeoIntersects(field, geometry) + + expected := bsonV2.M{ + OperatorGeoIntersects: bsonV2.M{ + OperatorGeometry: geometry, + }, + } + + assert.Equal(t, expected, qb.filter[field]) +} + +func TestSetNear(t *testing.T) { + qb := NewQuery() + + field := "location" + point := bsonV2.M{"type": "Point", "coordinates": []float64{40.7128, -74.0060}} + maxDistance := 500.0 + minDistance := 50.0 + + qb.SetNear(field, point, maxDistance, minDistance) + + expected := bsonV2.M{ + OperatorNear: bsonV2.M{ + OperatorGeometry: point, + OperatorMaxDistance: maxDistance, + OperatorMinDistance: minDistance, + }, + } + + assert.Equal(t, expected, qb.filter[field]) +} + +func TestSetNearSphere(t *testing.T) { + qb := NewQuery() + + field := "location" + point := bsonV2.M{"type": "Point", "coordinates": []float64{40.7128, -74.0060}} + maxDistance := 1000.0 + minDistance := 100.0 + + qb.SetNearSphere(field, point, maxDistance, minDistance) + + expected := bsonV2.M{ + OperatorNearSphere: bsonV2.M{ + OperatorGeometry: point, + OperatorMaxDistance: maxDistance, + OperatorMinDistance: minDistance, + }, + } + + assert.Equal(t, expected, qb.filter[field]) +} diff --git a/tag.bat b/tag.bat index 26c974f..bae6de4 100644 --- a/tag.bat +++ b/tag.bat @@ -10,7 +10,7 @@ git tag tracer/v0.0.10 --force git tag database/ent/v0.0.10 --force git tag database/gorm/v0.0.10 --force -git tag database/mongodb/v0.0.10 --force +git tag database/mongodb/v0.0.11 --force git tag database/influxdb/v0.0.11 --force git tag database/cassandra/v0.0.10 --force git tag database/clickhouse/v0.0.10 --force