├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_ZH.md ├── aggregate.go ├── aggregate_test.go ├── bson.go ├── bulk.go ├── bulk_test.go ├── client.go ├── client_test.go ├── collection.go ├── collection_test.go ├── cursor.go ├── cursor_test.go ├── database.go ├── database_test.go ├── errors.go ├── errors_test.go ├── example_test.go ├── field ├── custom_field.go ├── custom_field_test.go ├── default_field.go ├── default_field_test.go ├── field.go └── field_test.go ├── field_test.go ├── go.mod ├── go.sum ├── hook ├── hook.go └── hook_test.go ├── hook_test.go ├── interface.go ├── middleware ├── middleware.go └── middleware_test.go ├── operator ├── aggregation_pipeline_operators.go ├── aggregation_pipeline_stages.go ├── operate_type.go ├── query_and_projection.go ├── query_modifiers.go └── update.go ├── options ├── aggregate_options.go ├── change_stream_options.go ├── client_options.go ├── collection_options.go ├── createcollection_options.go ├── database_options.go ├── index_options.go ├── insert_options.go ├── query_options.go ├── remove_options.go ├── replace_options.go ├── runcmd_options.go ├── session_options.go ├── transaction_options.go ├── update_options.go └── upsert_options.go ├── query.go ├── query_test.go ├── results.go ├── session.go ├── session_test.go ├── util.go ├── util_test.go ├── validator ├── validator.go └── validator_test.go └── validator_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is, and what you expected to happen. 12 | 13 | **To Reproduce** 14 | Codes to reproduce the behavior. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # idea 2 | .idea 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.16" 4 | env: 5 | - GO111MODULE=on 6 | services: 7 | - mongodb 8 | before_script: 9 | - sleep 15 # https://docs.travis-ci.com/user/database-setup/#mongodb-does-not-immediately-accept-connections 10 | - echo "replication:" | sudo tee -a /etc/mongod.conf 11 | - |- 12 | echo " replSetName: \"rs0\"" | sudo tee -a /etc/mongod.conf 13 | - sudo service mongod restart 14 | - sleep 15 15 | - mongo --eval 'rs.initiate()' 16 | - sleep 5 17 | 18 | script: 19 | - mongod --version 20 | - go test -race -coverprofile=coverage.txt -covermode=atomic ./... 21 | after_success: 22 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 The Qmgo Authors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qmgo 2 | 3 | [![Build Status](https://travis-ci.org/qiniu/qmgo.png?branch=master)](https://travis-ci.org/qiniu/qmgo) 4 | [![Coverage Status](https://codecov.io/gh/qiniu/qmgo/branch/master/graph/badge.svg)](https://codecov.io/gh/qiniu/qmgo) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/qiniu/qmgo)](https://goreportcard.com/report/github.com/qiniu/qmgo) 6 | [![GitHub release](https://img.shields.io/github/v/tag/qiniu/qmgo.svg?label=release)](https://github.com/qiniu/qmgo/releases) 7 | [![GoDoc](https://pkg.go.dev/badge/github.com/qiniu/qmgo?status.svg)](https://pkg.go.dev/github.com/qiniu/qmgo?tab=doc) 8 | 9 | English | [简体中文](README_ZH.md) 10 | 11 | `Qmgo` is a `Go` `driver` for `MongoDB` . It is based on [MongoDB official driver](https://github.com/mongodb/mongo-go-driver), but easier to use like [mgo](https://github.com/go-mgo/mgo) (such as the chain call). 12 | 13 | - `Qmgo` allows users to use the new features of `MongoDB` in a more elegant way. 14 | 15 | - `Qmgo` is the first choice for migrating from `mgo` to the new `MongoDB driver` with minimal code changes. 16 | 17 | ## Requirements 18 | 19 | -`Go 1.10` and above. 20 | 21 | -`MongoDB 2.6` and above. 22 | 23 | ## Features 24 | - CRUD to documents, with all official supported options 25 | - Sort、limit、count、select、distinct 26 | - Transactions 27 | - Hooks 28 | - Automatically default and custom fields 29 | - Predefine operator keys 30 | - Aggregate、indexes operation、cursor 31 | - Validation tags 32 | - Plugin 33 | 34 | ## Installation 35 | 36 | - Use `go mod` to automatically install dependencies by `import github.com/qiniu/qmgo` 37 | 38 | Or 39 | 40 | - Use `go get github.com/qiniu/qmgo` 41 | 42 | ## Usage 43 | 44 | - Start 45 | 46 | `import` and create a new connection 47 | ```go 48 | import ( 49 | "context" 50 | 51 | "github.com/qiniu/qmgo" 52 | ) 53 | 54 | ctx := context.Background() 55 | client, err := qmgo.NewClient(ctx, &qmgo.Config{Uri: "mongodb://localhost:27017"}) 56 | db := client.Database("class") 57 | coll := db.Collection("user") 58 | ``` 59 | If your connection points to a fixed database and collection, recommend using the following way to initialize the connection. 60 | All operations can be based on `cli`: 61 | 62 | ```go 63 | cli, err := qmgo.Open(ctx, &qmgo.Config{Uri: "mongodb://localhost:27017", Database: "class", Coll: "user"}) 64 | ``` 65 | 66 | ***The following examples will be based on `cli`, if you use the first way for initialization, replace `cli` with `client`、`db` or `coll`*** 67 | 68 | Make sure to defer a call to Disconnect after instantiating your client: 69 | 70 | ```go 71 | defer func() { 72 | if err = cli.Close(ctx); err != nil { 73 | panic(err) 74 | } 75 | }() 76 | ``` 77 | 78 | - Create index 79 | 80 | Before doing the operation, we first initialize some data: 81 | 82 | ```go 83 | type UserInfo struct { 84 | Name string `bson:"name"` 85 | Age uint16 `bson:"age"` 86 | Weight uint32 `bson:"weight"` 87 | } 88 | 89 | var userInfo = UserInfo{ 90 | Name: "xm", 91 | Age: 7, 92 | Weight: 40, 93 | } 94 | ``` 95 | 96 | Create index 97 | 98 | ```go 99 | cli.CreateOneIndex(context.Background(), options.IndexModel{Key: []string{"name"}}) 100 | cli.CreateIndexes(context.Background(), []options.IndexModel{{Key: []string{"id2", "id3"}}}) 101 | ``` 102 | 103 | - Insert a document 104 | 105 | ```go 106 | // insert one document 107 | result, err := cli.InsertOne(ctx, userInfo) 108 | ``` 109 | 110 | - Find a document 111 | 112 | ```go 113 | // find one document 114 | one := UserInfo{} 115 | err = cli.Find(ctx, bson.M{"name": userInfo.Name}).One(&one) 116 | ``` 117 | 118 | - Delete documents 119 | 120 | ```go 121 | err = cli.Remove(ctx, bson.M{"age": 7}) 122 | ``` 123 | 124 | - Insert multiple data 125 | 126 | ```go 127 | // multiple insert 128 | var userInfos = []UserInfo{ 129 | UserInfo{Name: "a1", Age: 6, Weight: 20}, 130 | UserInfo{Name: "b2", Age: 6, Weight: 25}, 131 | UserInfo{Name: "c3", Age: 6, Weight: 30}, 132 | UserInfo{Name: "d4", Age: 6, Weight: 35}, 133 | UserInfo{Name: "a1", Age: 7, Weight: 40}, 134 | UserInfo{Name: "a1", Age: 8, Weight: 45}, 135 | } 136 | result, err = cli.Collection.InsertMany(ctx, userInfos) 137 | ``` 138 | 139 | - Search all, sort and limit 140 | ```go 141 | // find all, sort and limit 142 | batch := []UserInfo{} 143 | cli.Find(ctx, bson.M{"age": 6}).Sort("weight").Limit(7).All(&batch) 144 | ``` 145 | - Count 146 | 147 | ````go 148 | count, err := cli.Find(ctx, bson.M{"age": 6}).Count() 149 | ```` 150 | 151 | - Update 152 | 153 | ````go 154 | // UpdateOne one 155 | err := cli.UpdateOne(ctx, bson.M{"name": "d4"}, bson.M{"$set": bson.M{"age": 7}}) 156 | 157 | // UpdateAll 158 | result, err := cli.UpdateAll(ctx, bson.M{"age": 6}, bson.M{"$set": bson.M{"age": 10}}) 159 | ```` 160 | 161 | - Select 162 | 163 | ````go 164 | err := cli.Find(ctx, bson.M{"age": 10}).Select(bson.M{"age": 1}).One(&one) 165 | ```` 166 | 167 | - Aggregate 168 | 169 | ```go 170 | matchStage := bson.D{{"$match", []bson.E{{"weight", bson.D{{"$gt", 30}}}}}} 171 | groupStage := bson.D{{"$group", bson.D{{"_id", "$name"}, {"total", bson.D{{"$sum", "$age"}}}}}} 172 | var showsWithInfo []bson.M 173 | err = cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}).All(&showsWithInfo) 174 | ``` 175 | 176 | - Support All mongoDB Options when create connection 177 | 178 | ````go 179 | poolMonitor := &event.PoolMonitor{ 180 | Event: func(evt *event.PoolEvent) { 181 | switch evt.Type { 182 | case event.GetSucceeded: 183 | fmt.Println("GetSucceeded") 184 | case event.ConnectionReturned: 185 | fmt.Println("ConnectionReturned") 186 | } 187 | }, 188 | } 189 | opt := options.Client().SetPoolMonitor(poolMonitor) // more options use the chain options. 190 | cli, err := Open(ctx, &Config{Uri: URI, Database: DATABASE, Coll: COLL}, opt) 191 | 192 | 193 | ```` 194 | 195 | - Transactions 196 | 197 | The super simple and powerful transaction, with features like `timeout`、`retry`: 198 | ````go 199 | callback := func(sessCtx context.Context) (interface{}, error) { 200 | // Important: make sure the sessCtx used in every operation in the whole transaction 201 | if _, err := cli.InsertOne(sessCtx, bson.D{{"abc", int32(1)}}); err != nil { 202 | return nil, err 203 | } 204 | if _, err := cli.InsertOne(sessCtx, bson.D{{"xyz", int32(999)}}); err != nil { 205 | return nil, err 206 | } 207 | return nil, nil 208 | } 209 | result, err = cli.DoTransaction(ctx, callback) 210 | ```` 211 | [More about transaction](https://github.com/qiniu/qmgo/wiki/Transactions) 212 | 213 | - Predefine operator keys 214 | 215 | ````go 216 | // aggregate 217 | matchStage := bson.D{{operator.Match, []bson.E{{"weight", bson.D{{operator.Gt, 30}}}}}} 218 | groupStage := bson.D{{operator.Group, bson.D{{"_id", "$name"}, {"total", bson.D{{operator.Sum, "$age"}}}}}} 219 | var showsWithInfo []bson.M 220 | err = cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}).All(&showsWithInfo) 221 | ```` 222 | 223 | - Hooks 224 | 225 | Qmgo flexible hooks: 226 | 227 | ````go 228 | type User struct { 229 | Name string `bson:"name"` 230 | Age int `bson:"age"` 231 | } 232 | func (u *User) BeforeInsert(ctx context.Context) error { 233 | fmt.Println("before insert called") 234 | return nil 235 | } 236 | func (u *User) AfterInsert(ctx context.Context) error { 237 | fmt.Println("after insert called") 238 | return nil 239 | } 240 | 241 | u := &User{Name: "Alice", Age: 7} 242 | _, err := cli.InsertOne(context.Background(), u) 243 | ```` 244 | [More about hooks](https://github.com/qiniu/qmgo/wiki/Hooks) 245 | 246 | - Automatically fields 247 | 248 | Qmgo support two ways to make specific fields automatically update in specific API 249 | 250 | - Default fields 251 | 252 | Inject `field.DefaultField` in document struct, Qmgo will update `createAt`、`updateAt` and `_id` in update and insert operation. 253 | 254 | ````go 255 | type User struct { 256 | field.DefaultField `bson:",inline"` 257 | 258 | Name string `bson:"name"` 259 | Age int `bson:"age"` 260 | } 261 | 262 | u := &User{Name: "Lucas", Age: 7} 263 | _, err := cli.InsertOne(context.Background(), u) 264 | // Fields with tag createAt、updateAt and _id will be generated automatically 265 | ```` 266 | 267 | - Custom fields 268 | 269 | Define the custom fields, Qmgo will update them in update and insert operation. 270 | 271 | ```go 272 | type User struct { 273 | Name string `bson:"name"` 274 | Age int `bson:"age"` 275 | 276 | MyId string `bson:"myId"` 277 | CreateTimeAt time.Time `bson:"createTimeAt"` 278 | UpdateTimeAt int64 `bson:"updateTimeAt"` 279 | } 280 | // Define the custom fields 281 | func (u *User) CustomFields() field.CustomFieldsBuilder { 282 | return field.NewCustom().SetCreateAt("CreateTimeAt").SetUpdateAt("UpdateTimeAt").SetId("MyId") 283 | } 284 | 285 | u := &User{Name: "Lucas", Age: 7} 286 | _, err := cli.InsertOne(context.Background(), u) 287 | // CreateTimeAt、UpdateTimeAt and MyId will be generated automatically 288 | 289 | // suppose Id and ui is ready 290 | err = cli.ReplaceOne(context.Background(), bson.M{"_id": Id}, &ui) 291 | // UpdateTimeAt will update 292 | ``` 293 | 294 | Check [examples here](https://github.com/qiniu/qmgo/blob/master/field_test.go) 295 | 296 | [More about automatically fields](https://github.com/qiniu/qmgo/wiki/Automatically-update-fields) 297 | 298 | - Validation tags 299 | 300 | Qmgo Validation tags is Based on [go-playground/validator](https://github.com/go-playground/validator). 301 | 302 | So Qmgo support [all validations on structs in go-playground/validator](https://github.com/go-playground/validator#usage-and-documentation), such as: 303 | 304 | ```go 305 | type User struct { 306 | FirstName string `bson:"fname"` 307 | LastName string `bson:"lname"` 308 | Age uint8 `bson:"age" validate:"gte=0,lte=130" ` // Age must in [0,130] 309 | Email string `bson:"e-mail" validate:"required,email"` // Email can't be empty string, and must has email format 310 | CreateAt time.Time `bson:"createAt" validate:"lte"` // CreateAt must lte than current time 311 | Relations map[string]string `bson:"relations" validate:"max=2"` // Relations can't has more than 2 elements 312 | } 313 | ``` 314 | 315 | Qmgo tags only supported in following API: 316 | ` InsertOne、InsertyMany、Upsert、UpsertId、ReplaceOne ` 317 | 318 | - Plugin 319 | 320 | - Implement following method: 321 | 322 | ```go 323 | func Do(ctx context.Context, doc interface{}, opType operator.OpType, opts ...interface{}) error{ 324 | // do anything 325 | } 326 | ``` 327 | 328 | - Call Register() in package middleware, register the method `Do` 329 | 330 | Qmgo will call `Do` before and after the [operation](operator/operate_type.go) 331 | 332 | ```go 333 | middleware.Register(Do) 334 | ``` 335 | [Example](middleware/middleware_test.go) 336 | 337 | The `hook`、`automatically fields` and `validation tags` in Qmgo run on **plugin**. 338 | 339 | ## `Qmgo` vs `go.mongodb.org/mongo-driver` 340 | 341 | Below we give an example of multi-file search、sort and limit to illustrate the similarities between `qmgo` and `mgo` and the improvement compare to `go.mongodb.org/mongo-driver`. 342 | How do we do in`go.mongodb.org/mongo-driver`: 343 | 344 | ```go 345 | // go.mongodb.org/mongo-driver 346 | // find all, sort and limit 347 | findOptions := options.Find() 348 | findOptions.SetLimit(7) // set limit 349 | var sorts D 350 | sorts = append(sorts, E{Key: "weight", Value: 1}) 351 | findOptions.SetSort(sorts) // set sort 352 | 353 | batch := []UserInfo{} 354 | cur, err := coll.Find(ctx, bson.M{"age": 6}, findOptions) 355 | cur.All(ctx, &batch) 356 | ``` 357 | 358 | How do we do in `Qmgo` and `mgo`: 359 | 360 | ```go 361 | // qmgo 362 | // find all, sort and limit 363 | batch := []UserInfo{} 364 | cli.Find(ctx, bson.M{"age": 6}).Sort("weight").Limit(7).All(&batch) 365 | 366 | // mgo 367 | // find all, sort and limit 368 | coll.Find(bson.M{"age": 6}).Sort("weight").Limit(7).All(&batch) 369 | ``` 370 | 371 | ## `Qmgo` vs `mgo` 372 | [Differences between qmgo and mgo](https://github.com/qiniu/qmgo/wiki/Differences-between-Qmgo-and-Mgo) 373 | 374 | ## Contributing 375 | 376 | The Qmgo project welcomes all contributors. We appreciate your help! 377 | 378 | ## Communication: 379 | 380 | - Join [qmgo discussions](https://github.com/qiniu/qmgo/discussions) 381 | 382 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # Qmgo 2 | 3 | `Qmgo` 是一款`Go`语言的`MongoDB` `driver`,它基于[MongoDB 官方 driver](https://github.com/mongodb/mongo-go-driver) 开发实现,同时使用更易用的接口设计,比如参考[mgo](https://github.com/go-mgo/mgo) (比如`mgo`的链式调用)。 4 | 5 | - `Qmgo`让您以更优雅的姿势使用`MongoDB`的新特性。 6 | 7 | - `Qmgo`是从`mgo`迁移到新`MongoDB driver`的第一选择,对代码的改动影响最小。 8 | 9 | ## 要求 10 | 11 | - `Go 1.10` 及以上。 12 | - `MongoDB 2.6` 及以上。 13 | 14 | ## 功能 15 | 16 | - 文档的增删改查, 均支持官方driver支持的所有options 17 | - `Sort`、`limit`、`count`、`select`、`distinct` 18 | - 事务 19 | - `Hooks` 20 | - 自动化更新的默认和定制fields 21 | - 预定义操作符 22 | - 聚合`Aggregate`、索引操作、`cursor` 23 | - `validation tags` 基于tag的字段验证 24 | - 可自定义插件化编程 25 | 26 | ## 安装 27 | 28 | 推荐方式是使用`go mod`,通过在源码中`import github.com/qiniu/qmgo` 来自动安装依赖。 29 | 30 | 当然,通过下面方式同样可行: 31 | 32 | ``` 33 | go get github.com/qiniu/qmgo 34 | ``` 35 | 36 | ## Usage 37 | 38 | - 开始 39 | 40 | `import`并新建连接 41 | 42 | ```go 43 | import( 44 | "context" 45 | 46 | "github.com/qiniu/qmgo" 47 | ) 48 | 49 | ctx := context.Background() 50 | client, err := qmgo.NewClient(ctx, &qmgo.Config{Uri: "mongodb://localhost:27017"}) 51 | db := client.Database("class") 52 | coll := db.Collection("user") 53 | 54 | ``` 55 | 56 | 如果你的连接是指向固定的 database 和 collection,我们推荐使用下面的更方便的方法初始化连接,后续操作都基于`cli`而不用再关心 database 和 collection 57 | 58 | ```go 59 | cli, err := qmgo.Open(ctx, &qmgo.Config{Uri: "mongodb://localhost:27017", Database: "class", Coll: "user"}) 60 | ``` 61 | 62 | **_后面都会基于`cli`来举例,如果你使用第一种传统的方式进行初始化,根据上下文,将`cli`替换成`client`、`db` 或 `coll`即可_** 63 | 64 | 在初始化成功后,请`defer`来关闭连接 65 | 66 | ```go 67 | defer func() { 68 | if err = cli.Close(ctx); err != nil { 69 | panic(err) 70 | } 71 | }() 72 | ``` 73 | 74 | - 创建索引 75 | 76 | 做操作前,我们先初始化一些数据: 77 | 78 | ```go 79 | 80 | type UserInfo struct { 81 | Name string `bson:"name"` 82 | Age uint16 `bson:"age"` 83 | Weight uint32 `bson:"weight"` 84 | } 85 | 86 | var userInfo = UserInfo{ 87 | Name: "xm", 88 | Age: 7, 89 | Weight: 40, 90 | } 91 | ``` 92 | 93 | 创建索引 94 | 95 | ```go 96 | cli.CreateOneIndex(context.Background(), options.IndexModel{Key: []string{"name"}}) 97 | cli.CreateIndexes(context.Background(), []options.IndexModel{{Key: []string{"id2", "id3"}}}) 98 | ``` 99 | 100 | - 插入一个文档 101 | 102 | ```go 103 | // insert one document 104 | result, err := cli.InsertOne(ctx, userInfo) 105 | ``` 106 | 107 | - 查找一个文档 108 | 109 | ```go 110 | // find one document 111 | one := UserInfo{} 112 | err = cli.Find(ctx, bson.M{"name": userInfo.Name}).One(&one) 113 | ``` 114 | 115 | - 删除文档 116 | 117 | ```go 118 | err = cli.Remove(ctx, bson.M{"age": 7}) 119 | ``` 120 | 121 | - 插入多条数据 122 | 123 | ```go 124 | // multiple insert 125 | var userInfos = []UserInfo{ 126 | UserInfo{Name: "a1", Age: 6, Weight: 20}, 127 | UserInfo{Name: "b2", Age: 6, Weight: 25}, 128 | UserInfo{Name: "c3", Age: 6, Weight: 30}, 129 | UserInfo{Name: "d4", Age: 6, Weight: 35}, 130 | UserInfo{Name: "a1", Age: 7, Weight: 40}, 131 | UserInfo{Name: "a1", Age: 8, Weight: 45}, 132 | } 133 | result, err = cli.Collection.InsertMany(ctx, userInfos) 134 | ``` 135 | 136 | - 批量查找、`Sort`和`Limit` 137 | 138 | ```go 139 | // find all 、sort and limit 140 | batch := []UserInfo{} 141 | cli.Find(ctx, bson.M{"age": 6}).Sort("weight").Limit(7).All(&batch) 142 | ``` 143 | 144 | - Count 145 | 146 | ```go 147 | count, err := cli.Find(ctx, bson.M{"age": 6}).Count() 148 | ``` 149 | 150 | - Update 151 | 152 | ```go 153 | // UpdateOne one 154 | err := cli.UpdateOne(ctx, bson.M{"name": "d4"}, bson.M{"$set": bson.M{"age": 7}}) 155 | 156 | // UpdateAll 157 | result, err := cli.UpdateAll(ctx, bson.M{"age": 6}, bson.M{"$set": bson.M{"age": 10}}) 158 | ``` 159 | 160 | - Select 161 | 162 | ```go 163 | err := cli.Find(ctx, bson.M{"age": 10}).Select(bson.M{"age": 1}).One(&one) 164 | ``` 165 | 166 | - Aggregate 167 | 168 | ```go 169 | matchStage := bson.D{{"$match", []bson.E{{"weight", bson.D{{"$gt", 30}}}}}} 170 | groupStage := bson.D{{"$group", bson.D{{"_id", "$name"}, {"total", bson.D{{"$sum", "$age"}}}}}} 171 | var showsWithInfo []bson.M 172 | err = cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}).All(&showsWithInfo) 173 | ``` 174 | 175 | - 建立连接时支持所有 mongoDB 的`Options` 176 | 177 | ```go 178 | poolMonitor := &event.PoolMonitor{ 179 | Event: func(evt *event.PoolEvent) { 180 | switch evt.Type { 181 | case event.GetSucceeded: 182 | fmt.Println("GetSucceeded") 183 | case event.ConnectionReturned: 184 | fmt.Println("ConnectionReturned") 185 | } 186 | }, 187 | } 188 | 189 | opt := options.Client().SetPoolMonitor(poolMonitor) // more options use the chain options. 190 | cli, err := Open(ctx, &Config{Uri: URI, Database: DATABASE, Coll: COLL}, opt) 191 | 192 | ``` 193 | 194 | - 事务 195 | 196 | 有史以来最简单和强大的事务, 同时还有超时和重试等功能: 197 | 198 | ```go 199 | callback := func(sessCtx context.Context) (interface{}, error) { 200 | // 重要:确保事务中的每一个操作,都使用传入的sessCtx参数 201 | if _, err := cli.InsertOne(sessCtx, bson.D{{"abc", int32(1)}}); err != nil { 202 | return nil, err 203 | } 204 | if _, err := cli.InsertOne(sessCtx, bson.D{{"xyz", int32(999)}}); err != nil { 205 | return nil, err 206 | } 207 | return nil, nil 208 | } 209 | result, err = cli.DoTransaction(ctx, callback) 210 | ``` 211 | 212 | [关于事务的更多内容](https://github.com/qiniu/qmgo/wiki/Transactions) 213 | 214 | - 预定义操作符 215 | 216 | ```go 217 | // aggregate 218 | matchStage := bson.D{{operator.Match, []bson.E{{"weight", bson.D{{operator.Gt, 30}}}}}} 219 | groupStage := bson.D{{operator.Group, bson.D{{"_id", "$name"}, {"total", bson.D{{operator.Sum, "$age"}}}}}} 220 | var showsWithInfo []bson.M 221 | err = cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}).All(&showsWithInfo) 222 | ``` 223 | 224 | - Hooks 225 | 226 | Qmgo 灵活的 hooks: 227 | 228 | ```go 229 | type User struct { 230 | Name string `bson:"name"` 231 | Age int `bson:"age"` 232 | } 233 | func (u *User) BeforeInsert(ctx context.Context) error { 234 | fmt.Println("before insert called") 235 | return nil 236 | } 237 | func (u *User) AfterInsert(ctx context.Context) error { 238 | fmt.Println("after insert called") 239 | return nil 240 | } 241 | 242 | u := &User{Name: "Alice", Age: 7} 243 | _, err := cli.InsertOne(context.Background(), u) 244 | ``` 245 | 246 | [Hooks 详情介绍]() 247 | 248 | 249 | - 自动化更新fields 250 | 251 | Qmgo支持2种方式来自动化更新特定的字段 252 | 253 | - 默认 fields 254 | 255 | 在文档结构体里注入 `field.DefaultField`, `Qmgo` 会自动在更新和插入操作时更新 `createAt`、`updateAt` and `_id` field的值. 256 | 257 | ````go 258 | type User struct { 259 | field.DefaultField `bson:",inline"` 260 | 261 | Name string `bson:"name"` 262 | Age int `bson:"age"` 263 | } 264 | 265 | u := &User{Name: "Lucas", Age: 7} 266 | _, err := cli.InsertOne(context.Background(), u) 267 | // tag为createAt、updateAt 和 _id 的字段会自动更新插入 268 | ```` 269 | 270 | - Custom fields 271 | 272 | 可以自定义field名, `Qmgo` 会自动在更新和插入操作时更新他们. 273 | 274 | ```go 275 | type User struct { 276 | Name string `bson:"name"` 277 | Age int `bson:"age"` 278 | 279 | MyId string `bson:"myId"` 280 | CreateTimeAt time.Time `bson:"createTimeAt"` 281 | UpdateTimeAt int64 `bson:"updateTimeAt"` 282 | } 283 | // 指定自定义field的field名 284 | func (u *User) CustomFields() field.CustomFieldsBuilder { 285 | return field.NewCustom().SetCreateAt("CreateTimeAt").SetUpdateAt("UpdateTimeAt").SetId("MyId") 286 | } 287 | 288 | u := &User{Name: "Lucas", Age: 7} 289 | _, err := cli.InsertOne(context.Background(), u) 290 | // CreateTimeAt、UpdateTimeAt and MyId 会自动更新并插入DB 291 | 292 | // 假设Id和ui已经初始化 293 | err = cli.ReplaceOne(context.Background(), bson.M{"_id": Id}, &ui) 294 | // UpdateTimeAt 会被自动更新 295 | ``` 296 | 297 | [例子介绍](https://github.com/qiniu/qmgo/blob/master/field_test.go) 298 | 299 | [自动化 fields 详情介绍](https://github.com/qiniu/qmgo/wiki/Automatically-update-fields) 300 | 301 | - `Validation tags` 基于tag的字段验证 302 | 303 | 功能基于[go-playground/validator](https://github.com/go-playground/validator)实现。 304 | 305 | 所以`Qmgo`支持所有[go-playground/validator 的struct验证规则](https://github.com/go-playground/validator#usage-and-documentation),比如: 306 | ```go 307 | type User struct { 308 | FirstName string `bson:"fname"` 309 | LastName string `bson:"lname"` 310 | Age uint8 `bson:"age" validate:"gte=0,lte=130" ` // Age must in [0,130] 311 | Email string `bson:"e-mail" validate:"required,email"` // Email can't be empty string, and must has email format 312 | CreateAt time.Time `bson:"createAt" validate:"lte"` // CreateAt must lte than current time 313 | Relations map[string]string `bson:"relations" validate:"max=2"` // Relations can't has more than 2 elements 314 | } 315 | ``` 316 | 317 | 本功能只对以下API有效: 318 | ` InsertOne、InsertyMany、Upsert、UpsertId、ReplaceOne ` 319 | 320 | - 插件化编程 321 | 322 | - 实现以下方法 323 | ```go 324 | func Do(ctx context.Context, doc interface{}, opType operator.OpType, opts ...interface{}) error{ 325 | // do anything 326 | } 327 | ``` 328 | 329 | - 调用middleware包的Register方法,注入`Do` 330 | Qmgo会在支持的[操作](operator/operate_type.go)执行前后调用`Do` 331 | ```go 332 | middleware.Register(Do) 333 | ``` 334 | [Example](middleware/middleware_test.go) 335 | 336 | Qmgo的hook、自动更新field和validation tags都基于plugin的方式实现 337 | 338 | ## `qmgo` vs `go.mongodb.org/mongo-driver` 339 | 340 | 下面我们举一个多文件查找、`sort`和`limit`的例子, 说明`qmgo`和`mgo`的相似,以及对`go.mongodb.org/mongo-driver`的改进 341 | 342 | 官方`Driver`需要这样实现 343 | 344 | ```go 345 | // go.mongodb.org/mongo-driver 346 | // find all 、sort and limit 347 | findOptions := options.Find() 348 | findOptions.SetLimit(7) // set limit 349 | var sorts D 350 | sorts = append(sorts, E{Key: "weight", Value: 1}) 351 | findOptions.SetSort(sorts) // set sort 352 | 353 | batch := []UserInfo{} 354 | cur, err := coll.Find(ctx, bson.M{"age": 6}, findOptions) 355 | cur.All(ctx, &batch) 356 | ``` 357 | 358 | `Qmgo`和`mgo`更简单,而且实现相似: 359 | 360 | ```go 361 | // qmgo 362 | // find all 、sort and limit 363 | batch := []UserInfo{} 364 | cli.Find(ctx, bson.M{"age": 6}).Sort("weight").Limit(7).All(&batch) 365 | 366 | // mgo 367 | // find all 、sort and limit 368 | coll.Find(bson.M{"age": 6}).Sort("weight").Limit(7).All(&batch) 369 | ``` 370 | 371 | ## `Qmgo` vs `mgo` 372 | 373 | [Qmgo 和 Mgo 的差异](https://github.com/qiniu/qmgo/wiki/Differences-between-Qmgo-and-Mgo) 374 | 375 | ## Contributing 376 | 377 | 非常欢迎您对`Qmgo`的任何贡献,非常感谢您的帮助! 378 | 379 | 380 | ## 沟通交流: 381 | 382 | - 加入 [qmgo discussions](https://github.com/qiniu/qmgo/discussions) 383 | -------------------------------------------------------------------------------- /aggregate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | opts "github.com/qiniu/qmgo/options" 19 | "go.mongodb.org/mongo-driver/mongo/options" 20 | 21 | "go.mongodb.org/mongo-driver/bson" 22 | "go.mongodb.org/mongo-driver/mongo" 23 | ) 24 | 25 | // Pipeline define the pipeline for aggregate 26 | type Pipeline []bson.D 27 | 28 | // Aggregate is a handle to a aggregate 29 | type Aggregate struct { 30 | ctx context.Context 31 | pipeline interface{} 32 | collection *mongo.Collection 33 | options []opts.AggregateOptions 34 | } 35 | 36 | // All iterates the cursor from aggregate and decodes each document into results. 37 | func (a *Aggregate) All(results interface{}) error { 38 | opts := options.Aggregate() 39 | if len(a.options) > 0 { 40 | opts = a.options[0].AggregateOptions 41 | } 42 | c, err := a.collection.Aggregate(a.ctx, a.pipeline, opts) 43 | if err != nil { 44 | return err 45 | } 46 | return c.All(a.ctx, results) 47 | } 48 | 49 | // One iterates the cursor from aggregate and decodes current document into result. 50 | func (a *Aggregate) One(result interface{}) error { 51 | opts := options.Aggregate() 52 | if len(a.options) > 0 { 53 | opts = a.options[0].AggregateOptions 54 | } 55 | c, err := a.collection.Aggregate(a.ctx, a.pipeline, opts) 56 | if err != nil { 57 | return err 58 | } 59 | cr := Cursor{ 60 | ctx: a.ctx, 61 | cursor: c, 62 | err: err, 63 | } 64 | defer cr.Close() 65 | if !cr.Next(result) { 66 | if err := cr.Err(); err != nil { 67 | return err 68 | } 69 | return ErrNoSuchDocuments 70 | } 71 | return err 72 | } 73 | 74 | // Iter return the cursor after aggregate 75 | // Deprecated, please use Cursor 76 | func (a *Aggregate) Iter() CursorI { 77 | return a.Cursor() 78 | } 79 | 80 | // Cursor return the cursor after aggregate 81 | func (a *Aggregate) Cursor() CursorI { 82 | opts := options.Aggregate() 83 | if len(a.options) > 0 { 84 | opts = a.options[0].AggregateOptions 85 | } 86 | c, err := a.collection.Aggregate(a.ctx, a.pipeline, opts) 87 | return &Cursor{ 88 | ctx: a.ctx, 89 | cursor: c, 90 | err: err, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /aggregate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | opts "github.com/qiniu/qmgo/options" 20 | "go.mongodb.org/mongo-driver/mongo/options" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/require" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/bson/primitive" 26 | ) 27 | 28 | func TestAggregate(t *testing.T) { 29 | ast := require.New(t) 30 | cli := initClient("test") 31 | defer cli.Close(context.Background()) 32 | defer cli.DropCollection(context.Background()) 33 | cli.EnsureIndexes(context.Background(), nil, []string{"name"}) 34 | 35 | id1 := primitive.NewObjectID() 36 | id2 := primitive.NewObjectID() 37 | id3 := primitive.NewObjectID() 38 | id4 := primitive.NewObjectID() 39 | id5 := primitive.NewObjectID() 40 | docs := []interface{}{ 41 | QueryTestItem{Id: id1, Name: "Alice", Age: 10}, 42 | QueryTestItem{Id: id2, Name: "Alice", Age: 12}, 43 | QueryTestItem{Id: id3, Name: "Lucas", Age: 33}, 44 | QueryTestItem{Id: id4, Name: "Lucas", Age: 22}, 45 | QueryTestItem{Id: id5, Name: "Lucas", Age: 44}, 46 | } 47 | cli.InsertMany(context.Background(), docs) 48 | matchStage := bson.D{{"$match", []bson.E{{"age", bson.D{{"$gt", 11}}}}}} 49 | groupStage := bson.D{{"$group", bson.D{{"_id", "$name"}, {"total", bson.D{{"$sum", "$age"}}}}}} 50 | var showsWithInfo []bson.M 51 | 52 | opt := opts.AggregateOptions{ 53 | AggregateOptions: options.Aggregate().SetAllowDiskUse(true), 54 | } 55 | // aggregate ALL() 56 | err := cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}, opt).All(&showsWithInfo) 57 | ast.NoError(err) 58 | ast.Equal(2, len(showsWithInfo)) 59 | for _, v := range showsWithInfo { 60 | if "Alice" == v["_id"] { 61 | ast.Equal(int32(12), v["total"]) 62 | continue 63 | } 64 | if "Lucas" == v["_id"] { 65 | ast.Equal(int32(99), v["total"]) 66 | continue 67 | } 68 | ast.Error(errors.New("error"), "impossible") 69 | } 70 | // Iter() 71 | iter := cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}) 72 | ast.NotNil(iter) 73 | err = iter.All(&showsWithInfo) 74 | ast.NoError(err) 75 | for _, v := range showsWithInfo { 76 | if "Alice" == v["_id"] { 77 | ast.Equal(int32(12), v["total"]) 78 | continue 79 | } 80 | if "Lucas" == v["_id"] { 81 | ast.Equal(int32(99), v["total"]) 82 | continue 83 | } 84 | ast.Error(errors.New("error"), "impossible") 85 | } 86 | // One() 87 | var oneInfo bson.M 88 | 89 | opt = opts.AggregateOptions{ 90 | AggregateOptions: options.Aggregate().SetAllowDiskUse(true), 91 | } 92 | iter = cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}, opt) 93 | ast.NotNil(iter) 94 | iter = cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}) 95 | ast.NotNil(iter) 96 | err = iter.One(&oneInfo) 97 | ast.NoError(err) 98 | ast.Equal(true, oneInfo["_id"] == "Alice" || oneInfo["_id"] == "Lucas") 99 | 100 | // iter 101 | iter = cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}, opt) 102 | ast.NotNil(iter) 103 | 104 | i := iter.Iter() 105 | 106 | ct := i.Next(&oneInfo) 107 | ast.Equal(true, oneInfo["_id"] == "Alice" || oneInfo["_id"] == "Lucas") 108 | ast.Equal(true, ct) 109 | ct = i.Next(&oneInfo) 110 | ast.Equal(true, oneInfo["_id"] == "Alice" || oneInfo["_id"] == "Lucas") 111 | ast.Equal(true, ct) 112 | ct = i.Next(&oneInfo) 113 | ast.Equal(false, ct) 114 | 115 | // err 116 | ast.Error(cli.Aggregate(context.Background(), 1).All(&showsWithInfo)) 117 | ast.Error(cli.Aggregate(context.Background(), 1).One(&showsWithInfo)) 118 | ast.Error(cli.Aggregate(context.Background(), 1).Iter().Err()) 119 | matchStage = bson.D{{"$match", []bson.E{{"age", bson.D{{"$gt", 100}}}}}} 120 | groupStage = bson.D{{"$group", bson.D{{"_id", "$name"}, {"total", bson.D{{"$sum", "$age"}}}}}} 121 | ast.Error(cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}).One(&showsWithInfo)) 122 | 123 | } 124 | -------------------------------------------------------------------------------- /bson.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import "go.mongodb.org/mongo-driver/bson" 17 | 18 | // alias mongo drive bson primitives 19 | // thus user don't need to import go.mongodb.org/mongo-driver/mongo, it's all in qmgo 20 | type ( 21 | // M is an alias of bson.M 22 | M = bson.M 23 | // A is an alias of bson.A 24 | A = bson.A 25 | // D is an alias of bson.D 26 | D = bson.D 27 | // E is an alias of bson.E 28 | E = bson.E 29 | ) 30 | -------------------------------------------------------------------------------- /bulk.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | 19 | "go.mongodb.org/mongo-driver/bson" 20 | "go.mongodb.org/mongo-driver/mongo" 21 | "go.mongodb.org/mongo-driver/mongo/options" 22 | ) 23 | 24 | // BulkResult is the result type returned by Bulk.Run operation. 25 | type BulkResult struct { 26 | // The number of documents inserted. 27 | InsertedCount int64 28 | 29 | // The number of documents matched by filters in update and replace operations. 30 | MatchedCount int64 31 | 32 | // The number of documents modified by update and replace operations. 33 | ModifiedCount int64 34 | 35 | // The number of documents deleted. 36 | DeletedCount int64 37 | 38 | // The number of documents upserted by update and replace operations. 39 | UpsertedCount int64 40 | 41 | // A map of operation index to the _id of each upserted document. 42 | UpsertedIDs map[int64]interface{} 43 | } 44 | 45 | // Bulk is context for batching operations to be sent to database in a single 46 | // bulk write. 47 | // 48 | // Bulk is not safe for concurrent use. 49 | // 50 | // Notes: 51 | // 52 | // Individual operations inside a bulk do not trigger middlewares or hooks 53 | // at present. 54 | // 55 | // Different from original mgo, the qmgo implementation of Bulk does not emulate 56 | // bulk operations individually on old versions of MongoDB servers that do not 57 | // natively support bulk operations. 58 | // 59 | // Only operations supported by the official driver are exposed, that is why 60 | // InsertMany is missing from the methods. 61 | type Bulk struct { 62 | coll *Collection 63 | 64 | queue []mongo.WriteModel 65 | ordered *bool 66 | } 67 | 68 | // Bulk returns a new context for preparing bulk execution of operations. 69 | func (c *Collection) Bulk() *Bulk { 70 | return &Bulk{ 71 | coll: c, 72 | queue: nil, 73 | ordered: nil, 74 | } 75 | } 76 | 77 | // SetOrdered marks the bulk as ordered or unordered. 78 | // 79 | // If ordered, writes does not continue after one individual write fails. 80 | // Default is ordered. 81 | func (b *Bulk) SetOrdered(ordered bool) *Bulk { 82 | b.ordered = &ordered 83 | return b 84 | } 85 | 86 | // InsertOne queues an InsertOne operation for bulk execution. 87 | func (b *Bulk) InsertOne(doc interface{}) *Bulk { 88 | wm := mongo.NewInsertOneModel().SetDocument(doc) 89 | b.queue = append(b.queue, wm) 90 | return b 91 | } 92 | 93 | // Remove queues a Remove operation for bulk execution. 94 | func (b *Bulk) Remove(filter interface{}) *Bulk { 95 | wm := mongo.NewDeleteOneModel().SetFilter(filter) 96 | b.queue = append(b.queue, wm) 97 | return b 98 | } 99 | 100 | // RemoveId queues a RemoveId operation for bulk execution. 101 | func (b *Bulk) RemoveId(id interface{}) *Bulk { 102 | b.Remove(bson.M{"_id": id}) 103 | return b 104 | } 105 | 106 | // RemoveAll queues a RemoveAll operation for bulk execution. 107 | func (b *Bulk) RemoveAll(filter interface{}) *Bulk { 108 | wm := mongo.NewDeleteManyModel().SetFilter(filter) 109 | b.queue = append(b.queue, wm) 110 | return b 111 | } 112 | 113 | // Upsert queues an Upsert operation for bulk execution. 114 | // The replacement should be document without operator 115 | func (b *Bulk) Upsert(filter interface{}, replacement interface{}) *Bulk { 116 | wm := mongo.NewReplaceOneModel().SetFilter(filter).SetReplacement(replacement).SetUpsert(true) 117 | b.queue = append(b.queue, wm) 118 | return b 119 | } 120 | 121 | // UpsertOne queues an UpsertOne operation for bulk execution. 122 | // The update should contain operator 123 | func (b *Bulk) UpsertOne(filter interface{}, update interface{}) *Bulk { 124 | wm := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) 125 | b.queue = append(b.queue, wm) 126 | return b 127 | } 128 | 129 | // UpsertId queues an UpsertId operation for bulk execution. 130 | // The replacement should be document without operator 131 | func (b *Bulk) UpsertId(id interface{}, replacement interface{}) *Bulk { 132 | b.Upsert(bson.M{"_id": id}, replacement) 133 | return b 134 | } 135 | 136 | // UpdateOne queues an UpdateOne operation for bulk execution. 137 | // The update should contain operator 138 | func (b *Bulk) UpdateOne(filter interface{}, update interface{}) *Bulk { 139 | wm := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update) 140 | b.queue = append(b.queue, wm) 141 | return b 142 | } 143 | 144 | // UpdateId queues an UpdateId operation for bulk execution. 145 | // The update should contain operator 146 | func (b *Bulk) UpdateId(id interface{}, update interface{}) *Bulk { 147 | b.UpdateOne(bson.M{"_id": id}, update) 148 | return b 149 | } 150 | 151 | // UpdateAll queues an UpdateAll operation for bulk execution. 152 | // The update should contain operator 153 | func (b *Bulk) UpdateAll(filter interface{}, update interface{}) *Bulk { 154 | wm := mongo.NewUpdateManyModel().SetFilter(filter).SetUpdate(update) 155 | b.queue = append(b.queue, wm) 156 | return b 157 | } 158 | 159 | // Run executes the collected operations in a single bulk operation. 160 | // 161 | // A successful call resets the Bulk. If an error is returned, the internal 162 | // queue of operations is unchanged, containing both successful and failed 163 | // operations. 164 | func (b *Bulk) Run(ctx context.Context) (*BulkResult, error) { 165 | opts := options.BulkWriteOptions{ 166 | Ordered: b.ordered, 167 | } 168 | result, err := b.coll.collection.BulkWrite(ctx, b.queue, &opts) 169 | if err != nil { 170 | // In original mgo, queue is not reset in case of error. 171 | return nil, err 172 | } 173 | 174 | // Empty the queue for possible reuse, as per mgo's behavior. 175 | b.queue = nil 176 | 177 | return &BulkResult{ 178 | InsertedCount: result.InsertedCount, 179 | MatchedCount: result.MatchedCount, 180 | ModifiedCount: result.ModifiedCount, 181 | DeletedCount: result.DeletedCount, 182 | UpsertedCount: result.UpsertedCount, 183 | UpsertedIDs: result.UpsertedIDs, 184 | }, nil 185 | } 186 | -------------------------------------------------------------------------------- /bulk_test.go: -------------------------------------------------------------------------------- 1 | package qmgo 2 | 3 | import ( 4 | "context" 5 | 6 | "testing" 7 | 8 | "github.com/qiniu/qmgo/operator" 9 | "github.com/stretchr/testify/require" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | ) 13 | 14 | func TestBulk(t *testing.T) { 15 | ast := require.New(t) 16 | cli := initClient("test") 17 | defer cli.Close(context.Background()) 18 | defer cli.DropCollection(context.Background()) 19 | 20 | id := primitive.NewObjectID() 21 | lucas := UserInfo{Id: primitive.NewObjectID(), Name: "Lucas", Age: 12} 22 | alias := UserInfo{Id: id, Name: "Alias", Age: 21} 23 | jess := UserInfo{Id: primitive.NewObjectID(), Name: "Jess", Age: 22} 24 | joe := UserInfo{Id: primitive.NewObjectID(), Name: "Joe", Age: 22} 25 | ethanId := primitive.NewObjectID() 26 | ethan := UserInfo{Id: ethanId, Name: "Ethan", Age: 8} 27 | 28 | result, err := cli.Bulk(). 29 | InsertOne(lucas).InsertOne(alias).InsertOne(jess). 30 | UpdateOne(bson.M{"name": "Jess"}, bson.M{operator.Set: bson.M{"age": 23}}).UpdateId(id, bson.M{operator.Set: bson.M{"age": 23}}). 31 | UpdateAll(bson.M{"age": 23}, bson.M{operator.Set: bson.M{"age": 18}}). 32 | Upsert(bson.M{"age": 17}, joe).UpsertId(ethanId, ethan). 33 | Remove(bson.M{"name": "Joe"}).RemoveId(ethanId).RemoveAll(bson.M{"age": 18}). 34 | Run(context.Background()) 35 | ast.NoError(err) 36 | ast.Equal(int64(3), result.InsertedCount) 37 | ast.Equal(int64(4), result.ModifiedCount) 38 | ast.Equal(int64(4), result.DeletedCount) 39 | ast.Equal(int64(2), result.UpsertedCount) 40 | ast.Equal(2, len(result.UpsertedIDs)) 41 | ast.Equal(int64(4), result.MatchedCount) 42 | 43 | } 44 | 45 | func TestBulkUpsertOne(t *testing.T) { 46 | ast := require.New(t) 47 | cli := initClient("test") 48 | defer cli.Close(context.Background()) 49 | defer cli.DropCollection(context.Background()) 50 | 51 | result, err := cli.Bulk(). 52 | UpsertOne(bson.M{"name": "Jess"}, bson.M{operator.Set: bson.M{"age": 20}, operator.SetOnInsert: bson.M{"weight": 40}}). 53 | UpsertOne(bson.M{"name": "Jess"}, bson.M{operator.Set: bson.M{"age": 30}, operator.SetOnInsert: bson.M{"weight": 40}}). 54 | Run(context.Background()) 55 | 56 | ast.NoError(err) 57 | ast.Equal(int64(0), result.InsertedCount) 58 | ast.Equal(int64(1), result.ModifiedCount) 59 | ast.Equal(int64(0), result.DeletedCount) 60 | ast.Equal(int64(1), result.UpsertedCount) 61 | ast.Equal(1, len(result.UpsertedIDs)) 62 | ast.Equal(int64(1), result.MatchedCount) 63 | } 64 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "net/url" 20 | "strings" 21 | "time" 22 | 23 | "github.com/qiniu/qmgo/options" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/bson/bsoncodec" 26 | "go.mongodb.org/mongo-driver/mongo" 27 | officialOpts "go.mongodb.org/mongo-driver/mongo/options" 28 | "go.mongodb.org/mongo-driver/mongo/readpref" 29 | ) 30 | 31 | // Config for initial mongodb instance 32 | type Config struct { 33 | // URI example: [mongodb://][user:pass@]host1[:port1][,host2[:port2],...][/database][?options] 34 | // URI Reference: https://docs.mongodb.com/manual/reference/connection-string/ 35 | Uri string `json:"uri"` 36 | Database string `json:"database"` 37 | Coll string `json:"coll"` 38 | // ConnectTimeoutMS specifies a timeout that is used for creating connections to the server. 39 | // If set to 0, no timeout will be used. 40 | // The default is 30 seconds. 41 | ConnectTimeoutMS *int64 `json:"connectTimeoutMS"` 42 | // MaxPoolSize specifies that maximum number of connections allowed in the driver's connection pool to each server. 43 | // If this is 0, it will be set to math.MaxInt64, 44 | // The default is 100. 45 | MaxPoolSize *uint64 `json:"maxPoolSize"` 46 | // MinPoolSize specifies the minimum number of connections allowed in the driver's connection pool to each server. If 47 | // this is non-zero, each server's pool will be maintained in the background to ensure that the size does not fall below 48 | // the minimum. This can also be set through the "minPoolSize" URI option (e.g. "minPoolSize=100"). The default is 0. 49 | MinPoolSize *uint64 `json:"minPoolSize"` 50 | // SocketTimeoutMS specifies how long the driver will wait for a socket read or write to return before returning a 51 | // network error. If this is 0 meaning no timeout is used and socket operations can block indefinitely. 52 | // The default is 300,000 ms. 53 | SocketTimeoutMS *int64 `json:"socketTimeoutMS"` 54 | // ReadPreference determines which servers are considered suitable for read operations. 55 | // default is PrimaryMode 56 | ReadPreference *ReadPref `json:"readPreference"` 57 | // can be used to provide authentication options when configuring a Client. 58 | Auth *Credential `json:"auth"` 59 | } 60 | 61 | // Credential can be used to provide authentication options when configuring a Client. 62 | // 63 | // AuthMechanism: the mechanism to use for authentication. Supported values include "SCRAM-SHA-256", "SCRAM-SHA-1", 64 | // "MONGODB-CR", "PLAIN", "GSSAPI", "MONGODB-X509", and "MONGODB-AWS". This can also be set through the "authMechanism" 65 | // URI option. (e.g. "authMechanism=PLAIN"). For more information, see 66 | // https://docs.mongodb.com/manual/core/authentication-mechanisms/. 67 | // AuthSource: the name of the database to use for authentication. This defaults to "$external" for MONGODB-X509, 68 | // GSSAPI, and PLAIN and "admin" for all other mechanisms. This can also be set through the "authSource" URI option 69 | // (e.g. "authSource=otherDb"). 70 | // 71 | // Username: the username for authentication. This can also be set through the URI as a username:password pair before 72 | // the first @ character. For example, a URI for user "user", password "pwd", and host "localhost:27017" would be 73 | // "mongodb://user:pwd@localhost:27017". This is optional for X509 authentication and will be extracted from the 74 | // client certificate if not specified. 75 | // 76 | // Password: the password for authentication. This must not be specified for X509 and is optional for GSSAPI 77 | // authentication. 78 | // 79 | // PasswordSet: For GSSAPI, this must be true if a password is specified, even if the password is the empty string, and 80 | // false if no password is specified, indicating that the password should be taken from the context of the running 81 | // process. For other mechanisms, this field is ignored. 82 | type Credential struct { 83 | AuthMechanism string `json:"authMechanism"` 84 | AuthSource string `json:"authSource"` 85 | Username string `json:"username"` 86 | Password string `json:"password"` 87 | PasswordSet bool `json:"passwordSet"` 88 | } 89 | 90 | // ReadPref determines which servers are considered suitable for read operations. 91 | type ReadPref struct { 92 | // MaxStaleness is the maximum amount of time to allow a server to be considered eligible for selection. 93 | // Supported from version 3.4. 94 | MaxStalenessMS int64 `json:"maxStalenessMS"` 95 | // indicates the user's preference on reads. 96 | // PrimaryMode as default 97 | Mode readpref.Mode `json:"mode"` 98 | } 99 | 100 | // QmgoClient specifies the instance to operate mongoDB 101 | type QmgoClient struct { 102 | *Collection 103 | *Database 104 | *Client 105 | } 106 | 107 | // Open creates client instance according to config 108 | // QmgoClient can operates all qmgo.client 、qmgo.database and qmgo.collection 109 | func Open(ctx context.Context, conf *Config, o ...options.ClientOptions) (cli *QmgoClient, err error) { 110 | client, err := NewClient(ctx, conf, o...) 111 | if err != nil { 112 | fmt.Println("new client fail", err) 113 | return 114 | } 115 | 116 | db := client.Database(conf.Database) 117 | coll := db.Collection(conf.Coll) 118 | 119 | cli = &QmgoClient{ 120 | Client: client, 121 | Database: db, 122 | Collection: coll, 123 | } 124 | 125 | return 126 | } 127 | 128 | // Client creates client to mongo 129 | type Client struct { 130 | client *mongo.Client 131 | conf Config 132 | 133 | registry *bsoncodec.Registry 134 | } 135 | 136 | // NewClient creates Qmgo MongoDB client 137 | func NewClient(ctx context.Context, conf *Config, o ...options.ClientOptions) (cli *Client, err error) { 138 | opt, err := newConnectOpts(conf, o...) 139 | if err != nil { 140 | return nil, err 141 | } 142 | client, err := client(ctx, opt) 143 | if err != nil { 144 | fmt.Println("new client fail", err) 145 | return 146 | } 147 | cli = &Client{ 148 | client: client, 149 | conf: *conf, 150 | registry: opt.Registry, 151 | } 152 | return 153 | } 154 | 155 | // client creates connection to MongoDB 156 | func client(ctx context.Context, opt *officialOpts.ClientOptions) (client *mongo.Client, err error) { 157 | client, err = mongo.Connect(ctx, opt) 158 | if err != nil { 159 | fmt.Println(err) 160 | return 161 | } 162 | // half of default connect timeout 163 | pCtx, cancel := context.WithTimeout(ctx, 15*time.Second) 164 | defer cancel() 165 | if err = client.Ping(pCtx, readpref.Primary()); err != nil { 166 | fmt.Println(err) 167 | return 168 | } 169 | return 170 | } 171 | 172 | // newConnectOpts creates client options from conf 173 | // Qmgo will follow this way official mongodb driver do: 174 | // - the configuration in uri takes precedence over the configuration in the setter 175 | // - Check the validity of the configuration in the uri, while the configuration in the setter is basically not checked 176 | func newConnectOpts(conf *Config, o ...options.ClientOptions) (*officialOpts.ClientOptions, error) { 177 | option := officialOpts.Client() 178 | for _, apply := range o { 179 | option = officialOpts.MergeClientOptions(apply.ClientOptions) 180 | } 181 | if conf.ConnectTimeoutMS != nil { 182 | timeoutDur := time.Duration(*conf.ConnectTimeoutMS) * time.Millisecond 183 | option.SetConnectTimeout(timeoutDur) 184 | 185 | } 186 | if conf.SocketTimeoutMS != nil { 187 | timeoutDur := time.Duration(*conf.SocketTimeoutMS) * time.Millisecond 188 | option.SetSocketTimeout(timeoutDur) 189 | } else { 190 | option.SetSocketTimeout(300 * time.Second) 191 | } 192 | if conf.MaxPoolSize != nil { 193 | option.SetMaxPoolSize(*conf.MaxPoolSize) 194 | } 195 | if conf.MinPoolSize != nil { 196 | option.SetMinPoolSize(*conf.MinPoolSize) 197 | } 198 | if conf.ReadPreference != nil { 199 | readPreference, err := newReadPref(*conf.ReadPreference) 200 | if err != nil { 201 | return nil, err 202 | } 203 | option.SetReadPreference(readPreference) 204 | } 205 | if conf.Auth != nil { 206 | auth, err := newAuth(*conf.Auth) 207 | if err != nil { 208 | return nil, err 209 | } 210 | option.SetAuth(auth) 211 | } 212 | option.ApplyURI(conf.Uri) 213 | 214 | return option, nil 215 | } 216 | 217 | // newAuth create options.Credential from conf.Auth 218 | func newAuth(auth Credential) (credential officialOpts.Credential, err error) { 219 | if auth.AuthMechanism != "" { 220 | credential.AuthMechanism = auth.AuthMechanism 221 | } 222 | if auth.AuthSource != "" { 223 | credential.AuthSource = auth.AuthSource 224 | } 225 | if auth.Username != "" { 226 | // Validate and process the username. 227 | if strings.Contains(auth.Username, "/") { 228 | err = ErrNotSupportedUsername 229 | return 230 | } 231 | credential.Username, err = url.QueryUnescape(auth.Username) 232 | if err != nil { 233 | err = ErrNotSupportedUsername 234 | return 235 | } 236 | } 237 | credential.PasswordSet = auth.PasswordSet 238 | if auth.Password != "" { 239 | if strings.Contains(auth.Password, ":") { 240 | err = ErrNotSupportedPassword 241 | return 242 | } 243 | if strings.Contains(auth.Password, "/") { 244 | err = ErrNotSupportedPassword 245 | return 246 | } 247 | credential.Password, err = url.QueryUnescape(auth.Password) 248 | if err != nil { 249 | err = ErrNotSupportedPassword 250 | return 251 | } 252 | credential.Password = auth.Password 253 | } 254 | return 255 | } 256 | 257 | // newReadPref create readpref.ReadPref from config 258 | func newReadPref(pref ReadPref) (*readpref.ReadPref, error) { 259 | readPrefOpts := make([]readpref.Option, 0, 1) 260 | if pref.MaxStalenessMS != 0 { 261 | readPrefOpts = append(readPrefOpts, readpref.WithMaxStaleness(time.Duration(pref.MaxStalenessMS)*time.Millisecond)) 262 | } 263 | mode := readpref.PrimaryMode 264 | if pref.Mode != 0 { 265 | mode = pref.Mode 266 | } 267 | readPreference, err := readpref.New(mode, readPrefOpts...) 268 | return readPreference, err 269 | } 270 | 271 | // Close closes sockets to the topology referenced by this Client. 272 | func (c *Client) Close(ctx context.Context) error { 273 | err := c.client.Disconnect(ctx) 274 | return err 275 | } 276 | 277 | // Ping confirm connection is alive 278 | func (c *Client) Ping(timeout int64) error { 279 | var err error 280 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) 281 | defer cancel() 282 | 283 | if err = c.client.Ping(ctx, readpref.Primary()); err != nil { 284 | return err 285 | } 286 | return nil 287 | } 288 | 289 | // Database create connection to database 290 | func (c *Client) Database(name string, options ...*options.DatabaseOptions) *Database { 291 | opts := make([]*officialOpts.DatabaseOptions, 0, len(options)) 292 | for _, o := range options { 293 | opts = append(opts, o.DatabaseOptions) 294 | } 295 | databaseOpts := officialOpts.MergeDatabaseOptions(opts...) 296 | return &Database{database: c.client.Database(name, databaseOpts), registry: c.registry} 297 | } 298 | 299 | // Session create one session on client 300 | // Watch out, close session after operation done 301 | func (c *Client) Session(opt ...*options.SessionOptions) (*Session, error) { 302 | sessionOpts := officialOpts.Session() 303 | if len(opt) > 0 && opt[0].SessionOptions != nil { 304 | sessionOpts = opt[0].SessionOptions 305 | } 306 | s, err := c.client.StartSession(sessionOpts) 307 | return &Session{session: s}, err 308 | } 309 | 310 | // DoTransaction do whole transaction in one function 311 | // precondition: 312 | // - version of mongoDB server >= v4.0 313 | // - Topology of mongoDB server is not Single 314 | // At the same time, please pay attention to the following 315 | // - make sure all operations in callback use the sessCtx as context parameter 316 | // - if operations in callback takes more than(include equal) 120s, the operations will not take effect, 317 | // - if operation in callback return qmgo.ErrTransactionRetry, 318 | // the whole transaction will retry, so this transaction must be idempotent 319 | // - if operations in callback return qmgo.ErrTransactionNotSupported, 320 | // - If the ctx parameter already has a Session attached to it, it will be replaced by this session. 321 | func (c *Client) DoTransaction(ctx context.Context, callback func(sessCtx context.Context) (interface{}, error), opts ...*options.TransactionOptions) (interface{}, error) { 322 | if !c.transactionAllowed() { 323 | return nil, ErrTransactionNotSupported 324 | } 325 | s, err := c.Session() 326 | if err != nil { 327 | return nil, err 328 | } 329 | defer s.EndSession(ctx) 330 | return s.StartTransaction(ctx, callback, opts...) 331 | } 332 | 333 | // ServerVersion get the version of mongoDB server, like 4.4.0 334 | func (c *Client) ServerVersion() string { 335 | var buildInfo bson.Raw 336 | err := c.client.Database("admin").RunCommand( 337 | context.Background(), 338 | bson.D{{"buildInfo", 1}}, 339 | ).Decode(&buildInfo) 340 | if err != nil { 341 | fmt.Println("run command err", err) 342 | return "" 343 | } 344 | v, err := buildInfo.LookupErr("version") 345 | if err != nil { 346 | fmt.Println("look up err", err) 347 | return "" 348 | } 349 | return v.StringValue() 350 | } 351 | 352 | // transactionAllowed check if transaction is allowed 353 | func (c *Client) transactionAllowed() bool { 354 | vr, err := CompareVersions("4.0", c.ServerVersion()) 355 | if err != nil { 356 | return false 357 | } 358 | if vr > 0 { 359 | fmt.Println("transaction is not supported because mongo server version is below 4.0") 360 | return false 361 | } 362 | // TODO dont know why need to do `cli, err := Open(ctx, &c.conf)` in topology() to get topo, 363 | // Before figure it out, we only use this function in UT 364 | //topo, err := c.topology() 365 | //if topo == description.Single { 366 | // fmt.Println("transaction is not supported because mongo server topology is single") 367 | // return false 368 | //} 369 | return true 370 | } 371 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/qiniu/qmgo/options" 22 | "github.com/stretchr/testify/require" 23 | "go.mongodb.org/mongo-driver/bson" 24 | officialOpts "go.mongodb.org/mongo-driver/mongo/options" 25 | "go.mongodb.org/mongo-driver/mongo/readpref" 26 | ) 27 | 28 | func initClient(col string) *QmgoClient { 29 | cfg := Config{ 30 | Uri: "mongodb://localhost:27017", 31 | Database: "qmgotest", 32 | Coll: col, 33 | } 34 | var cTimeout int64 = 0 35 | var sTimeout int64 = 500000 36 | var maxPoolSize uint64 = 30000 37 | var minPoolSize uint64 = 0 38 | cfg.ConnectTimeoutMS = &cTimeout 39 | cfg.SocketTimeoutMS = &sTimeout 40 | cfg.MaxPoolSize = &maxPoolSize 41 | cfg.MinPoolSize = &minPoolSize 42 | cfg.ReadPreference = &ReadPref{Mode: readpref.PrimaryMode} 43 | qClient, err := Open(context.Background(), &cfg) 44 | if err != nil { 45 | fmt.Println(err) 46 | panic(err) 47 | } 48 | return qClient 49 | } 50 | 51 | func TestQmgoClient(t *testing.T) { 52 | ast := require.New(t) 53 | var timeout int64 = 50 54 | 55 | // uri 错误 56 | cfg := Config{ 57 | Uri: "://127.0.0.1", 58 | ConnectTimeoutMS: &timeout, 59 | } 60 | 61 | var err error 62 | _, err = Open(context.Background(), &cfg) 63 | ast.NotNil(err) 64 | 65 | // Open 成功 66 | var maxPoolSize uint64 = 100 67 | var minPoolSize uint64 = 0 68 | 69 | cfg = Config{ 70 | Uri: "mongodb://localhost:27017", 71 | Database: "qmgotest", 72 | Coll: "testopen", 73 | ConnectTimeoutMS: &timeout, 74 | MaxPoolSize: &maxPoolSize, 75 | MinPoolSize: &minPoolSize, 76 | ReadPreference: &ReadPref{Mode: readpref.SecondaryMode, MaxStalenessMS: 500}, 77 | } 78 | 79 | cli, err := Open(context.Background(), &cfg) 80 | ast.NoError(err) 81 | ast.Equal(cli.GetDatabaseName(), "qmgotest") 82 | ast.Equal(cli.GetCollectionName(), "testopen") 83 | 84 | err = cli.Ping(5) 85 | ast.NoError(err) 86 | 87 | res, err := cli.InsertOne(context.Background(), bson.D{{Key: "x", Value: 1}}) 88 | ast.NoError(err) 89 | ast.NotNil(res) 90 | 91 | cli.DropCollection(context.Background()) 92 | 93 | // close Client 94 | cli.Close(context.TODO()) 95 | _, err = cli.InsertOne(context.Background(), bson.D{{Key: "x", Value: 1}}) 96 | ast.EqualError(err, "client is disconnected") 97 | 98 | err = cli.Ping(5) 99 | ast.Error(err) 100 | 101 | // primary mode with max stalenessMS, error 102 | cfg = Config{ 103 | Uri: "mongodb://localhost:27017", 104 | Database: "qmgotest", 105 | Coll: "testopen", 106 | ConnectTimeoutMS: &timeout, 107 | MaxPoolSize: &maxPoolSize, 108 | ReadPreference: &ReadPref{Mode: readpref.PrimaryMode, MaxStalenessMS: 500}, 109 | } 110 | 111 | cli, err = Open(context.Background(), &cfg) 112 | ast.Error(err) 113 | } 114 | 115 | func TestClient(t *testing.T) { 116 | ast := require.New(t) 117 | 118 | var maxPoolSize uint64 = 100 119 | var minPoolSize uint64 = 0 120 | var timeout int64 = 50 121 | 122 | cfg := &Config{ 123 | Uri: "mongodb://localhost:27017", 124 | ConnectTimeoutMS: &timeout, 125 | MaxPoolSize: &maxPoolSize, 126 | MinPoolSize: &minPoolSize, 127 | } 128 | 129 | c, err := NewClient(context.Background(), cfg) 130 | ast.Equal(nil, err) 131 | 132 | opts := &options.DatabaseOptions{DatabaseOptions: officialOpts.Database().SetReadPreference(readpref.PrimaryPreferred())} 133 | cOpts := &options.CollectionOptions{CollectionOptions: officialOpts.Collection().SetReadPreference(readpref.PrimaryPreferred())} 134 | coll := c.Database("qmgotest", opts).Collection("testopen", cOpts) 135 | 136 | res, err := coll.InsertOne(context.Background(), bson.D{{Key: "x", Value: 1}}) 137 | ast.NoError(err) 138 | ast.NotNil(res) 139 | coll.DropCollection(context.Background()) 140 | } 141 | 142 | func TestClient_ServerVersion(t *testing.T) { 143 | ast := require.New(t) 144 | 145 | cfg := &Config{ 146 | Uri: "mongodb://localhost:27017", 147 | Database: "qmgotest", 148 | Coll: "transaction", 149 | } 150 | 151 | ctx := context.Background() 152 | cli, err := Open(ctx, cfg) 153 | ast.NoError(err) 154 | 155 | version := cli.ServerVersion() 156 | ast.NotEmpty(version) 157 | fmt.Println(version) 158 | } 159 | 160 | func TestClient_newAuth(t *testing.T) { 161 | ast := require.New(t) 162 | 163 | auth := Credential{ 164 | AuthMechanism: "PLAIN", 165 | AuthSource: "PLAIN", 166 | Username: "qmgo", 167 | Password: "123", 168 | PasswordSet: false, 169 | } 170 | cred, err := newAuth(auth) 171 | ast.NoError(err) 172 | ast.Equal(auth.PasswordSet, cred.PasswordSet) 173 | ast.Equal(auth.AuthSource, cred.AuthSource) 174 | ast.Equal(auth.AuthMechanism, cred.AuthMechanism) 175 | ast.Equal(auth.Username, cred.Username) 176 | ast.Equal(auth.Password, cred.Password) 177 | 178 | auth = Credential{ 179 | AuthMechanism: "PLAIN", 180 | AuthSource: "PLAIN", 181 | Username: "qmg/o", 182 | Password: "123", 183 | PasswordSet: false, 184 | } 185 | _, err = newAuth(auth) 186 | ast.Equal(ErrNotSupportedUsername, err) 187 | 188 | auth = Credential{ 189 | AuthMechanism: "PLAIN", 190 | AuthSource: "PLAIN", 191 | Username: "qmgo", 192 | Password: "12:3", 193 | PasswordSet: false, 194 | } 195 | _, err = newAuth(auth) 196 | ast.Equal(ErrNotSupportedPassword, err) 197 | 198 | auth = Credential{ 199 | AuthMechanism: "PLAIN", 200 | AuthSource: "PLAIN", 201 | Username: "qmgo", 202 | Password: "1/23", 203 | PasswordSet: false, 204 | } 205 | _, err = newAuth(auth) 206 | ast.Equal(ErrNotSupportedPassword, err) 207 | 208 | auth = Credential{ 209 | AuthMechanism: "PLAIN", 210 | AuthSource: "PLAIN", 211 | Username: "qmgo", 212 | Password: "1%3", 213 | PasswordSet: false, 214 | } 215 | _, err = newAuth(auth) 216 | ast.Equal(ErrNotSupportedPassword, err) 217 | 218 | auth = Credential{ 219 | AuthMechanism: "PLAIN", 220 | AuthSource: "PLAIN", 221 | Username: "q%3mgo", 222 | Password: "13", 223 | PasswordSet: false, 224 | } 225 | _, err = newAuth(auth) 226 | ast.Equal(ErrNotSupportedUsername, err) 227 | } 228 | -------------------------------------------------------------------------------- /cursor.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | 19 | "go.mongodb.org/mongo-driver/mongo" 20 | ) 21 | 22 | // Cursor struct define 23 | type Cursor struct { 24 | ctx context.Context 25 | cursor *mongo.Cursor 26 | err error 27 | } 28 | 29 | // Next gets the next document for this cursor. It returns true if there were no errors and the cursor has not been 30 | // exhausted. 31 | func (c *Cursor) Next(result interface{}) bool { 32 | if c.err != nil { 33 | return false 34 | } 35 | if c.cursor.Next(c.ctx) { 36 | err := c.cursor.Decode(result) 37 | if err != nil { 38 | c.err = err 39 | return false 40 | } 41 | return true 42 | } 43 | return false 44 | } 45 | 46 | // All iterates the cursor and decodes each document into results. The results parameter must be a pointer to a slice. 47 | // recommend to use All() in struct Query or Aggregate 48 | func (c *Cursor) All(results interface{}) error { 49 | if c.err != nil { 50 | return c.err 51 | } 52 | return c.cursor.All(c.ctx, results) 53 | } 54 | 55 | // ID returns the ID of this cursor, or 0 if the cursor has been closed or exhausted. 56 | //func (c *Cursor) ID() int64 { 57 | // if c.err != nil { 58 | // return 0 59 | // } 60 | // return c.cursor.ID() 61 | //} 62 | 63 | // Close closes this cursor. Next and TryNext must not be called after Close has been called. 64 | // When the cursor object is no longer in use, it should be actively closed 65 | func (c *Cursor) Close() error { 66 | if c.err != nil { 67 | return c.err 68 | } 69 | return c.cursor.Close(c.ctx) 70 | } 71 | 72 | // Err return the last error of Cursor, if no error occurs, return nil 73 | func (c *Cursor) Err() error { 74 | if c.err != nil { 75 | return c.err 76 | } 77 | return c.cursor.Err() 78 | } 79 | -------------------------------------------------------------------------------- /cursor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | "go.mongodb.org/mongo-driver/bson" 22 | "go.mongodb.org/mongo-driver/bson/primitive" 23 | ) 24 | 25 | func TestCursor(t *testing.T) { 26 | ast := require.New(t) 27 | cli := initClient("test") 28 | defer cli.Close(context.Background()) 29 | defer cli.DropCollection(context.Background()) 30 | cli.EnsureIndexes(context.Background(), nil, []string{"name"}) 31 | 32 | id1 := primitive.NewObjectID() 33 | id2 := primitive.NewObjectID() 34 | id3 := primitive.NewObjectID() 35 | id4 := primitive.NewObjectID() 36 | docs := []interface{}{ 37 | bson.M{"_id": id1, "name": "Alice", "age": 18}, 38 | bson.M{"_id": id2, "name": "Alice", "age": 19}, 39 | bson.M{"_id": id3, "name": "Lucas", "age": 20}, 40 | bson.M{"_id": id4, "name": "Lucas", "age": 21}, 41 | } 42 | _, err := cli.InsertMany(context.Background(), docs) 43 | ast.NoError(err) 44 | 45 | var res QueryTestItem 46 | 47 | // if query has 1 record,cursor can run Next one time, Next time return false 48 | filter1 := bson.M{ 49 | "name": "Alice", 50 | } 51 | projection1 := bson.M{ 52 | "name": 0, 53 | } 54 | 55 | cursor := cli.Find(context.Background(), filter1).Select(projection1).Sort("age").Limit(2).Skip(1).Cursor() 56 | ast.NoError(cursor.Err()) 57 | 58 | val := cursor.Next(&res) 59 | ast.Equal(true, val) 60 | ast.Equal(id2, res.Id) 61 | 62 | val = cursor.Next(&res) 63 | ast.Equal(false, val) 64 | 65 | cursor.Close() 66 | 67 | // cursor ALL 68 | cursor = cli.Find(context.Background(), filter1).Select(projection1).Sort("age").Limit(2).Cursor() 69 | ast.NoError(cursor.Err()) 70 | 71 | var results []QueryTestItem 72 | cursor.All(&results) 73 | ast.Equal(2, len(results)) 74 | // can't match record, cursor run Next and return false 75 | filter2 := bson.M{ 76 | "name": "Lily", 77 | } 78 | 79 | cursor = cli.Find(context.Background(), filter2).Cursor() 80 | ast.NoError(cursor.Err()) 81 | ast.NotNil(cursor) 82 | 83 | res = QueryTestItem{} 84 | val = cursor.Next(&res) 85 | ast.Equal(false, val) 86 | ast.Empty(res) 87 | 88 | cursor.Close() 89 | 90 | // 1 record,after cursor close,Next return false 91 | cursor = cli.Find(context.Background(), filter1).Select(projection1).Sort("age").Limit(2).Skip(1).Cursor() 92 | ast.NoError(cursor.Err()) 93 | ast.NotNil(cursor) 94 | 95 | cursor.Close() 96 | 97 | ast.Equal(false, cursor.Next(&res)) 98 | ast.NoError(cursor.Err()) 99 | 100 | // generate Cursor with err 101 | cursor = cli.Find(context.Background(), 1).Select(projection1).Sort("age").Limit(2).Skip(1).Cursor() 102 | ast.Error(cursor.Err()) 103 | //ast.Equal(int64(0), cursor.ID()) 104 | ast.Error(cursor.All(&res)) 105 | ast.Error(cursor.Close()) 106 | ast.Equal(false, cursor.Next(&res)) 107 | } 108 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/qiniu/qmgo/options" 20 | "go.mongodb.org/mongo-driver/bson/bsoncodec" 21 | "go.mongodb.org/mongo-driver/mongo" 22 | officialOpts "go.mongodb.org/mongo-driver/mongo/options" 23 | ) 24 | 25 | // Database is a handle to a MongoDB database 26 | type Database struct { 27 | database *mongo.Database 28 | 29 | registry *bsoncodec.Registry 30 | } 31 | 32 | // Collection gets collection from database 33 | func (d *Database) Collection(name string, opts ...*options.CollectionOptions) *Collection { 34 | var cp *mongo.Collection 35 | var opt = make([]*officialOpts.CollectionOptions, 0, len(opts)) 36 | for _, o := range opts { 37 | opt = append(opt, o.CollectionOptions) 38 | } 39 | collOpt := officialOpts.MergeCollectionOptions(opt...) 40 | cp = d.database.Collection(name, collOpt) 41 | 42 | return &Collection{ 43 | collection: cp, 44 | registry: d.registry, 45 | } 46 | } 47 | 48 | // GetDatabaseName returns the name of database 49 | func (d *Database) GetDatabaseName() string { 50 | return d.database.Name() 51 | } 52 | 53 | // DropDatabase drops database 54 | func (d *Database) DropDatabase(ctx context.Context) error { 55 | return d.database.Drop(ctx) 56 | } 57 | 58 | // RunCommand executes the given command against the database. 59 | // 60 | // The runCommand parameter must be a document for the command to be executed. It cannot be nil. 61 | // This must be an order-preserving type such as bson.D. Map types such as bson.M are not valid. 62 | // If the command document contains a session ID or any transaction-specific fields, the behavior is undefined. 63 | // 64 | // The opts parameter can be used to specify options for this operation (see the options.RunCmdOptions documentation). 65 | func (d *Database) RunCommand(ctx context.Context, runCommand interface{}, opts ...options.RunCommandOptions) *mongo.SingleResult { 66 | option := officialOpts.RunCmd() 67 | if len(opts) > 0 && opts[0].RunCmdOptions != nil { 68 | option = opts[0].RunCmdOptions 69 | } 70 | return d.database.RunCommand(ctx, runCommand, option) 71 | } 72 | 73 | // CreateCollection executes a create command to explicitly create a new collection with the specified name on the 74 | // server. If the collection being created already exists, this method will return a mongo.CommandError. This method 75 | // requires driver version 1.4.0 or higher. 76 | // 77 | // The opts parameter can be used to specify options for the operation (see the options.CreateCollectionOptions 78 | // documentation). 79 | func (db *Database) CreateCollection(ctx context.Context, name string, opts ...options.CreateCollectionOptions) error { 80 | var option = make([]*officialOpts.CreateCollectionOptions, 0, len(opts)) 81 | for _, opt := range opts { 82 | if opt.CreateCollectionOptions != nil { 83 | option = append(option, opt.CreateCollectionOptions) 84 | } 85 | } 86 | return db.database.CreateCollection(ctx, name, option...) 87 | } 88 | -------------------------------------------------------------------------------- /database_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "go.mongodb.org/mongo-driver/mongo/options" 19 | "go.mongodb.org/mongo-driver/mongo/readpref" 20 | "testing" 21 | 22 | opts "github.com/qiniu/qmgo/options" 23 | "github.com/stretchr/testify/require" 24 | "go.mongodb.org/mongo-driver/bson" 25 | ) 26 | 27 | func TestDatabase(t *testing.T) { 28 | ast := require.New(t) 29 | 30 | var sTimeout int64 = 500000 31 | var cTimeout int64 = 3000 32 | var maxPoolSize uint64 = 3000 33 | var minPoolSize uint64 = 0 34 | collName := "testopen" 35 | dbName := "qmgotest" 36 | 37 | cfg := Config{ 38 | Uri: "mongodb://localhost:27017", 39 | Database: dbName, 40 | Coll: collName, 41 | ConnectTimeoutMS: &cTimeout, 42 | SocketTimeoutMS: &sTimeout, 43 | MaxPoolSize: &maxPoolSize, 44 | MinPoolSize: &minPoolSize, 45 | } 46 | 47 | c, err := NewClient(context.Background(), &cfg) 48 | ast.NoError(err) 49 | cli := c.Database(cfg.Database) 50 | ast.Nil(err) 51 | ast.Equal(dbName, cli.GetDatabaseName()) 52 | coll := cli.Collection(collName) 53 | ast.Equal(collName, coll.GetCollectionName()) 54 | cli.Collection(collName).DropCollection(context.Background()) 55 | cli.DropDatabase(context.Background()) 56 | 57 | } 58 | 59 | func TestRunCommand(t *testing.T) { 60 | ast := require.New(t) 61 | 62 | cli := initClient("test") 63 | 64 | opts := opts.RunCommandOptions{RunCmdOptions: options.RunCmd().SetReadPreference(readpref.Primary())} 65 | res := cli.RunCommand(context.Background(), bson.D{ 66 | {"ping", 1}}, opts) 67 | ast.NoError(res.Err()) 68 | } 69 | 70 | //func TestCreateCollection(t *testing.T) { 71 | // ast := require.New(t) 72 | // 73 | // cli := initClient("test") 74 | // 75 | // timeSeriesOpt := options.TimeSeriesOptions{ 76 | // TimeField:"timestamp", 77 | // } 78 | // timeSeriesOpt.SetMetaField("metadata") 79 | // ctx := context.Background() 80 | // createCollectionOpts := opts.CreateCollectionOptions{CreateCollectionOptions: options.CreateCollection().SetTimeSeriesOptions(&timeSeriesOpt)} 81 | // if err := cli.CreateCollection(ctx, "syslog", createCollectionOpts); err != nil { 82 | // ast.NoError(err) 83 | // } 84 | // cli.DropCollection(ctx) 85 | // cli.DropDatabase(ctx) 86 | //} 87 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "errors" 18 | "strings" 19 | 20 | "go.mongodb.org/mongo-driver/mongo" 21 | ) 22 | 23 | var ( 24 | // ErrQueryNotSlicePointer return if result argument is not a pointer to a slice 25 | ErrQueryNotSlicePointer = errors.New("result argument must be a pointer to a slice") 26 | // ErrQueryNotSliceType return if result argument is not slice address 27 | ErrQueryNotSliceType = errors.New("result argument must be a slice address") 28 | // ErrQueryResultTypeInconsistent return if result type is not equal mongodb value type 29 | ErrQueryResultTypeInconsistent = errors.New("result type is not equal mongodb value type") 30 | // ErrQueryResultValCanNotChange return if the value of result can not be changed 31 | ErrQueryResultValCanNotChange = errors.New("the value of result can not be changed") 32 | // ErrNoSuchDocuments return if no document found 33 | ErrNoSuchDocuments = mongo.ErrNoDocuments 34 | // ErrTransactionRetry return if transaction need to retry 35 | ErrTransactionRetry = errors.New("retry transaction") 36 | // ErrTransactionNotSupported return if transaction not supported 37 | ErrTransactionNotSupported = errors.New("transaction not supported") 38 | // ErrNotSupportedUsername return if username is invalid 39 | ErrNotSupportedUsername = errors.New("username not supported") 40 | // ErrNotSupportedPassword return if password is invalid 41 | ErrNotSupportedPassword = errors.New("password not supported") 42 | // ErrNotValidSliceToInsert return if insert argument is not valid slice 43 | ErrNotValidSliceToInsert = errors.New("must be valid slice to insert") 44 | // ErrReplacementContainUpdateOperators return if replacement document contain update operators 45 | ErrReplacementContainUpdateOperators = errors.New("replacement document cannot contain keys beginning with '$'") 46 | ) 47 | 48 | // IsErrNoDocuments check if err is no documents, both mongo-go-driver error and qmgo custom error 49 | // Deprecated, simply call if err == ErrNoSuchDocuments or if err == mongo.ErrNoDocuments 50 | func IsErrNoDocuments(err error) bool { 51 | if err == ErrNoSuchDocuments { 52 | return true 53 | } 54 | return false 55 | } 56 | 57 | // IsDup check if err is mongo E11000 (duplicate err)。 58 | func IsDup(err error) bool { 59 | return err != nil && strings.Contains(err.Error(), "E11000") 60 | } 61 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "errors" 18 | "github.com/stretchr/testify/require" 19 | "go.mongodb.org/mongo-driver/mongo" 20 | "testing" 21 | ) 22 | 23 | func TestIsErrNoDocuments(t *testing.T) { 24 | ast := require.New(t) 25 | ast.False(IsErrNoDocuments(errors.New("dont match"))) 26 | ast.True(IsErrNoDocuments(ErrNoSuchDocuments)) 27 | ast.True(IsErrNoDocuments(mongo.ErrNoDocuments)) 28 | } 29 | 30 | func TestIsDup(t *testing.T) { 31 | ast := require.New(t) 32 | ast.False(IsDup(nil)) 33 | ast.False(IsDup(errors.New("invaliderror"))) 34 | ast.True(IsDup(errors.New("E11000"))) 35 | } 36 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "testing" 20 | 21 | "github.com/qiniu/qmgo/operator" 22 | "github.com/qiniu/qmgo/options" 23 | "github.com/stretchr/testify/require" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/bson/primitive" 26 | "go.mongodb.org/mongo-driver/event" 27 | opts "go.mongodb.org/mongo-driver/mongo/options" 28 | ) 29 | 30 | const ( 31 | URI = "mongodb://localhost:27017" 32 | DATABASE = "class" 33 | COLL = "user" 34 | ) 35 | 36 | type UserInfo struct { 37 | Id primitive.ObjectID `bson:"_id"` 38 | Name string `bson:"name"` 39 | Age uint16 `bson:"age"` 40 | Weight uint32 `bson:"weight"` 41 | } 42 | 43 | var userInfo = UserInfo{ 44 | Id: NewObjectID(), 45 | Name: "xm", 46 | Age: 7, 47 | Weight: 40, 48 | } 49 | 50 | var userInfos = []UserInfo{ 51 | {Id: NewObjectID(), Name: "a1", Age: 6, Weight: 20}, 52 | {Id: NewObjectID(), Name: "b2", Age: 6, Weight: 25}, 53 | {Id: NewObjectID(), Name: "c3", Age: 6, Weight: 30}, 54 | {Id: NewObjectID(), Name: "d4", Age: 6, Weight: 35}, 55 | {Id: NewObjectID(), Name: "a1", Age: 7, Weight: 40}, 56 | {Id: NewObjectID(), Name: "a1", Age: 8, Weight: 45}, 57 | } 58 | 59 | var poolMonitor = &event.PoolMonitor{ 60 | Event: func(evt *event.PoolEvent) { 61 | switch evt.Type { 62 | case event.GetSucceeded: 63 | case event.ConnectionReturned: 64 | } 65 | }, 66 | } 67 | 68 | func TestQmgo(t *testing.T) { 69 | ast := require.New(t) 70 | ctx := context.Background() 71 | 72 | // create connect 73 | opt := opts.Client().SetAppName("example") 74 | cli, err := Open(ctx, &Config{Uri: URI, Database: DATABASE, Coll: COLL}, options.ClientOptions{ClientOptions: opt}) 75 | 76 | ast.Nil(err) 77 | defer func() { 78 | if err = cli.Close(ctx); err != nil { 79 | panic(err) 80 | } 81 | }() 82 | defer cli.DropDatabase(ctx) 83 | 84 | cli.EnsureIndexes(ctx, []string{}, []string{"age", "name,weight"}) 85 | // insert one document 86 | _, err = cli.InsertOne(ctx, userInfo) 87 | ast.Nil(err) 88 | 89 | // find one document 90 | one := UserInfo{} 91 | err = cli.Find(ctx, bson.M{"name": userInfo.Name}).One(&one) 92 | ast.Nil(err) 93 | ast.Equal(userInfo, one) 94 | 95 | // multiple insert 96 | _, err = cli.Collection.InsertMany(ctx, userInfos) 97 | ast.Nil(err) 98 | 99 | // find all 、sort and limit 100 | batch := []UserInfo{} 101 | cli.Find(ctx, bson.M{"age": 6}).Sort("weight").Limit(7).All(&batch) 102 | ast.Equal(4, len(batch)) 103 | 104 | count, err := cli.Find(ctx, bson.M{"age": 6}).Count() 105 | ast.NoError(err) 106 | ast.Equal(int64(4), count) 107 | 108 | // aggregate 109 | matchStage := bson.D{{operator.Match, []bson.E{{"weight", bson.D{{operator.Gt, 30}}}}}} 110 | groupStage := bson.D{{operator.Group, bson.D{{"_id", "$name"}, {"total", bson.D{{operator.Sum, "$age"}}}}}} 111 | var showsWithInfo []bson.M 112 | err = cli.Aggregate(context.Background(), Pipeline{matchStage, groupStage}).All(&showsWithInfo) 113 | ast.Equal(3, len(showsWithInfo)) 114 | for _, v := range showsWithInfo { 115 | if "a1" == v["_id"] { 116 | ast.Equal(int32(15), v["total"]) 117 | continue 118 | } 119 | if "d4" == v["_id"] { 120 | ast.Equal(int32(6), v["total"]) 121 | continue 122 | } 123 | ast.Error(errors.New("error"), "impossible") 124 | } 125 | // Update one 126 | err = cli.UpdateOne(ctx, bson.M{"name": "d4"}, bson.M{"$set": bson.M{"age": 17}}) 127 | ast.NoError(err) 128 | cli.Find(ctx, bson.M{"age": 17}).One(&one) 129 | ast.Equal("d4", one.Name) 130 | // UpdateAll 131 | result, err := cli.UpdateAll(ctx, bson.M{"age": 6}, bson.M{"$set": bson.M{"age": 10}}) 132 | ast.NoError(err) 133 | count, err = cli.Find(ctx, bson.M{"age": 10}).Count() 134 | ast.NoError(err) 135 | ast.Equal(result.ModifiedCount, count) 136 | // select 137 | one = UserInfo{} 138 | err = cli.Find(ctx, bson.M{"age": 10}).Select(bson.M{"age": 1}).One(&one) 139 | ast.NoError(err) 140 | ast.Equal(10, int(one.Age)) 141 | ast.Equal("", one.Name) 142 | // remove 143 | err = cli.Remove(ctx, bson.M{"age": 7}) 144 | ast.Nil(err) 145 | } 146 | -------------------------------------------------------------------------------- /field/custom_field.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package field 15 | 16 | import ( 17 | "fmt" 18 | "go.mongodb.org/mongo-driver/bson/primitive" 19 | "reflect" 20 | "time" 21 | ) 22 | 23 | // CustomFields defines struct of supported custom fields 24 | type CustomFields struct { 25 | createAt string 26 | updateAt string 27 | id string 28 | } 29 | 30 | // CustomFieldsHook defines the interface, CustomFields return custom field user want to change 31 | type CustomFieldsHook interface { 32 | CustomFields() CustomFieldsBuilder 33 | } 34 | 35 | // CustomFieldsBuilder defines the interface which user use to set custom fields 36 | type CustomFieldsBuilder interface { 37 | SetUpdateAt(fieldName string) CustomFieldsBuilder 38 | SetCreateAt(fieldName string) CustomFieldsBuilder 39 | SetId(fieldName string) CustomFieldsBuilder 40 | } 41 | 42 | // NewCustom creates new Builder which is used to set the custom fields 43 | func NewCustom() CustomFieldsBuilder { 44 | return &CustomFields{} 45 | } 46 | 47 | // SetUpdateAt set the custom UpdateAt field 48 | func (c *CustomFields) SetUpdateAt(fieldName string) CustomFieldsBuilder { 49 | c.updateAt = fieldName 50 | return c 51 | } 52 | 53 | // SetCreateAt set the custom CreateAt field 54 | func (c *CustomFields) SetCreateAt(fieldName string) CustomFieldsBuilder { 55 | c.createAt = fieldName 56 | return c 57 | } 58 | 59 | // SetId set the custom Id field 60 | func (c *CustomFields) SetId(fieldName string) CustomFieldsBuilder { 61 | c.id = fieldName 62 | return c 63 | } 64 | 65 | // CustomCreateTime changes the custom create time 66 | func (c CustomFields) CustomCreateTime(doc interface{}) { 67 | if c.createAt == "" { 68 | return 69 | } 70 | fieldName := c.createAt 71 | setTime(doc, fieldName, false) 72 | return 73 | } 74 | 75 | // CustomUpdateTime changes the custom update time 76 | func (c CustomFields) CustomUpdateTime(doc interface{}) { 77 | if c.updateAt == "" { 78 | return 79 | } 80 | fieldName := c.updateAt 81 | setTime(doc, fieldName, true) 82 | return 83 | } 84 | 85 | // CustomUpdateTime changes the custom update time 86 | func (c CustomFields) CustomId(doc interface{}) { 87 | if c.id == "" { 88 | return 89 | } 90 | fieldName := c.id 91 | setId(doc, fieldName) 92 | return 93 | } 94 | 95 | // setTime changes the custom time fields 96 | // The overWrite defines if change value when the filed has valid value 97 | func setTime(doc interface{}, fieldName string, overWrite bool) { 98 | if reflect.Ptr != reflect.TypeOf(doc).Kind() { 99 | fmt.Println("not a point type") 100 | return 101 | } 102 | e := reflect.ValueOf(doc).Elem() 103 | ca := e.FieldByName(fieldName) 104 | if ca.CanSet() { 105 | tt := time.Now() 106 | switch a := ca.Interface().(type) { 107 | case time.Time: 108 | if ca.Interface().(time.Time).IsZero() { 109 | ca.Set(reflect.ValueOf(tt)) 110 | } else if overWrite { 111 | ca.Set(reflect.ValueOf(tt)) 112 | } 113 | case int64: 114 | if ca.Interface().(int64) == 0 { 115 | ca.SetInt(tt.Unix()) 116 | } else if overWrite { 117 | ca.SetInt(tt.Unix()) 118 | } 119 | default: 120 | fmt.Println("unsupported type to setTime", a) 121 | } 122 | } 123 | } 124 | 125 | // setId changes the custom Id fields 126 | func setId(doc interface{}, fieldName string) { 127 | if reflect.Ptr != reflect.TypeOf(doc).Kind() { 128 | fmt.Println("not a point type") 129 | return 130 | } 131 | e := reflect.ValueOf(doc).Elem() 132 | ca := e.FieldByName(fieldName) 133 | if ca.CanSet() { 134 | switch a := ca.Interface().(type) { 135 | case primitive.ObjectID: 136 | if ca.Interface().(primitive.ObjectID).IsZero() { 137 | ca.Set(reflect.ValueOf(primitive.NewObjectID())) 138 | } 139 | case string: 140 | if ca.String() == "" { 141 | ca.SetString(primitive.NewObjectID().Hex()) 142 | } 143 | default: 144 | fmt.Println("unsupported type to setId", a) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /field/custom_field_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package field 15 | 16 | import ( 17 | "github.com/stretchr/testify/require" 18 | "go.mongodb.org/mongo-driver/bson/primitive" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | type CustomUser struct { 24 | Create time.Time 25 | Update int64 26 | MyId primitive.ObjectID 27 | MyIdString string 28 | InvalidId int 29 | InvalidCreate int 30 | InvalidUpdate float32 31 | } 32 | 33 | func (c *CustomUser) CustomFields() CustomFieldsBuilder { 34 | return NewCustom().SetUpdateAt("Create").SetCreateAt("Update").SetId("MyId") 35 | } 36 | 37 | func (c *CustomUser) CustomFieldsIdString() CustomFieldsBuilder { 38 | return NewCustom().SetId("MyIdString") 39 | } 40 | 41 | func TestCustomFields(t *testing.T) { 42 | ast := require.New(t) 43 | u := &CustomUser{} 44 | c := u.CustomFields() 45 | c.(*CustomFields).CustomCreateTime(u) 46 | c.(*CustomFields).CustomUpdateTime(u) 47 | c.(*CustomFields).CustomId(u) 48 | ast.NotEqual(0, u.Update) 49 | ast.NotEqual(time.Time{}, u.Create) 50 | ast.NotEqual(primitive.NilObjectID, u.MyId) 51 | 52 | // id string 53 | u1 := &CustomUser{} 54 | c1 := u.CustomFieldsIdString() 55 | c1.(*CustomFields).CustomId(u1) 56 | ast.NotEqual("", u1.MyIdString) 57 | 58 | } 59 | 60 | func (c *CustomUser) CustomFieldsInvalid() CustomFieldsBuilder { 61 | return NewCustom().SetCreateAt("InvalidCreate") 62 | } 63 | func (c *CustomUser) CustomFieldsInvalid2() CustomFieldsBuilder { 64 | return NewCustom().SetUpdateAt("InvalidUpdate") 65 | } 66 | 67 | func (c *CustomUser) CustomFieldsInvalid3() CustomFieldsBuilder { 68 | return NewCustom().SetId("InvalidId") 69 | } 70 | 71 | func TestCustomFieldsInvalid(t *testing.T) { 72 | u := &CustomUser{} 73 | c := u.CustomFieldsInvalid() 74 | c.(*CustomFields).CustomCreateTime(u) 75 | c.(*CustomFields).CustomUpdateTime(u) 76 | ast := require.New(t) 77 | ast.Equal(0, u.InvalidCreate) 78 | ast.Equal(float32(0), u.InvalidUpdate) 79 | 80 | u1 := &CustomUser{} 81 | c = u1.CustomFieldsInvalid2() 82 | c.(*CustomFields).CustomCreateTime(u1) 83 | c.(*CustomFields).CustomUpdateTime(u1) 84 | ast.Equal(0, u1.InvalidCreate) 85 | ast.Equal(float32(0), u1.InvalidUpdate) 86 | 87 | u2 := CustomUser{} 88 | c = u2.CustomFieldsInvalid() 89 | c.(*CustomFields).CustomCreateTime(u2) 90 | c.(*CustomFields).CustomUpdateTime(u2) 91 | ast.Equal(0, u2.InvalidCreate) 92 | ast.Equal(float32(0), u2.InvalidUpdate) 93 | 94 | u3 := CustomUser{} 95 | c = u3.CustomFieldsInvalid3() 96 | c.(*CustomFields).CustomId(u3) 97 | ast.Equal(0, u3.InvalidId) 98 | 99 | u4 := &CustomUser{} 100 | c = u4.CustomFieldsInvalid3() 101 | c.(*CustomFields).CustomId(u4) 102 | ast.Equal(0, u4.InvalidId) 103 | 104 | } 105 | -------------------------------------------------------------------------------- /field/default_field.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package field 15 | 16 | import ( 17 | "time" 18 | 19 | "go.mongodb.org/mongo-driver/bson/primitive" 20 | ) 21 | 22 | // DefaultFieldHook defines the interface to change default fields by hook 23 | type DefaultFieldHook interface { 24 | DefaultUpdateAt() 25 | DefaultCreateAt() 26 | DefaultId() 27 | } 28 | 29 | // DefaultField defines the default fields to handle when operation happens 30 | // import the DefaultField in document struct to make it working 31 | type DefaultField struct { 32 | Id primitive.ObjectID `bson:"_id"` 33 | CreateAt time.Time `bson:"createAt"` 34 | UpdateAt time.Time `bson:"updateAt"` 35 | } 36 | 37 | // DefaultUpdateAt changes the default updateAt field 38 | func (df *DefaultField) DefaultUpdateAt() { 39 | df.UpdateAt = time.Now().Local() 40 | } 41 | 42 | // DefaultCreateAt changes the default createAt field 43 | func (df *DefaultField) DefaultCreateAt() { 44 | if df.CreateAt.IsZero() { 45 | df.CreateAt = time.Now().Local() 46 | } 47 | } 48 | 49 | // DefaultId changes the default _id field 50 | func (df *DefaultField) DefaultId() { 51 | if df.Id.IsZero() { 52 | df.Id = primitive.NewObjectID() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /field/default_field_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package field 15 | 16 | import ( 17 | "testing" 18 | "time" 19 | 20 | "github.com/stretchr/testify/require" 21 | "go.mongodb.org/mongo-driver/bson/primitive" 22 | ) 23 | 24 | func TestDefaultField(t *testing.T) { 25 | ast := require.New(t) 26 | 27 | df := &DefaultField{} 28 | df.DefaultCreateAt() 29 | df.DefaultUpdateAt() 30 | df.DefaultId() 31 | ast.NotEqual(time.Time{}, df.UpdateAt) 32 | ast.NotEqual(time.Time{}, df.CreateAt) 33 | ast.NotEqual(primitive.NilObjectID, df.Id) 34 | } 35 | -------------------------------------------------------------------------------- /field/field.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package field 15 | 16 | import ( 17 | "context" 18 | "reflect" 19 | "time" 20 | 21 | "github.com/qiniu/qmgo/operator" 22 | ) 23 | 24 | var nilTime time.Time 25 | 26 | // filedHandler defines the relations between field type and handler 27 | var fieldHandler = map[operator.OpType]func(doc interface{}) error{ 28 | operator.BeforeInsert: beforeInsert, 29 | operator.BeforeUpdate: beforeUpdate, 30 | operator.BeforeReplace: beforeUpdate, 31 | operator.BeforeUpsert: beforeUpsert, 32 | } 33 | 34 | //func init() { 35 | // middleware.Register(Do) 36 | //} 37 | 38 | // Do call the specific method to handle field based on fType 39 | // Don't use opts here 40 | func Do(ctx context.Context, doc interface{}, opType operator.OpType, opts ...interface{}) error { 41 | to := reflect.TypeOf(doc) 42 | if to == nil { 43 | return nil 44 | } 45 | switch reflect.TypeOf(doc).Kind() { 46 | case reflect.Slice: 47 | return sliceHandle(doc, opType) 48 | case reflect.Ptr: 49 | v := reflect.ValueOf(doc).Elem() 50 | switch v.Kind() { 51 | case reflect.Slice: 52 | return sliceHandle(v.Interface(), opType) 53 | default: 54 | return do(doc, opType) 55 | } 56 | } 57 | //fmt.Println("not support type") 58 | return nil 59 | } 60 | 61 | // sliceHandle handles the slice docs 62 | func sliceHandle(docs interface{}, opType operator.OpType) error { 63 | // []interface{}{UserType{}...} 64 | if h, ok := docs.([]interface{}); ok { 65 | for _, v := range h { 66 | if err := do(v, opType); err != nil { 67 | return err 68 | } 69 | } 70 | return nil 71 | } 72 | // []UserType{} 73 | s := reflect.ValueOf(docs) 74 | for i := 0; i < s.Len(); i++ { 75 | if err := do(s.Index(i).Interface(), opType); err != nil { 76 | return err 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | // beforeInsert handles field before insert 83 | // If value of field createAt is valid in doc, upsert doesn't change it 84 | // If value of field id is valid in doc, upsert doesn't change it 85 | // Change the value of field updateAt anyway 86 | func beforeInsert(doc interface{}) error { 87 | if ih, ok := doc.(DefaultFieldHook); ok { 88 | ih.DefaultId() 89 | ih.DefaultCreateAt() 90 | ih.DefaultUpdateAt() 91 | } 92 | if ih, ok := doc.(CustomFieldsHook); ok { 93 | fields := ih.CustomFields() 94 | fields.(*CustomFields).CustomId(doc) 95 | fields.(*CustomFields).CustomCreateTime(doc) 96 | fields.(*CustomFields).CustomUpdateTime(doc) 97 | } 98 | return nil 99 | } 100 | 101 | // beforeUpdate handles field before update 102 | func beforeUpdate(doc interface{}) error { 103 | if ih, ok := doc.(DefaultFieldHook); ok { 104 | ih.DefaultUpdateAt() 105 | } 106 | if ih, ok := doc.(CustomFieldsHook); ok { 107 | fields := ih.CustomFields() 108 | fields.(*CustomFields).CustomUpdateTime(doc) 109 | } 110 | return nil 111 | } 112 | 113 | // beforeUpsert handles field before upsert 114 | // If value of field createAt is valid in doc, upsert doesn't change it 115 | // If value of field id is valid in doc, upsert doesn't change it 116 | // Change the value of field updateAt anyway 117 | func beforeUpsert(doc interface{}) error { 118 | if ih, ok := doc.(DefaultFieldHook); ok { 119 | ih.DefaultId() 120 | ih.DefaultCreateAt() 121 | ih.DefaultUpdateAt() 122 | } 123 | if ih, ok := doc.(CustomFieldsHook); ok { 124 | fields := ih.CustomFields() 125 | fields.(*CustomFields).CustomId(doc) 126 | fields.(*CustomFields).CustomCreateTime(doc) 127 | fields.(*CustomFields).CustomUpdateTime(doc) 128 | } 129 | return nil 130 | } 131 | 132 | // do check if opType is supported and call fieldHandler 133 | func do(doc interface{}, opType operator.OpType) error { 134 | if f, ok := fieldHandler[opType]; !ok { 135 | return nil 136 | } else { 137 | return f(doc) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /field/field_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package field 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | "time" 20 | 21 | "github.com/qiniu/qmgo/operator" 22 | "github.com/stretchr/testify/require" 23 | "go.mongodb.org/mongo-driver/bson/primitive" 24 | ) 25 | 26 | type User struct { 27 | DefaultField `bson:",inline"` 28 | 29 | Name string `bson:"name"` 30 | Age int `bson:"age"` 31 | CreateTimeAt time.Time `bson:"createTimeAt"` 32 | UpdateTimeAt int64 `bson:"updateTimeAt"` 33 | MyId primitive.ObjectID `bson:"myId"` 34 | } 35 | 36 | func (u *User) CustomFields() CustomFieldsBuilder { 37 | return NewCustom().SetCreateAt("CreateTimeAt").SetUpdateAt("UpdateTimeAt").SetId("MyId") 38 | } 39 | 40 | func TestBeforeInsert(t *testing.T) { 41 | ast := require.New(t) 42 | ctx := context.Background() 43 | 44 | u := &User{Name: "Lucas", Age: 7} 45 | err := Do(ctx, u, operator.BeforeInsert) 46 | ast.NoError(err) 47 | // default fields 48 | ast.NotEqual(time.Time{}, u.CreateAt) 49 | ast.NotEqual(time.Time{}, u.UpdateAt) 50 | ast.NotEqual(primitive.NilObjectID, u.Id) 51 | // custom fields 52 | ast.NotEqual(time.Time{}, u.CreateTimeAt) 53 | ast.NotEqual(time.Time{}, u.UpdateTimeAt) 54 | ast.NotEqual(primitive.NilObjectID, u.MyId) 55 | 56 | u1, u2 := &User{Name: "Lucas", Age: 7}, &User{Name: "Alice", Age: 8} 57 | us := []*User{u1, u2} 58 | err = Do(ctx, us, operator.BeforeInsert) 59 | ast.NoError(err) 60 | 61 | for _, v := range us { 62 | ast.NotEqual(time.Time{}, v.CreateAt) 63 | ast.NotEqual(time.Time{}, v.UpdateAt) 64 | ast.NotEqual(primitive.NilObjectID, v.Id) 65 | } 66 | 67 | u3 := User{Name: "Lucas", Age: 7} 68 | err = Do(ctx, u3, operator.BeforeInsert) 69 | ast.NoError(err) 70 | 71 | // insert with valid value 72 | tBefore3s := time.Now().Add(-3 * time.Second) 73 | id := primitive.NewObjectID() 74 | u = &User{Name: "Lucas", Age: 7} 75 | u.CreateAt = tBefore3s 76 | u.UpdateAt = tBefore3s 77 | u.Id = id 78 | u.MyId = id 79 | u.CreateTimeAt = tBefore3s 80 | u.UpdateTimeAt = tBefore3s.Unix() 81 | 82 | err = Do(ctx, u, operator.BeforeUpsert) 83 | ast.NoError(err) 84 | 85 | ast.Equal(tBefore3s, u.CreateAt) 86 | ast.Equal(id, u.Id) 87 | ast.NotEqual(tBefore3s, u.UpdateAt) 88 | 89 | ast.Equal(tBefore3s, u.CreateTimeAt) 90 | ast.Equal(id, u.MyId) 91 | ast.NotEqual(tBefore3s.Unix(), u.UpdateTimeAt) 92 | } 93 | 94 | func TestBeforeUpdate(t *testing.T) { 95 | ast := require.New(t) 96 | ctx := context.Background() 97 | 98 | u := &User{Name: "Lucas", Age: 7} 99 | err := Do(ctx, u, operator.BeforeUpdate) 100 | ast.NoError(err) 101 | // default field 102 | ast.NotEqual(time.Time{}, u.UpdateAt) 103 | 104 | // custom fields 105 | ast.NotEqual(time.Time{}, u.UpdateTimeAt) 106 | 107 | u1, u2 := &User{Name: "Lucas", Age: 7}, &User{Name: "Alice", Age: 8} 108 | us := []*User{u1, u2} 109 | err = Do(ctx, us, operator.BeforeUpdate) 110 | ast.NoError(err) 111 | for _, v := range us { 112 | // default field 113 | ast.NotEqual(time.Time{}, v.UpdateAt) 114 | 115 | // custom fields 116 | ast.NotEqual(time.Time{}, v.UpdateTimeAt) 117 | } 118 | 119 | us1 := []interface{}{u1, u2} 120 | err = Do(ctx, us1, operator.BeforeUpdate) 121 | ast.NoError(err) 122 | for _, v := range us { 123 | // default field 124 | ast.NotEqual(time.Time{}, v.UpdateAt) 125 | 126 | // custom fields 127 | ast.NotEqual(time.Time{}, v.UpdateTimeAt) 128 | } 129 | 130 | } 131 | 132 | type UserField struct { 133 | DefaultField `bson:",inline"` 134 | 135 | Name string `bson:"name"` 136 | Age int `bson:"age"` 137 | CreateTimeAt int64 `bson:"createTimeAt"` 138 | UpdateTimeAt time.Time `bson:"updateTimeAt"` 139 | MyId primitive.ObjectID `bson:"myId"` 140 | } 141 | 142 | func (u *UserField) CustomFields() CustomFieldsBuilder { 143 | return NewCustom().SetCreateAt("CreateTimeAt").SetUpdateAt("UpdateTimeAt").SetId("MyId") 144 | } 145 | 146 | func TestBeforeUpsert(t *testing.T) { 147 | ast := require.New(t) 148 | ctx := context.Background() 149 | 150 | // with empty fields 151 | u := &User{Name: "Lucas", Age: 7} 152 | err := Do(ctx, u, operator.BeforeUpsert) 153 | ast.NoError(err) 154 | // default fields 155 | ast.NotEqual(time.Time{}, u.CreateAt) 156 | ast.NotEqual(time.Time{}, u.UpdateAt) 157 | ast.NotEqual(primitive.NilObjectID, u.Id) 158 | // custom fields 159 | ast.NotEqual(time.Time{}, u.CreateTimeAt) 160 | ast.NotEqual(0, u.UpdateTimeAt) 161 | ast.NotEqual(primitive.NilObjectID, u.MyId) 162 | 163 | u1, u2 := &User{Name: "Lucas", Age: 7}, &User{Name: "Alice", Age: 8} 164 | us := []*User{u1, u2} 165 | err = Do(ctx, us, operator.BeforeUpsert) 166 | ast.NoError(err) 167 | 168 | for _, v := range us { 169 | ast.NotEqual(time.Time{}, v.CreateAt) 170 | ast.NotEqual(time.Time{}, v.UpdateAt) 171 | ast.NotEqual(time.Time{}, u.CreateTimeAt) 172 | ast.NotEqual(0, u.UpdateTimeAt) 173 | ast.NotEqual(primitive.NilObjectID, v.Id) 174 | } 175 | 176 | u3 := User{Name: "Lucas", Age: 7} 177 | err = Do(ctx, u3, operator.BeforeUpsert) 178 | ast.NoError(err) 179 | 180 | // upsert with valid value 181 | tBefore3s := time.Now().Add(-3 * time.Second) 182 | id := primitive.NewObjectID() 183 | u = &User{Name: "Lucas", Age: 7} 184 | u.CreateAt = tBefore3s 185 | u.UpdateAt = tBefore3s 186 | u.Id = id 187 | u.MyId = id 188 | u.CreateTimeAt = tBefore3s 189 | u.UpdateTimeAt = tBefore3s.Unix() 190 | 191 | err = Do(ctx, u, operator.BeforeUpsert) 192 | ast.NoError(err) 193 | 194 | ast.Equal(tBefore3s, u.CreateAt) 195 | ast.Equal(id, u.Id) 196 | ast.NotEqual(tBefore3s, u.UpdateAt) 197 | 198 | ast.Equal(tBefore3s, u.CreateTimeAt) 199 | ast.Equal(id, u.MyId) 200 | ast.NotEqual(tBefore3s.Unix(), u.UpdateTimeAt) 201 | 202 | } 203 | 204 | // same as TestBeforeUpsert, just switch type of CreateTimeAt and UpdateTimeAt 205 | func TestBeforeUpsertUserFiled(t *testing.T) { 206 | ast := require.New(t) 207 | ctx := context.Background() 208 | 209 | // with empty fileds 210 | u := &UserField{Name: "Lucas", Age: 7} 211 | err := Do(ctx, u, operator.BeforeUpsert) 212 | ast.NoError(err) 213 | // default fields 214 | ast.NotEqual(time.Time{}, u.CreateAt) 215 | ast.NotEqual(time.Time{}, u.UpdateAt) 216 | ast.NotEqual(primitive.NilObjectID, u.Id) 217 | // custom fields 218 | ast.NotEqual(0, u.CreateTimeAt) 219 | ast.NotEqual(time.Time{}, u.UpdateTimeAt) 220 | ast.NotEqual(primitive.NilObjectID, u.MyId) 221 | 222 | u1, u2 := &UserField{Name: "Lucas", Age: 7}, &UserField{Name: "Alice", Age: 8} 223 | us := []*UserField{u1, u2} 224 | err = Do(ctx, us, operator.BeforeUpsert) 225 | ast.NoError(err) 226 | 227 | for _, v := range us { 228 | ast.NotEqual(time.Time{}, v.CreateAt) 229 | ast.NotEqual(time.Time{}, v.UpdateAt) 230 | ast.NotEqual(0, u.CreateTimeAt) 231 | ast.NotEqual(time.Time{}, u.UpdateTimeAt) 232 | ast.NotEqual(primitive.NilObjectID, v.Id) 233 | } 234 | 235 | u3 := User{Name: "Lucas", Age: 7} 236 | err = Do(ctx, u3, operator.BeforeUpsert) 237 | ast.NoError(err) 238 | 239 | // upsert with valid value 240 | tBefore3s := time.Now().Add(-3 * time.Second) 241 | id := primitive.NewObjectID() 242 | u = &UserField{Name: "Lucas", Age: 7} 243 | u.CreateAt = tBefore3s 244 | u.UpdateAt = tBefore3s 245 | u.Id = id 246 | u.MyId = id 247 | u.CreateTimeAt = tBefore3s.Unix() 248 | u.UpdateTimeAt = tBefore3s 249 | 250 | err = Do(ctx, u, operator.BeforeUpsert) 251 | ast.NoError(err) 252 | 253 | ast.Equal(tBefore3s, u.CreateAt) 254 | ast.Equal(id, u.Id) 255 | ast.NotEqual(tBefore3s, u.UpdateAt) 256 | 257 | ast.NotEqual(tBefore3s, u.UpdateTimeAt) 258 | ast.Equal(id, u.MyId) 259 | ast.Equal(tBefore3s.Unix(), u.CreateTimeAt) 260 | 261 | } 262 | 263 | func TestNilError(t *testing.T) { 264 | ast := require.New(t) 265 | ctx := context.Background() 266 | 267 | err := Do(ctx, nil, operator.BeforeUpsert) 268 | ast.NoError(err) 269 | 270 | } 271 | -------------------------------------------------------------------------------- /field_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | "time" 21 | 22 | "github.com/qiniu/qmgo/field" 23 | "github.com/stretchr/testify/require" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/bson/primitive" 26 | ) 27 | 28 | type UserField struct { 29 | field.DefaultField `bson:",inline"` 30 | 31 | Name string `bson:"name"` 32 | Age int `bson:"age"` 33 | 34 | MyId string `bson:"myId"` 35 | CreateTimeAt time.Time `bson:"createTimeAt"` 36 | UpdateTimeAt int64 `bson:"updateTimeAt"` 37 | } 38 | 39 | func (u *UserField) CustomFields() field.CustomFieldsBuilder { 40 | return field.NewCustom().SetCreateAt("CreateTimeAt").SetUpdateAt("UpdateTimeAt").SetId("MyId") 41 | } 42 | 43 | func TestFieldInsert(t *testing.T) { 44 | ast := require.New(t) 45 | cli := initClient("test") 46 | ctx := context.Background() 47 | defer cli.Close(ctx) 48 | defer cli.DropCollection(ctx) 49 | 50 | u := &UserField{Name: "Lucas", Age: 7} 51 | _, err := cli.InsertOne(context.Background(), u) 52 | ast.NoError(err) 53 | 54 | uc := bson.M{"name": "Lucas"} 55 | ur := &UserField{} 56 | err = cli.Find(ctx, uc).One(ur) 57 | ast.NoError(err) 58 | 59 | // default fields 60 | ast.NotEqual(time.Time{}, ur.CreateAt) 61 | ast.NotEqual(time.Time{}, ur.UpdateAt) 62 | ast.NotEqual(primitive.NilObjectID, ur.Id) 63 | // custom fields 64 | ast.NotEqual(time.Time{}, ur.CreateTimeAt) 65 | ast.NotEqual(int64(0), ur.UpdateTimeAt) 66 | ast.NotEqual("", ur.MyId) 67 | 68 | } 69 | 70 | func TestFieldInsertMany(t *testing.T) { 71 | ast := require.New(t) 72 | cli := initClient("test") 73 | ctx := context.Background() 74 | defer cli.Close(ctx) 75 | defer cli.DropCollection(ctx) 76 | 77 | u1 := &UserField{Name: "Lucas", Age: 7} 78 | u2 := &UserField{Name: "Alice", Age: 7} 79 | us := []*UserField{u1, u2} 80 | _, err := cli.InsertMany(ctx, us) 81 | ast.NoError(err) 82 | 83 | uc := bson.M{"age": 7} 84 | ur := []UserField{} 85 | err = cli.Find(ctx, uc).All(&ur) 86 | ast.NoError(err) 87 | 88 | // default fields 89 | ast.NotEqual(time.Time{}, ur[0].CreateAt) 90 | ast.NotEqual(time.Time{}, ur[0].UpdateAt) 91 | ast.NotEqual(primitive.NilObjectID, ur[0].Id) 92 | // default fields 93 | ast.NotEqual(time.Time{}, ur[1].CreateAt) 94 | ast.NotEqual(time.Time{}, ur[1].UpdateAt) 95 | ast.NotEqual(primitive.NilObjectID, ur[1].Id) 96 | 97 | // custom fields 98 | ast.NotEqual(time.Time{}, ur[0].CreateTimeAt) 99 | ast.NotEqual(int64(0), ur[0].UpdateTimeAt) 100 | } 101 | 102 | func TestFieldUpdate(t *testing.T) { 103 | ast := require.New(t) 104 | cli := initClient("test") 105 | defer cli.Close(context.Background()) 106 | defer cli.DropCollection(context.Background()) 107 | cli.EnsureIndexes(context.Background(), []string{"name"}, nil) 108 | 109 | ui := &UserField{Name: "Lucas", Age: 17} 110 | _, err := cli.InsertOne(context.Background(), ui) 111 | ast.NoError(err) 112 | 113 | err = cli.UpdateOne(context.Background(), bson.M{"name": "Lucas"}, bson.M{"$set": bson.M{"updateTimeAt": 0, "updateAt": time.Time{}}}) 114 | ast.NoError(err) 115 | 116 | findUi := UserField{} 117 | err = cli.Find(context.Background(), bson.M{"name": "Lucas"}).One(&findUi) 118 | ast.Equal(int64(0), findUi.UpdateTimeAt) 119 | ast.Equal(time.Time{}, findUi.UpdateAt) 120 | 121 | ast.NoError(err) 122 | ui.Id = findUi.Id 123 | err = cli.ReplaceOne(context.Background(), bson.M{"_id": findUi.Id}, &ui) 124 | ast.NoError(err) 125 | err = cli.Find(context.Background(), bson.M{"name": "Lucas"}).One(&findUi) 126 | ast.NotEqual(int64(0), findUi.UpdateTimeAt) 127 | ast.NotEqual(time.Time{}, findUi.UpdateAt) 128 | 129 | } 130 | 131 | func TestFieldUpsert(t *testing.T) { 132 | ast := require.New(t) 133 | cli := initClient("test") 134 | ctx := context.Background() 135 | defer cli.Close(ctx) 136 | defer cli.DropCollection(ctx) 137 | 138 | u := &UserField{Name: "Lucas", Age: 7} 139 | id := primitive.NewObjectID() 140 | u.Id = id 141 | id_1 := primitive.NewObjectID() 142 | u.MyId = id_1.String() 143 | _, err := cli.InsertOne(context.Background(), u) 144 | ast.NoError(err) 145 | 146 | time.Sleep(2 * time.Second) 147 | u.Age = 17 148 | tBefore3s := time.Now().Add(-3 * time.Second).Local() 149 | u.CreateAt = tBefore3s 150 | u.UpdateAt = tBefore3s 151 | u.CreateTimeAt = tBefore3s 152 | u.UpdateTimeAt = tBefore3s.Unix() 153 | result, err := cli.Upsert(ctx, bson.M{"_id": id}, u) 154 | ast.NoError(err) 155 | fmt.Println(result) 156 | 157 | ui := UserField{} 158 | err = cli.Find(ctx, bson.M{"_id": id}).One(&ui) 159 | 160 | ast.NoError(err) 161 | ast.Equal(u.Age, ui.Age) 162 | ast.Equal(id, ui.Id) 163 | ast.Equal(id_1.String(), ui.MyId) 164 | fmt.Println(tBefore3s.Unix(), ui.CreateAt.Unix()) 165 | ast.Equal(tBefore3s.Unix(), ui.CreateAt.Unix()) 166 | ast.Equal(tBefore3s.Unix(), ui.CreateTimeAt.Unix()) 167 | ast.NotEqual(tBefore3s.Unix(), ui.UpdateAt.Unix()) 168 | ast.NotEqual(tBefore3s.Unix(), ui.UpdateTimeAt) 169 | 170 | } 171 | 172 | func TestFieldUpsertId(t *testing.T) { 173 | ast := require.New(t) 174 | cli := initClient("test") 175 | ctx := context.Background() 176 | defer cli.Close(ctx) 177 | defer cli.DropCollection(ctx) 178 | 179 | u := &UserField{Name: "Lucas", Age: 7} 180 | id := primitive.NewObjectID() 181 | u.Id = id 182 | id_1 := primitive.NewObjectID() 183 | u.MyId = id_1.String() 184 | _, err := cli.InsertOne(context.Background(), u) 185 | ast.NoError(err) 186 | 187 | time.Sleep(2 * time.Second) 188 | u.Age = 17 189 | tBefore3s := time.Now().Add(-3 * time.Second).Local() 190 | u.CreateAt = tBefore3s 191 | u.UpdateAt = tBefore3s 192 | u.CreateTimeAt = tBefore3s 193 | u.UpdateTimeAt = tBefore3s.Unix() 194 | _, err = cli.UpsertId(ctx, id, u) 195 | ast.NoError(err) 196 | 197 | ui := UserField{} 198 | err = cli.Find(ctx, bson.M{"_id": id}).One(&ui) 199 | 200 | ast.NoError(err) 201 | ast.Equal(u.Age, ui.Age) 202 | ast.Equal(id, ui.Id) 203 | ast.Equal(id_1.String(), ui.MyId) 204 | ast.Equal(tBefore3s.Unix(), ui.CreateAt.Unix()) 205 | ast.Equal(tBefore3s.Unix(), ui.CreateTimeAt.Unix()) 206 | ast.NotEqual(tBefore3s.Unix(), ui.UpdateAt.Unix()) 207 | ast.NotEqual(tBefore3s.Unix(), ui.UpdateTimeAt) 208 | } 209 | 210 | func TestFieldUpdateId(t *testing.T) { 211 | ast := require.New(t) 212 | cli := initClient("test") 213 | defer cli.Close(context.Background()) 214 | defer cli.DropCollection(context.Background()) 215 | cli.EnsureIndexes(context.Background(), []string{"name"}, nil) 216 | 217 | ui := &UserField{Name: "Lucas", Age: 17} 218 | res, err := cli.InsertOne(context.Background(), ui) 219 | ast.NoError(err) 220 | 221 | err = cli.UpdateId(context.Background(), res.InsertedID, bson.M{"$set": bson.M{"updateTimeAt": 0, "updateAt": time.Time{}}}) 222 | ast.NoError(err) 223 | 224 | findUi := UserField{} 225 | err = cli.Find(context.Background(), bson.M{"name": "Lucas"}).One(&findUi) 226 | ast.NoError(err) 227 | ast.Equal(int64(0), findUi.UpdateTimeAt) 228 | ast.Equal(time.Time{}, findUi.UpdateAt) 229 | } 230 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/qiniu/qmgo 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-playground/validator/v10 v10.4.1 7 | github.com/stretchr/testify v1.6.1 8 | go.mongodb.org/mongo-driver v1.17.1 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 5 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 6 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 7 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 8 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 9 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 10 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 11 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 12 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 13 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 14 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 15 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 16 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 17 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 18 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 19 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 20 | github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= 21 | github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 27 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 28 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 29 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 30 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 31 | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= 32 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 33 | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= 34 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 35 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 36 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 37 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 38 | go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= 39 | go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= 40 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 41 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 42 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 43 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 44 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 45 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 46 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 47 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 48 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 49 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 50 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 51 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 52 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 53 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 54 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 55 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 56 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 57 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 58 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 59 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 60 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 61 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 62 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 63 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 67 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 68 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 69 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 70 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 71 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 81 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 82 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 83 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 84 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 85 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 86 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 87 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 88 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 89 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 90 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 91 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 92 | golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 93 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 94 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 95 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 96 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 97 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 98 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 99 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 100 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 101 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 102 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 103 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 104 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 105 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 106 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 107 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 108 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 109 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 110 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 111 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 112 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 113 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 114 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 115 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 116 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 117 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 118 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | -------------------------------------------------------------------------------- /hook/hook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package hook 15 | 16 | import ( 17 | "context" 18 | "github.com/qiniu/qmgo/operator" 19 | "reflect" 20 | ) 21 | 22 | // hookHandler defines the relations between hook type and handler 23 | var hookHandler = map[operator.OpType]func(ctx context.Context, hook interface{}) error{ 24 | operator.BeforeInsert: beforeInsert, 25 | operator.AfterInsert: afterInsert, 26 | operator.BeforeUpdate: beforeUpdate, 27 | operator.AfterUpdate: afterUpdate, 28 | operator.BeforeQuery: beforeQuery, 29 | operator.AfterQuery: afterQuery, 30 | operator.BeforeRemove: beforeRemove, 31 | operator.AfterRemove: afterRemove, 32 | operator.BeforeUpsert: beforeUpsert, 33 | operator.AfterUpsert: afterUpsert, 34 | operator.BeforeReplace: beforeUpdate, 35 | operator.AfterReplace: afterUpdate, 36 | } 37 | 38 | // 39 | //func init() { 40 | // middleware.Register(Do) 41 | //} 42 | 43 | // Do call the specific method to handle hook based on hType 44 | // If opts has valid value, use it instead of original hook 45 | func Do(ctx context.Context, hook interface{}, opType operator.OpType, opts ...interface{}) error { 46 | if len(opts) > 0 { 47 | hook = opts[0] 48 | } 49 | 50 | to := reflect.TypeOf(hook) 51 | if to == nil { 52 | return nil 53 | } 54 | switch to.Kind() { 55 | case reflect.Slice: 56 | return sliceHandle(ctx, hook, opType) 57 | case reflect.Ptr: 58 | v := reflect.ValueOf(hook).Elem() 59 | switch v.Kind() { 60 | case reflect.Slice: 61 | return sliceHandle(ctx, v.Interface(), opType) 62 | default: 63 | return do(ctx, hook, opType) 64 | } 65 | default: 66 | return do(ctx, hook, opType) 67 | } 68 | } 69 | 70 | // sliceHandle handles the slice hooks 71 | func sliceHandle(ctx context.Context, hook interface{}, opType operator.OpType) error { 72 | // []interface{}{UserType{}...} 73 | if h, ok := hook.([]interface{}); ok { 74 | for _, v := range h { 75 | if err := do(ctx, v, opType); err != nil { 76 | return err 77 | } 78 | } 79 | return nil 80 | } 81 | // []UserType{} 82 | s := reflect.ValueOf(hook) 83 | for i := 0; i < s.Len(); i++ { 84 | if err := do(ctx, s.Index(i).Interface(), opType); err != nil { 85 | return err 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | // BeforeInsertHook InsertHook defines the insert hook interface 92 | type BeforeInsertHook interface { 93 | BeforeInsert(ctx context.Context) error 94 | } 95 | type AfterInsertHook interface { 96 | AfterInsert(ctx context.Context) error 97 | } 98 | 99 | // beforeInsert calls custom BeforeInsert 100 | func beforeInsert(ctx context.Context, hook interface{}) error { 101 | if ih, ok := hook.(BeforeInsertHook); ok { 102 | return ih.BeforeInsert(ctx) 103 | } 104 | return nil 105 | } 106 | 107 | // afterInsert calls custom AfterInsert 108 | func afterInsert(ctx context.Context, hook interface{}) error { 109 | if ih, ok := hook.(AfterInsertHook); ok { 110 | return ih.AfterInsert(ctx) 111 | } 112 | return nil 113 | } 114 | 115 | // BeforeUpdateHook defines the Update hook interface 116 | type BeforeUpdateHook interface { 117 | BeforeUpdate(ctx context.Context) error 118 | } 119 | type AfterUpdateHook interface { 120 | AfterUpdate(ctx context.Context) error 121 | } 122 | 123 | // beforeUpdate calls custom BeforeUpdate 124 | func beforeUpdate(ctx context.Context, hook interface{}) error { 125 | if ih, ok := hook.(BeforeUpdateHook); ok { 126 | return ih.BeforeUpdate(ctx) 127 | } 128 | return nil 129 | } 130 | 131 | // afterUpdate calls custom AfterUpdate 132 | func afterUpdate(ctx context.Context, hook interface{}) error { 133 | if ih, ok := hook.(AfterUpdateHook); ok { 134 | return ih.AfterUpdate(ctx) 135 | } 136 | return nil 137 | } 138 | 139 | // BeforeQueryHook QueryHook defines the query hook interface 140 | type BeforeQueryHook interface { 141 | BeforeQuery(ctx context.Context) error 142 | } 143 | type AfterQueryHook interface { 144 | AfterQuery(ctx context.Context) error 145 | } 146 | 147 | // beforeQuery calls custom BeforeQuery 148 | func beforeQuery(ctx context.Context, hook interface{}) error { 149 | if ih, ok := hook.(BeforeQueryHook); ok { 150 | return ih.BeforeQuery(ctx) 151 | } 152 | return nil 153 | } 154 | 155 | // afterQuery calls custom AfterQuery 156 | func afterQuery(ctx context.Context, hook interface{}) error { 157 | if ih, ok := hook.(AfterQueryHook); ok { 158 | return ih.AfterQuery(ctx) 159 | } 160 | return nil 161 | } 162 | 163 | // BeforeRemoveHook RemoveHook defines the remove hook interface 164 | type BeforeRemoveHook interface { 165 | BeforeRemove(ctx context.Context) error 166 | } 167 | type AfterRemoveHook interface { 168 | AfterRemove(ctx context.Context) error 169 | } 170 | 171 | // beforeRemove calls custom BeforeRemove 172 | func beforeRemove(ctx context.Context, hook interface{}) error { 173 | if ih, ok := hook.(BeforeRemoveHook); ok { 174 | return ih.BeforeRemove(ctx) 175 | } 176 | return nil 177 | } 178 | 179 | // afterRemove calls custom AfterRemove 180 | func afterRemove(ctx context.Context, hook interface{}) error { 181 | if ih, ok := hook.(AfterRemoveHook); ok { 182 | return ih.AfterRemove(ctx) 183 | } 184 | return nil 185 | } 186 | 187 | // BeforeUpsertHook UpsertHook defines the upsert hook interface 188 | type BeforeUpsertHook interface { 189 | BeforeUpsert(ctx context.Context) error 190 | } 191 | type AfterUpsertHook interface { 192 | AfterUpsert(ctx context.Context) error 193 | } 194 | 195 | // beforeUpsert calls custom BeforeUpsert 196 | func beforeUpsert(ctx context.Context, hook interface{}) error { 197 | if ih, ok := hook.(BeforeUpsertHook); ok { 198 | return ih.BeforeUpsert(ctx) 199 | } 200 | return nil 201 | } 202 | 203 | // afterUpsert calls custom AfterUpsert 204 | func afterUpsert(ctx context.Context, hook interface{}) error { 205 | if ih, ok := hook.(AfterUpsertHook); ok { 206 | return ih.AfterUpsert(ctx) 207 | } 208 | return nil 209 | } 210 | 211 | // do check if opType is supported and call hookHandler 212 | func do(ctx context.Context, hook interface{}, opType operator.OpType) error { 213 | if f, ok := hookHandler[opType]; !ok { 214 | return nil 215 | } else { 216 | return f(ctx, hook) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /hook/hook_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package hook 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/qiniu/qmgo/operator" 23 | "github.com/stretchr/testify/mock" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | type User struct { 28 | Name string `bson:"name"` 29 | Age int `bson:"age"` 30 | // for test 31 | AfterCalled bool `bson:"afterCalled"` 32 | } 33 | 34 | func (u *User) BeforeInsert(ctx context.Context) error { 35 | if u.Name == "Lucas" || u.Name == "Alices" { 36 | u.Age = 17 37 | } 38 | return nil 39 | } 40 | 41 | func (u *User) AfterInsert(ctx context.Context) error { 42 | u.AfterCalled = true 43 | return nil 44 | } 45 | 46 | func TestInsertHook(t *testing.T) { 47 | ast := require.New(t) 48 | ctx := context.Background() 49 | 50 | u := &User{Name: "Lucas", Age: 7} 51 | err := Do(ctx, u, operator.BeforeInsert) 52 | ast.NoError(err) 53 | ast.Equal(17, u.Age) 54 | 55 | err = Do(ctx, u, operator.AfterInsert) 56 | ast.NoError(err) 57 | ast.True(u.AfterCalled) 58 | 59 | u1, u2 := &User{Name: "Lucas", Age: 7}, &User{Name: "Alices", Age: 8} 60 | us := []interface{}{u1, u2} 61 | err = Do(ctx, us, operator.BeforeInsert) 62 | ast.NoError(err) 63 | err = Do(ctx, us, operator.AfterInsert) 64 | ast.NoError(err) 65 | for _, v := range us { 66 | vv := v.(*User) 67 | if vv.Name == "Lucas" { 68 | ast.Equal(17, vv.Age) 69 | } 70 | if vv.Name == "Alices" { 71 | ast.Equal(17, vv.Age) 72 | } 73 | ast.True(vv.AfterCalled) 74 | } 75 | 76 | u3 := User{Name: "Lucas", Age: 7} 77 | err = Do(ctx, u3, operator.BeforeInsert) 78 | ast.NoError(err) 79 | } 80 | 81 | func (u *User) BeforeUpdate(ctx context.Context) error { 82 | if u.Name == "Lucas" || u.Name == "Alices" { 83 | u.Age = 17 84 | } 85 | return nil 86 | } 87 | 88 | func (u *User) AfterUpdate(ctx context.Context) error { 89 | u.AfterCalled = true 90 | return nil 91 | } 92 | func TestUpdateHook(t *testing.T) { 93 | ast := require.New(t) 94 | ctx := context.Background() 95 | 96 | u := &User{Name: "Lucas", Age: 7} 97 | err := Do(ctx, u, operator.BeforeUpdate) 98 | ast.NoError(err) 99 | ast.Equal(17, u.Age) 100 | 101 | err = Do(ctx, u, operator.AfterUpdate) 102 | ast.NoError(err) 103 | ast.True(u.AfterCalled) 104 | 105 | u1, u2 := &User{Name: "Lucas", Age: 7}, &User{Name: "Alices", Age: 8} 106 | us := []interface{}{u1, u2} 107 | err = Do(ctx, us, operator.BeforeUpdate) 108 | ast.NoError(err) 109 | err = Do(ctx, us, operator.AfterUpdate) 110 | ast.NoError(err) 111 | for _, v := range us { 112 | vv := v.(*User) 113 | if vv.Name == "Lucas" { 114 | ast.Equal(17, vv.Age) 115 | } 116 | if vv.Name == "Alices" { 117 | ast.Equal(17, vv.Age) 118 | } 119 | ast.True(vv.AfterCalled) 120 | } 121 | 122 | } 123 | 124 | func (u *User) BeforeQuery(ctx context.Context) error { 125 | if u.Name == "Lucas" || u.Name == "Alices" { 126 | u.Age = 17 127 | } 128 | fmt.Println("into before query") 129 | sliceBeforeQueryCount++ 130 | return nil 131 | } 132 | 133 | func (u *User) AfterQuery(ctx context.Context) error { 134 | u.AfterCalled = true 135 | return nil 136 | } 137 | func TestQueryHook(t *testing.T) { 138 | ast := require.New(t) 139 | ctx := context.Background() 140 | 141 | u := &User{Name: "Lucas", Age: 7} 142 | err := Do(ctx, u, operator.BeforeQuery) 143 | ast.NoError(err) 144 | ast.Equal(17, u.Age) 145 | 146 | err = Do(ctx, u, operator.AfterQuery) 147 | ast.NoError(err) 148 | ast.True(u.AfterCalled) 149 | 150 | u1, u2 := &User{Name: "Lucas", Age: 7}, &User{Name: "Alices", Age: 8} 151 | us := []interface{}{u1, u2} 152 | err = Do(ctx, us, operator.BeforeQuery) 153 | ast.NoError(err) 154 | err = Do(ctx, us, operator.AfterQuery) 155 | ast.NoError(err) 156 | for _, v := range us { 157 | vv := v.(*User) 158 | if vv.Name == "Lucas" { 159 | ast.Equal(17, vv.Age) 160 | } 161 | if vv.Name == "Alices" { 162 | ast.Equal(17, vv.Age) 163 | } 164 | ast.True(vv.AfterCalled) 165 | } 166 | 167 | uss := []*User{&User{Name: "Lucas"}, &User{Name: "Alices"}} 168 | Do(ctx, &uss, operator.BeforeQuery) 169 | 170 | } 171 | func (u *User) BeforeRemove(ctx context.Context) error { 172 | if u.Name == "Lucas" || u.Name == "Alices" { 173 | u.Age = 17 174 | } 175 | return nil 176 | } 177 | 178 | func (u *User) AfterRemove(ctx context.Context) error { 179 | u.AfterCalled = true 180 | return nil 181 | } 182 | 183 | func TestRemoveHook(t *testing.T) { 184 | ast := require.New(t) 185 | ctx := context.Background() 186 | 187 | u := &User{Name: "Lucas", Age: 7} 188 | err := Do(ctx, u, operator.BeforeRemove) 189 | ast.NoError(err) 190 | ast.Equal(17, u.Age) 191 | 192 | err = Do(ctx, u, operator.AfterRemove) 193 | ast.NoError(err) 194 | ast.True(u.AfterCalled) 195 | 196 | u1, u2 := &User{Name: "Lucas", Age: 7}, &User{Name: "Alices", Age: 8} 197 | us := []interface{}{u1, u2} 198 | err = Do(ctx, us, operator.BeforeRemove) 199 | ast.NoError(err) 200 | err = Do(ctx, us, operator.AfterRemove) 201 | ast.NoError(err) 202 | for _, v := range us { 203 | vv := v.(*User) 204 | if vv.Name == "Lucas" { 205 | ast.Equal(17, vv.Age) 206 | } 207 | if vv.Name == "Alices" { 208 | ast.Equal(17, vv.Age) 209 | } 210 | ast.True(vv.AfterCalled) 211 | } 212 | 213 | } 214 | func (u *User) BeforeUpsert(ctx context.Context) error { 215 | if u.Name == "Lucas" || u.Name == "Alices" { 216 | u.Age = 17 217 | } 218 | return nil 219 | } 220 | 221 | func (u *User) AfterUpsert(ctx context.Context) error { 222 | u.AfterCalled = true 223 | return nil 224 | } 225 | 226 | func TestUpsertHook(t *testing.T) { 227 | ast := require.New(t) 228 | ctx := context.Background() 229 | 230 | u := &User{Name: "Lucas", Age: 7} 231 | err := Do(ctx, u, operator.BeforeUpsert) 232 | ast.NoError(err) 233 | ast.Equal(17, u.Age) 234 | 235 | err = Do(ctx, u, operator.AfterUpsert) 236 | ast.NoError(err) 237 | ast.True(u.AfterCalled) 238 | 239 | u1, u2 := &User{Name: "Lucas", Age: 7}, &User{Name: "Alices", Age: 8} 240 | us := []interface{}{u1, u2} 241 | err = Do(ctx, us, operator.BeforeUpsert) 242 | ast.NoError(err) 243 | err = Do(ctx, us, operator.AfterUpsert) 244 | ast.NoError(err) 245 | for _, v := range us { 246 | vv := v.(*User) 247 | if vv.Name == "Lucas" { 248 | ast.Equal(17, vv.Age) 249 | } 250 | if vv.Name == "Alices" { 251 | ast.Equal(17, vv.Age) 252 | } 253 | ast.True(vv.AfterCalled) 254 | } 255 | 256 | u3 := User{Name: "Lucas", Age: 7} 257 | err = Do(ctx, u3, operator.BeforeInsert) 258 | ast.NoError(err) 259 | } 260 | 261 | type UserError struct { 262 | Name string 263 | Age int 264 | 265 | // for test 266 | mock.Mock `bson:"-"` 267 | } 268 | 269 | func (u *UserError) BeforeInsert(ctx context.Context) error { 270 | return nil 271 | } 272 | 273 | func (u *UserError) AfterInsert(ctx context.Context) error { 274 | args := u.Called() 275 | return args.Error(0) 276 | } 277 | func TestSliceError(t *testing.T) { 278 | ast := require.New(t) 279 | ctx := context.Background() 280 | 281 | u1, u2 := &UserError{Name: "Lucas", Age: 7}, &UserError{Name: "Alices", Age: 8} 282 | us := []interface{}{u1, u2} 283 | 284 | u1.On("AfterInsert").Return(nil) 285 | u2.On("AfterInsert").Return(errors.New("called")) 286 | err := Do(ctx, us, operator.AfterInsert) 287 | ast.Equal("called", err.Error()) 288 | 289 | } 290 | 291 | type UserNoHook struct { 292 | Name string 293 | Age int 294 | 295 | // for test 296 | AfterCalled bool 297 | } 298 | 299 | func TestUserNoHook(t *testing.T) { 300 | ast := require.New(t) 301 | ctx := context.Background() 302 | 303 | u := &UserNoHook{Name: "Lucas", Age: 7} 304 | err := Do(ctx, u, operator.BeforeInsert) 305 | ast.NoError(err) 306 | ast.Equal(7, u.Age) 307 | 308 | err = Do(ctx, u, operator.AfterInsert) 309 | ast.NoError(err) 310 | 311 | u1, u2 := &UserNoHook{Name: "Lucas", Age: 7}, &UserNoHook{Name: "Alices", Age: 8} 312 | us := []interface{}{u1, u2} 313 | err = Do(ctx, us, operator.BeforeInsert) 314 | ast.NoError(err) 315 | err = Do(ctx, us, operator.AfterInsert) 316 | ast.NoError(err) 317 | for _, v := range us { 318 | vv := v.(*UserNoHook) 319 | if vv.Name == "Lucas" { 320 | ast.Equal(7, vv.Age) 321 | } 322 | if vv.Name == "Alices" { 323 | ast.Equal(8, vv.Age) 324 | } 325 | ast.False(vv.AfterCalled) 326 | } 327 | 328 | err = Do(ctx, u, operator.BeforeUpdate) 329 | ast.NoError(err) 330 | err = Do(ctx, u, operator.AfterUpdate) 331 | ast.NoError(err) 332 | err = Do(ctx, us, operator.BeforeUpdate) 333 | ast.NoError(err) 334 | err = Do(ctx, us, operator.AfterUpdate) 335 | ast.NoError(err) 336 | 337 | err = Do(ctx, u, operator.BeforeQuery) 338 | ast.NoError(err) 339 | err = Do(ctx, u, operator.AfterQuery) 340 | ast.NoError(err) 341 | err = Do(ctx, us, operator.BeforeQuery) 342 | ast.NoError(err) 343 | err = Do(ctx, us, operator.AfterQuery) 344 | ast.NoError(err) 345 | 346 | err = Do(ctx, u, operator.BeforeRemove) 347 | ast.NoError(err) 348 | err = Do(ctx, u, operator.AfterRemove) 349 | ast.NoError(err) 350 | err = Do(ctx, us, operator.BeforeRemove) 351 | ast.NoError(err) 352 | err = Do(ctx, us, operator.AfterRemove) 353 | ast.NoError(err) 354 | } 355 | 356 | var sliceBeforeQueryCount = 0 357 | 358 | func TestSliceHook(t *testing.T) { 359 | sliceBeforeQueryCount = 0 360 | ast := require.New(t) 361 | ctx := context.Background() 362 | 363 | u := &User{Name: "Lucas"} 364 | Do(ctx, u, operator.BeforeQuery) 365 | 366 | uss := []*User{&User{Name: "Lucas"}, &User{Name: "Alices"}} 367 | Do(ctx, uss, operator.BeforeQuery) 368 | 369 | Do(ctx, &uss, operator.BeforeQuery) 370 | 371 | ast.Equal(5, sliceBeforeQueryCount) 372 | 373 | } 374 | 375 | func TestNilError(t *testing.T) { 376 | ast := require.New(t) 377 | ctx := context.Background() 378 | 379 | err := Do(ctx, nil, operator.BeforeUpsert) 380 | ast.NoError(err) 381 | 382 | } 383 | 384 | func TestOpts(t *testing.T) { 385 | ast := require.New(t) 386 | ctx := context.Background() 387 | 388 | u := &User{Name: "Lucas", Age: 7} 389 | err := Do(ctx, nil, operator.BeforeInsert, u) 390 | ast.NoError(err) 391 | } 392 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import "go.mongodb.org/mongo-driver/mongo/options" 17 | 18 | // CollectionI 19 | // 集合操作接口 20 | //type CollectionI interface { 21 | // Find(filter interface{}) QueryI 22 | // InsertOne(doc interface{}) (*mongo.InsertOneResult, error) 23 | // InsertMany(docs ...interface{}) (*mongo.InsertManyResult, error) 24 | // Upsert(filter interface{}, replacement interface{}) (*mongo.UpdateResult, error) 25 | // UpdateOne(filter interface{}, update interface{}) error 26 | // UpdateAll(filter interface{}, update interface{}) (*mongo.UpdateResult, error) 27 | // DeleteOne(filter interface{}) error 28 | // RemoveAll(selector interface{}) (*mongo.DeleteResult, error) 29 | // EnsureIndex(indexes []string, isUnique bool) 30 | // EnsureIndexes(uniques []string, indexes []string) 31 | //} 32 | 33 | // Change holds fields for running a findAndModify command via the Query.Apply method. 34 | type Change struct { 35 | Update interface{} // update/replace document 36 | Replace bool // Whether to replace the document rather than updating 37 | Remove bool // Whether to remove the document found rather than updating 38 | Upsert bool // Whether to insert in case the document isn't found, take effect when Remove is false 39 | ReturnNew bool // Should the modified document be returned rather than the old one, take effect when Remove is false 40 | } 41 | 42 | // CursorI Cursor interface 43 | type CursorI interface { 44 | Next(result interface{}) bool 45 | Close() error 46 | Err() error 47 | All(results interface{}) error 48 | //ID() int64 49 | } 50 | 51 | // QueryI Query interface 52 | type QueryI interface { 53 | Collation(collation *options.Collation) QueryI 54 | SetArrayFilters(*options.ArrayFilters) QueryI 55 | Sort(fields ...string) QueryI 56 | Select(selector interface{}) QueryI 57 | Skip(n int64) QueryI 58 | BatchSize(n int64) QueryI 59 | NoCursorTimeout(n bool) QueryI 60 | Limit(n int64) QueryI 61 | One(result interface{}) error 62 | All(result interface{}) error 63 | Count(opts ...*options.CountOptions) (n int64, err error) 64 | EstimatedCount(opts ...*options.EstimatedDocumentCountOptions) (n int64, err error) 65 | Distinct(key string, result interface{}) error 66 | Cursor() CursorI 67 | Apply(change Change, result interface{}) error 68 | Hint(hint interface{}) QueryI 69 | } 70 | 71 | // AggregateI define the interface of aggregate 72 | type AggregateI interface { 73 | All(results interface{}) error 74 | One(result interface{}) error 75 | Iter() CursorI // Deprecated, please use Cursor instead 76 | Cursor() CursorI 77 | } 78 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/qiniu/qmgo/field" 6 | "github.com/qiniu/qmgo/hook" 7 | "github.com/qiniu/qmgo/operator" 8 | "github.com/qiniu/qmgo/validator" 9 | ) 10 | 11 | // callback define the callback function type 12 | type callback func(ctx context.Context, doc interface{}, opType operator.OpType, opts ...interface{}) error 13 | 14 | // middlewareCallback the register callback slice 15 | // some callbacks initial here without Register() for order 16 | var middlewareCallback = []callback{ 17 | hook.Do, 18 | field.Do, 19 | validator.Do, 20 | } 21 | 22 | // Register register callback into middleware 23 | func Register(cb callback) { 24 | middlewareCallback = append(middlewareCallback, cb) 25 | } 26 | 27 | // Do call every registers 28 | // The doc is always the document to operate 29 | func Do(ctx context.Context, content interface{}, opType operator.OpType, opts ...interface{}) error { 30 | for _, cb := range middlewareCallback { 31 | if err := cb(ctx, content, opType, opts...); err != nil { 32 | return err 33 | } 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/qiniu/qmgo/operator" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestMiddleware(t *testing.T) { 13 | ast := require.New(t) 14 | ctx := context.Background() 15 | // not register 16 | ast.NoError(Do(ctx, "success", operator.BeforeInsert)) 17 | 18 | // valid register 19 | Register(callbackTest) 20 | ast.NoError(Do(ctx, "success", operator.BeforeInsert)) 21 | ast.Error(Do(ctx, "failure", operator.BeforeUpsert)) 22 | ast.NoError(Do(ctx, "failure", operator.BeforeUpdate, "success")) 23 | } 24 | 25 | func callbackTest(ctx context.Context, doc interface{}, opType operator.OpType, opts ...interface{}) error { 26 | if doc.(string) == "success" && opType == operator.BeforeInsert { 27 | return nil 28 | } 29 | if len(opts) > 0 && opts[0].(string) == "success" { 30 | return nil 31 | } 32 | if doc.(string) == "failure" && opType == operator.BeforeUpsert { 33 | return fmt.Errorf("this is error") 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /operator/aggregation_pipeline_operators.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package operator 15 | 16 | // Aggregation Pipeline Operators 17 | // refer: https://docs.mongodb.com/manual/reference/operator/aggregation/ 18 | const ( 19 | // Arithmetic Expression Operators 20 | Abs = "$abs" 21 | Add = "$add" 22 | Ceil = "$ceil" 23 | Divide = "$divide" 24 | Exp = "$exp" 25 | Floor = "$floor" 26 | Ln = "$ln" 27 | Log = "$log" 28 | Log10 = "$log10" 29 | Multiply = "$multiply" 30 | Pow = "$pow" 31 | Round = "$round" 32 | Sqrt = "$sqrt" 33 | Subtract = "$subtract" 34 | Trunc = "$trunc" 35 | 36 | // Array Expression Operators 37 | ArrayElemAt = "$arrayElemAt" 38 | ArrayToObject = "$arrayToObject" 39 | ConcatArrays = "$concatArrays" 40 | Filter = "$filter" 41 | IndexOfArray = "$indexOfArray" 42 | IsArray = "$isArray" 43 | Map = "$map" 44 | ObjectToArray = "$objectToArray" 45 | Range = "$range" 46 | Reduce = "$reduce" 47 | ReverseArray = "$reverseArray" 48 | Zip = "$zip" 49 | 50 | // Comparison Expression Operators 51 | Cmp = "$cmp" 52 | 53 | // Conditional Expression Operators 54 | Cond = "$cond" 55 | IfNull = "$ifNull" 56 | Switch = "$switch" 57 | 58 | // Custom Aggregation Expression Operators 59 | Accumulator = "$accumulator" 60 | Function = "$function" 61 | 62 | // Data Size Operators 63 | BinarySize = "$binarySize" 64 | BsonSize = "$bsonSize" 65 | 66 | // Date Expression Operators 67 | DateFromParts = "$dateFromParts" 68 | DateFromString = "$dateFromString" 69 | DateToParts = "$dateToParts" 70 | DateToString = "$dateToString" 71 | DayOfMonth = "$dayOfMonth" 72 | DayOfWeek = "$dayOfWeek" 73 | DayOfYear = "$dayOfYear" 74 | Hour = "$hour" 75 | IsoDayOfWeek = "$isoDayOfWeek" 76 | IsoWeek = "$isoWeek" 77 | IsoWeekYear = "$isoWeekYear" 78 | Millisecond = "$millisecond" 79 | Minute = "$minute" 80 | Month = "$month" 81 | Second = "$second" 82 | ToDate = "$toDate" 83 | Week = "$week" 84 | Year = "$year" 85 | 86 | // Literal Expression Operator 87 | Literal = "$literal" 88 | 89 | // Object Expression Operators 90 | MergeObjects = "$mergeObjects" 91 | 92 | // Set Expression Operators 93 | AllElementsTrue = "$allElementsTrue" 94 | AnyElementTrue = "$anyElementTrue" 95 | SetDifference = "$setDifference" 96 | SetEquals = "$setEquals" 97 | SetIntersection = "$setIntersection" 98 | SetIsSubset = "$setIsSubset" 99 | SetUnion = "$setUnion" 100 | 101 | // String Expression Operators 102 | Concat = "$concat" 103 | IndexOfBytes = "$indexOfBytes" 104 | IndexOfCP = "$indexOfCP" 105 | Ltrim = "$ltrim" 106 | RegexFind = "$regexFind" 107 | RegexFindAll = "$regexFindAll" 108 | RegexMatch = "$regexMatch" 109 | Rtrim = "$rtrim" 110 | Split = "$split" 111 | StrLenBytes = "$strLenBytes" 112 | StrLenCP = "$strLenCP" 113 | Strcasecmp = "$strcasecmp" 114 | Substr = "$substr" 115 | SubstrBytes = "$substrBytes" 116 | SubstrCP = "$substrCP" 117 | ToLower = "$toLower" 118 | ToString = "$toString" 119 | Trim = "$trim" 120 | ToUpper = "$toUpper" 121 | ReplaceOne = "$replaceOne" 122 | ReplaceAll = "$replaceAll" 123 | 124 | // Trigonometry Expression Operators 125 | Sin = "$sin" 126 | Cos = "$cos" 127 | Tan = "$tan" 128 | Asin = "$asin" 129 | Acos = "$acos" 130 | Atan = "$atan" 131 | Atan2 = "$atan2" 132 | Asinh = "$asinh" 133 | Acosh = "$acosh" 134 | Atanh = "$atanh" 135 | DegreesToRadians = "$degreesToRadians" 136 | RadiansToDegrees = "$radiansToDegrees" 137 | 138 | // Type Expression Operators 139 | Convert = "$convert" 140 | ToBool = "$toBool" 141 | ToDecimal = "$toDecimal" 142 | ToDouble = "$toDouble" 143 | ToInt = "$toInt" 144 | ToLong = "$toLong" 145 | ToObjectID = "$toObjectId" 146 | IsNumber = "$isNumber" 147 | 148 | // Accumulators ($group) 149 | Avg = "$avg" 150 | First = "$first" 151 | Last = "$last" 152 | 153 | StdDevPop = "$stdDevPop" 154 | StdDevSamp = "$stdDevSamp" 155 | Sum = "$sum" 156 | 157 | // Variable Expression Operators 158 | Let = "$let" 159 | ) 160 | -------------------------------------------------------------------------------- /operator/aggregation_pipeline_stages.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package operator 15 | 16 | // define the aggregation pipeline stages 17 | // refer: https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/ 18 | const ( 19 | // Collection Aggregate Stages 20 | AddFields = "$addFields" 21 | Bucket = "$bucket" 22 | BucketAuto = "$bucketAuto" 23 | CollStats = "$collStats" 24 | Count = "$count" 25 | Facet = "$facet" 26 | GeoNear = "$geoNear" 27 | GraphLookup = "$graphLookup" 28 | Group = "$group" 29 | IndexStats = "$indexStats" 30 | Limit = "$limit" 31 | ListSessions = "$listSessions" 32 | Lookup = "$lookup" 33 | Match = "$match" 34 | Merge = "$merge" 35 | Out = "$out" 36 | PlanCacheStats = "$planCacheStats" 37 | Project = "$project" 38 | Redact = "$redact" 39 | ReplaceRoot = "$replaceRoot" 40 | ReplaceWith = "$replaceWith" 41 | Sample = "$sample" 42 | Skip = "$skip" 43 | SortByCount = "$sortByCount" 44 | UnionWith = "$unionWith" 45 | Unwind = "$unwind" 46 | 47 | // Database Aggregate stages 48 | CurrentOp = "$currentOp" 49 | ListLocalSessions = "$listLocalSessions" 50 | ) 51 | -------------------------------------------------------------------------------- /operator/operate_type.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | type OpType string 4 | 5 | const ( 6 | BeforeInsert OpType = "beforeInsert" 7 | AfterInsert OpType = "afterInsert" 8 | BeforeUpdate OpType = "beforeUpdate" 9 | AfterUpdate OpType = "afterUpdate" 10 | BeforeQuery OpType = "beforeQuery" 11 | AfterQuery OpType = "afterQuery" 12 | BeforeRemove OpType = "beforeRemove" 13 | AfterRemove OpType = "afterRemove" 14 | BeforeUpsert OpType = "beforeUpsert" 15 | AfterUpsert OpType = "afterUpsert" 16 | BeforeReplace OpType = "beforeReplace" 17 | AfterReplace OpType = "afterReplace" 18 | ) 19 | -------------------------------------------------------------------------------- /operator/query_and_projection.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package operator 15 | 16 | // define the query and projection operators 17 | // refer: https://docs.mongodb.com/manual/reference/operator/query/ 18 | const ( 19 | // Comparison 20 | Eq = "$eq" 21 | Gt = "$gt" 22 | Gte = "$gte" 23 | In = "$in" 24 | Lt = "$lt" 25 | Lte = "$lte" 26 | Ne = "$ne" 27 | Nin = "$nin" 28 | 29 | // Logical 30 | And = "$and" 31 | Not = "$not" 32 | Nor = "$nor" 33 | Or = "$or" 34 | 35 | // Element 36 | Exists = "$exists" 37 | Type = "$type" 38 | 39 | // Evaluation 40 | Expr = "$expr" 41 | JsonSchema = "$jsonSchema" 42 | Mod = "$mod" 43 | Regex = "$regex" 44 | Text = "$text" 45 | Where = "$where" 46 | 47 | // Geo spatial 48 | GeoIntersects = "$geoIntersects" 49 | GeoWithin = "$geoWithin" 50 | Near = "$near" 51 | NearSphere = "$nearSphere" 52 | 53 | // Array 54 | All = "$all" 55 | ElemMatch = "$elemMatch" 56 | Size = "$size" 57 | 58 | // Bitwise 59 | BitsAllClear = "$bitsAllClear" 60 | BitsAllSet = "$bitsAllSet" 61 | BitsAnyClear = "$bitsAnyClear" 62 | BitsAnySet = "$bitsAnySet" 63 | 64 | // Comments 65 | Comment = "$comment" 66 | 67 | // Projection operators 68 | Dollar = "$" 69 | Meta = "$meta" 70 | Slice = "$slice" 71 | ) 72 | -------------------------------------------------------------------------------- /operator/query_modifiers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package operator 15 | 16 | //Query Modifiers 17 | // refer:https://docs.mongodb.com/manual/reference/operator/query-modifier/ 18 | const ( 19 | // Modifiers 20 | Explain = "$explain" 21 | Hint = "$hint" 22 | MaxTimeMS = "$maxTimeMS" 23 | OrderBy = "$orderby" 24 | Query = "$query" 25 | ReturnKey = "$returnKey" 26 | ShowDiskLoc = "$showDiskLoc" 27 | 28 | // Sort Order 29 | Natural = "$natural" 30 | ) 31 | -------------------------------------------------------------------------------- /operator/update.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package operator 15 | 16 | // define the update operators 17 | // refer: https://docs.mongodb.com/manual/reference/operator/update/ 18 | const ( 19 | // Fields 20 | CurrentDate = "$currentDate" 21 | Inc = "$inc" 22 | Min = "$min" 23 | Max = "$max" 24 | Mul = "$mul" 25 | Rename = "$rename" 26 | Set = "$set" 27 | SetOnInsert = "$setOnInsert" 28 | Unset = "$unset" 29 | 30 | // Array Operators 31 | AddToSet = "$addToSet" 32 | Pop = "$pop" 33 | Pull = "$pull" 34 | Push = "$push" 35 | PullAll = "$pullAll" 36 | 37 | // Array modifiers 38 | Each = "$each" 39 | Position = "$position" 40 | Sort = "$sort" 41 | 42 | // Array bitwise 43 | Bit = "$bit" 44 | ) 45 | -------------------------------------------------------------------------------- /options/aggregate_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type AggregateOptions struct { 6 | *options.AggregateOptions 7 | } 8 | -------------------------------------------------------------------------------- /options/change_stream_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type ChangeStreamOptions struct { 6 | *options.ChangeStreamOptions 7 | } -------------------------------------------------------------------------------- /options/client_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type ClientOptions struct { 6 | *options.ClientOptions 7 | } 8 | -------------------------------------------------------------------------------- /options/collection_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type CollectionOptions struct { 6 | *options.CollectionOptions 7 | } 8 | -------------------------------------------------------------------------------- /options/createcollection_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type CreateCollectionOptions struct { 6 | *options.CreateCollectionOptions 7 | } -------------------------------------------------------------------------------- /options/database_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type DatabaseOptions struct { 6 | *options.DatabaseOptions 7 | } 8 | -------------------------------------------------------------------------------- /options/index_options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package options 15 | 16 | import "go.mongodb.org/mongo-driver/mongo/options" 17 | 18 | type IndexModel struct { 19 | Key []string // Index key fields; prefix name with dash (-) for descending order 20 | *options.IndexOptions 21 | } 22 | -------------------------------------------------------------------------------- /options/insert_options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package options 15 | 16 | import "go.mongodb.org/mongo-driver/mongo/options" 17 | 18 | type InsertOneOptions struct { 19 | InsertHook interface{} 20 | *options.InsertOneOptions 21 | } 22 | type InsertManyOptions struct { 23 | InsertHook interface{} 24 | *options.InsertManyOptions 25 | } 26 | -------------------------------------------------------------------------------- /options/query_options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package options 15 | 16 | type FindOptions struct { 17 | QueryHook interface{} 18 | } 19 | -------------------------------------------------------------------------------- /options/remove_options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package options 15 | 16 | import "go.mongodb.org/mongo-driver/mongo/options" 17 | 18 | type RemoveOptions struct { 19 | RemoveHook interface{} 20 | *options.DeleteOptions 21 | } 22 | -------------------------------------------------------------------------------- /options/replace_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type ReplaceOptions struct { 6 | UpdateHook interface{} 7 | *options.ReplaceOptions 8 | } 9 | -------------------------------------------------------------------------------- /options/runcmd_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type RunCommandOptions struct { 6 | *options.RunCmdOptions 7 | } 8 | -------------------------------------------------------------------------------- /options/session_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type SessionOptions struct { 6 | *options.SessionOptions 7 | } 8 | -------------------------------------------------------------------------------- /options/transaction_options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "go.mongodb.org/mongo-driver/mongo/options" 4 | 5 | type TransactionOptions struct { 6 | *options.TransactionOptions 7 | } 8 | -------------------------------------------------------------------------------- /options/update_options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package options 15 | 16 | import "go.mongodb.org/mongo-driver/mongo/options" 17 | 18 | type UpdateOptions struct { 19 | UpdateHook interface{} 20 | *options.UpdateOptions 21 | } 22 | -------------------------------------------------------------------------------- /options/upsert_options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package options 15 | 16 | import "go.mongodb.org/mongo-driver/mongo/options" 17 | 18 | type UpsertOptions struct { 19 | UpsertHook interface{} 20 | *options.ReplaceOptions 21 | } 22 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "reflect" 20 | 21 | "github.com/qiniu/qmgo/middleware" 22 | "github.com/qiniu/qmgo/operator" 23 | qOpts "github.com/qiniu/qmgo/options" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/bson/bsoncodec" 26 | "go.mongodb.org/mongo-driver/mongo" 27 | "go.mongodb.org/mongo-driver/mongo/options" 28 | ) 29 | 30 | // Query struct definition 31 | type Query struct { 32 | filter interface{} 33 | sort interface{} 34 | project interface{} 35 | hint interface{} 36 | arrayFilters *options.ArrayFilters 37 | limit *int64 38 | skip *int64 39 | batchSize *int64 40 | noCursorTimeout *bool 41 | collation *options.Collation 42 | 43 | ctx context.Context 44 | collection *mongo.Collection 45 | opts []qOpts.FindOptions 46 | registry *bsoncodec.Registry 47 | } 48 | 49 | func (q *Query) Collation(collation *options.Collation) QueryI { 50 | newQ := q 51 | newQ.collation = collation 52 | return newQ 53 | } 54 | 55 | func (q *Query) NoCursorTimeout(n bool) QueryI { 56 | newQ := q 57 | newQ.noCursorTimeout = &n 58 | return newQ 59 | } 60 | 61 | // BatchSize sets the value for the BatchSize field. 62 | // Means the maximum number of documents to be included in each batch returned by the server. 63 | func (q *Query) BatchSize(n int64) QueryI { 64 | newQ := q 65 | newQ.batchSize = &n 66 | return newQ 67 | } 68 | 69 | // Sort is Used to set the sorting rules for the returned results 70 | // Format: "age" or "+age" means to sort the age field in ascending order, "-age" means in descending order 71 | // When multiple sort fields are passed in at the same time, they are arranged in the order in which the fields are passed in. 72 | // For example, {"age", "-name"}, first sort by age in ascending order, then sort by name in descending order 73 | func (q *Query) Sort(fields ...string) QueryI { 74 | if len(fields) == 0 { 75 | // A nil bson.D will not correctly serialize, but this case is no-op 76 | // so an early return will do. 77 | return q 78 | } 79 | 80 | var sorts bson.D 81 | for _, field := range fields { 82 | key, n := SplitSortField(field) 83 | if key == "" { 84 | panic("Sort: empty field name") 85 | } 86 | sorts = append(sorts, bson.E{Key: key, Value: n}) 87 | } 88 | newQ := q 89 | newQ.sort = sorts 90 | return newQ 91 | } 92 | 93 | // SetArrayFilter use for apply update array 94 | // For Example : 95 | // var res = QueryTestItem{} 96 | // change := Change{ 97 | // Update: bson.M{"$set": bson.M{"instock.$[elem].qty": 100}}, 98 | // ReturnNew: false, 99 | // } 100 | // cli.Find(context.Background(), bson.M{"name": "Lucas"}). 101 | // SetArrayFilters(&options.ArrayFilters{Filters: []interface{}{bson.M{"elem.warehouse": bson.M{"$in": []string{"C", "F"}}},}}). 102 | // Apply(change, &res) 103 | func (q *Query) SetArrayFilters(filter *options.ArrayFilters) QueryI { 104 | newQ := q 105 | newQ.arrayFilters = filter 106 | return newQ 107 | } 108 | 109 | // Select is used to determine which fields are displayed or not displayed in the returned results 110 | // Format: bson.M{"age": 1} means that only the age field is displayed 111 | // bson.M{"age": 0} means to display other fields except age 112 | // When _id is not displayed and is set to 0, it will be returned to display 113 | func (q *Query) Select(projection interface{}) QueryI { 114 | newQ := q 115 | newQ.project = projection 116 | return newQ 117 | } 118 | 119 | // Skip skip n records 120 | func (q *Query) Skip(n int64) QueryI { 121 | newQ := q 122 | newQ.skip = &n 123 | return newQ 124 | } 125 | 126 | // Hint sets the value for the Hint field. 127 | // This should either be the index name as a string or the index specification 128 | // as a document. The default value is nil, which means that no hint will be sent. 129 | func (q *Query) Hint(hint interface{}) QueryI { 130 | newQ := q 131 | newQ.hint = hint 132 | return newQ 133 | } 134 | 135 | // Limit limits the maximum number of documents found to n 136 | // The default value is 0, and 0 means no limit, and all matching results are returned 137 | // When the limit value is less than 0, the negative limit is similar to the positive limit, but the cursor is closed after returning a single batch result. 138 | // Reference https://docs.mongodb.com/manual/reference/method/cursor.limit/index.html 139 | func (q *Query) Limit(n int64) QueryI { 140 | newQ := q 141 | newQ.limit = &n 142 | return newQ 143 | } 144 | 145 | // One query a record that meets the filter conditions 146 | // If the search fails, an error will be returned 147 | func (q *Query) One(result interface{}) error { 148 | if len(q.opts) > 0 { 149 | if err := middleware.Do(q.ctx, q.opts[0].QueryHook, operator.BeforeQuery); err != nil { 150 | return err 151 | } 152 | } 153 | opt := options.FindOne() 154 | 155 | if q.collation != nil { 156 | opt.SetCollation(q.collation) 157 | } 158 | if q.sort != nil { 159 | opt.SetSort(q.sort) 160 | } 161 | if q.project != nil { 162 | opt.SetProjection(q.project) 163 | } 164 | if q.skip != nil { 165 | opt.SetSkip(*q.skip) 166 | } 167 | if q.hint != nil { 168 | opt.SetHint(q.hint) 169 | } 170 | 171 | err := q.collection.FindOne(q.ctx, q.filter, opt).Decode(result) 172 | 173 | if err != nil { 174 | return err 175 | } 176 | if len(q.opts) > 0 { 177 | if err := middleware.Do(q.ctx, q.opts[0].QueryHook, operator.AfterQuery); err != nil { 178 | return err 179 | } 180 | } 181 | return nil 182 | } 183 | 184 | // All query multiple records that meet the filter conditions 185 | // The static type of result must be a slice pointer 186 | func (q *Query) All(result interface{}) error { 187 | if len(q.opts) > 0 { 188 | if err := middleware.Do(q.ctx, q.opts[0].QueryHook, operator.BeforeQuery); err != nil { 189 | return err 190 | } 191 | } 192 | opt := options.Find() 193 | if q.collation != nil { 194 | opt.SetCollation(q.collation) 195 | } 196 | if q.sort != nil { 197 | opt.SetSort(q.sort) 198 | } 199 | if q.project != nil { 200 | opt.SetProjection(q.project) 201 | } 202 | if q.limit != nil { 203 | opt.SetLimit(*q.limit) 204 | } 205 | if q.skip != nil { 206 | opt.SetSkip(*q.skip) 207 | } 208 | if q.hint != nil { 209 | opt.SetHint(q.hint) 210 | } 211 | if q.batchSize != nil { 212 | opt.SetBatchSize(int32(*q.batchSize)) 213 | } 214 | if q.noCursorTimeout != nil { 215 | opt.SetNoCursorTimeout(*q.noCursorTimeout) 216 | } 217 | 218 | var err error 219 | var cursor *mongo.Cursor 220 | 221 | cursor, err = q.collection.Find(q.ctx, q.filter, opt) 222 | 223 | c := Cursor{ 224 | ctx: q.ctx, 225 | cursor: cursor, 226 | err: err, 227 | } 228 | err = c.All(result) 229 | if err != nil { 230 | return err 231 | } 232 | if len(q.opts) > 0 { 233 | if err := middleware.Do(q.ctx, q.opts[0].QueryHook, operator.AfterQuery); err != nil { 234 | return err 235 | } 236 | } 237 | return nil 238 | } 239 | 240 | // Count count the number of eligible entries 241 | func (q *Query) Count(opts ...*options.CountOptions) (n int64, err error) { 242 | opt := options.MergeCountOptions(opts...) 243 | if q.limit != nil { 244 | opt.SetLimit(*q.limit) 245 | } 246 | if q.skip != nil { 247 | opt.SetSkip(*q.skip) 248 | } 249 | 250 | return q.collection.CountDocuments(q.ctx, q.filter, opt) 251 | } 252 | 253 | // EstimatedCount count the number of the collection by using the metadata 254 | func (q *Query) EstimatedCount(opts ...*options.EstimatedDocumentCountOptions) (n int64, err error) { 255 | co := options.MergeEstimatedDocumentCountOptions(opts...) 256 | 257 | return q.collection.EstimatedDocumentCount(q.ctx, co) 258 | } 259 | 260 | // Distinct gets the unique value of the specified field in the collection and return it in the form of slice 261 | // result should be passed a pointer to slice 262 | // The function will verify whether the static type of the elements in the result slice is consistent with the data type obtained in mongodb 263 | // reference https://docs.mongodb.com/manual/reference/command/distinct/ 264 | func (q *Query) Distinct(key string, result interface{}) error { 265 | resultVal := reflect.ValueOf(result) 266 | 267 | if resultVal.Kind() != reflect.Ptr { 268 | return ErrQueryNotSlicePointer 269 | } 270 | 271 | resultElmVal := resultVal.Elem() 272 | if resultElmVal.Kind() != reflect.Interface && resultElmVal.Kind() != reflect.Slice { 273 | return ErrQueryNotSliceType 274 | } 275 | 276 | opt := options.Distinct() 277 | res, err := q.collection.Distinct(q.ctx, key, q.filter, opt) 278 | if err != nil { 279 | return err 280 | } 281 | registry := q.registry 282 | if registry == nil { 283 | registry = bson.DefaultRegistry 284 | } 285 | valueType, valueBytes, err_ := bson.MarshalValueWithRegistry(registry, res) 286 | if err_ != nil { 287 | fmt.Printf("bson.MarshalValue err: %+v\n", err_) 288 | return err_ 289 | } 290 | 291 | rawValue := bson.RawValue{Type: valueType, Value: valueBytes} 292 | err = rawValue.Unmarshal(result) 293 | if err != nil { 294 | fmt.Printf("rawValue.Unmarshal err: %+v\n", err) 295 | return ErrQueryResultTypeInconsistent 296 | } 297 | 298 | return nil 299 | } 300 | 301 | // Cursor gets a Cursor object, which can be used to traverse the query result set 302 | // After obtaining the CursorI object, you should actively call the Close interface to close the cursor 303 | func (q *Query) Cursor() CursorI { 304 | opt := options.Find() 305 | 306 | if q.sort != nil { 307 | opt.SetSort(q.sort) 308 | } 309 | if q.project != nil { 310 | opt.SetProjection(q.project) 311 | } 312 | if q.limit != nil { 313 | opt.SetLimit(*q.limit) 314 | } 315 | if q.skip != nil { 316 | opt.SetSkip(*q.skip) 317 | } 318 | 319 | if q.batchSize != nil { 320 | opt.SetBatchSize(int32(*q.batchSize)) 321 | } 322 | if q.noCursorTimeout != nil { 323 | opt.SetNoCursorTimeout(*q.noCursorTimeout) 324 | } 325 | 326 | var err error 327 | var cur *mongo.Cursor 328 | cur, err = q.collection.Find(q.ctx, q.filter, opt) 329 | return &Cursor{ 330 | ctx: q.ctx, 331 | cursor: cur, 332 | err: err, 333 | } 334 | } 335 | 336 | // Apply runs the findAndModify command, which allows updating, replacing 337 | // or removing a document matching a query and atomically returning either the old 338 | // version (the default) or the new version of the document (when ReturnNew is true) 339 | // 340 | // The Sort and Select query methods affect the result of Apply. In case 341 | // multiple documents match the query, Sort enables selecting which document to 342 | // act upon by ordering it first. Select enables retrieving only a selection 343 | // of fields of the new or old document. 344 | // 345 | // When Change.Replace is true, it means replace at most one document in the collection 346 | // and the update parameter must be a document and cannot contain any update operators; 347 | // if no objects are found and Change.Upsert is false, it will returns ErrNoDocuments. 348 | // When Change.Remove is true, it means delete at most one document in the collection 349 | // and returns the document as it appeared before deletion; if no objects are found, 350 | // it will returns ErrNoDocuments. 351 | // When both Change.Replace and Change.Remove are false,it means update at most one document 352 | // in the collection and the update parameter must be a document containing update operators; 353 | // if no objects are found and Change.Upsert is false, it will returns ErrNoDocuments. 354 | // 355 | // reference: https://docs.mongodb.com/manual/reference/command/findAndModify/ 356 | func (q *Query) Apply(change Change, result interface{}) error { 357 | var err error 358 | 359 | if change.Remove { 360 | err = q.findOneAndDelete(change, result) 361 | } else if change.Replace { 362 | err = q.findOneAndReplace(change, result) 363 | } else { 364 | err = q.findOneAndUpdate(change, result) 365 | } 366 | 367 | return err 368 | } 369 | 370 | // findOneAndDelete 371 | // reference: https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndDelete/ 372 | func (q *Query) findOneAndDelete(change Change, result interface{}) error { 373 | opts := options.FindOneAndDelete() 374 | if q.sort != nil { 375 | opts.SetSort(q.sort) 376 | } 377 | if q.project != nil { 378 | opts.SetProjection(q.project) 379 | } 380 | 381 | return q.collection.FindOneAndDelete(q.ctx, q.filter, opts).Decode(result) 382 | } 383 | 384 | // findOneAndReplace 385 | // reference: https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/ 386 | func (q *Query) findOneAndReplace(change Change, result interface{}) error { 387 | opts := options.FindOneAndReplace() 388 | if q.sort != nil { 389 | opts.SetSort(q.sort) 390 | } 391 | if q.project != nil { 392 | opts.SetProjection(q.project) 393 | } 394 | if change.Upsert { 395 | opts.SetUpsert(change.Upsert) 396 | } 397 | if change.ReturnNew { 398 | opts.SetReturnDocument(options.After) 399 | } 400 | 401 | err := q.collection.FindOneAndReplace(q.ctx, q.filter, change.Update, opts).Decode(result) 402 | if change.Upsert && !change.ReturnNew && err == mongo.ErrNoDocuments { 403 | return nil 404 | } 405 | 406 | return err 407 | } 408 | 409 | // findOneAndUpdate 410 | // reference: https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/ 411 | func (q *Query) findOneAndUpdate(change Change, result interface{}) error { 412 | opts := options.FindOneAndUpdate() 413 | if q.sort != nil { 414 | opts.SetSort(q.sort) 415 | } 416 | if q.project != nil { 417 | opts.SetProjection(q.project) 418 | } 419 | if change.Upsert { 420 | opts.SetUpsert(change.Upsert) 421 | } 422 | if change.ReturnNew { 423 | opts.SetReturnDocument(options.After) 424 | } 425 | 426 | if q.arrayFilters != nil { 427 | opts.SetArrayFilters(*q.arrayFilters) 428 | } 429 | 430 | err := q.collection.FindOneAndUpdate(q.ctx, q.filter, change.Update, opts).Decode(result) 431 | if change.Upsert && !change.ReturnNew && err == mongo.ErrNoDocuments { 432 | return nil 433 | } 434 | 435 | return err 436 | } 437 | -------------------------------------------------------------------------------- /results.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | // InsertOneResult is the result type returned by an InsertOne operation. 17 | type InsertOneResult struct { 18 | // The _id of the inserted document. A value generated by the driver will be of type primitive.ObjectID. 19 | InsertedID interface{} 20 | } 21 | 22 | // InsertManyResult is a result type returned by an InsertMany operation. 23 | type InsertManyResult struct { 24 | // The _id values of the inserted documents. Values generated by the driver will be of type primitive.ObjectID. 25 | InsertedIDs []interface{} 26 | } 27 | 28 | // UpdateResult is the result type returned from UpdateOne, UpdateMany, and ReplaceOne operations. 29 | type UpdateResult struct { 30 | MatchedCount int64 // The number of documents matched by the filter. 31 | ModifiedCount int64 // The number of documents modified by the operation. 32 | UpsertedCount int64 // The number of documents upsert by the operation. 33 | UpsertedID interface{} // The _id field of the upsert document, or nil if no upsert was done. 34 | } 35 | 36 | // DeleteResult is the result type returned by DeleteOne and DeleteMany operations. 37 | type DeleteResult struct { 38 | DeletedCount int64 // The number of documents deleted. 39 | } 40 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | 19 | opts "github.com/qiniu/qmgo/options" 20 | "go.mongodb.org/mongo-driver/mongo" 21 | "go.mongodb.org/mongo-driver/mongo/options" 22 | "go.mongodb.org/mongo-driver/x/mongo/driver" 23 | ) 24 | 25 | // Session is an struct that represents a MongoDB logical session 26 | type Session struct { 27 | session mongo.Session 28 | } 29 | 30 | // StartTransaction starts transaction 31 | //precondition: 32 | //- version of mongoDB server >= v4.0 33 | //- Topology of mongoDB server is not Single 34 | //At the same time, please pay attention to the following 35 | //- make sure all operations in callback use the sessCtx as context parameter 36 | //- Dont forget to call EndSession if session is not used anymore 37 | //- if operations in callback takes more than(include equal) 120s, the operations will not take effect, 38 | //- if operation in callback return qmgo.ErrTransactionRetry, 39 | // the whole transaction will retry, so this transaction must be idempotent 40 | //- if operations in callback return qmgo.ErrTransactionNotSupported, 41 | //- If the ctx parameter already has a Session attached to it, it will be replaced by this session. 42 | func (s *Session) StartTransaction(ctx context.Context, cb func(sessCtx context.Context) (interface{}, error), opts ...*opts.TransactionOptions) (interface{}, error) { 43 | transactionOpts := options.Transaction() 44 | if len(opts) > 0 && opts[0].TransactionOptions != nil { 45 | transactionOpts = opts[0].TransactionOptions 46 | } 47 | result, err := s.session.WithTransaction(ctx, wrapperCustomCb(cb), transactionOpts) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return result, nil 52 | } 53 | 54 | // EndSession will abort any existing transactions and close the session. 55 | func (s *Session) EndSession(ctx context.Context) { 56 | s.session.EndSession(ctx) 57 | } 58 | 59 | // AbortTransaction aborts the active transaction for this session. This method will return an error if there is no 60 | // active transaction for this session or the transaction has been committed or aborted. 61 | func (s *Session) AbortTransaction(ctx context.Context) error { 62 | return s.session.AbortTransaction(ctx) 63 | } 64 | 65 | // wrapperCustomF wrapper caller's callback function to mongo dirver's 66 | func wrapperCustomCb(cb func(ctx context.Context) (interface{}, error)) func(sessCtx mongo.SessionContext) (interface{}, error) { 67 | return func(sessCtx mongo.SessionContext) (interface{}, error) { 68 | result, err := cb(sessCtx) 69 | if err == ErrTransactionRetry { 70 | return nil, mongo.CommandError{Labels: []string{driver.TransientTransactionError}} 71 | } 72 | return result, err 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "fmt" 20 | "testing" 21 | 22 | opts "github.com/qiniu/qmgo/options" 23 | "github.com/stretchr/testify/require" 24 | "go.mongodb.org/mongo-driver/bson" 25 | "go.mongodb.org/mongo-driver/mongo/options" 26 | "go.mongodb.org/mongo-driver/mongo/readpref" 27 | ) 28 | 29 | func initTransactionClient(coll string) *QmgoClient { 30 | cfg := Config{ 31 | Uri: "mongodb://localhost:27017", 32 | Database: "transaction", 33 | Coll: coll, 34 | } 35 | var cTimeout int64 = 0 36 | var sTimeout int64 = 500000 37 | var maxPoolSize uint64 = 30000 38 | var minPoolSize uint64 = 0 39 | cfg.ConnectTimeoutMS = &cTimeout 40 | cfg.SocketTimeoutMS = &sTimeout 41 | cfg.MaxPoolSize = &maxPoolSize 42 | cfg.MinPoolSize = &minPoolSize 43 | cfg.ReadPreference = &ReadPref{Mode: readpref.PrimaryMode} 44 | qClient, err := Open(context.Background(), &cfg) 45 | if err != nil { 46 | fmt.Println(err) 47 | panic(err) 48 | } 49 | qClient.InsertOne(context.Background(), bson.M{"name": "before_transaction"}) 50 | return qClient 51 | 52 | } 53 | func TestClient_DoTransaction(t *testing.T) { 54 | ast := require.New(t) 55 | ctx := context.Background() 56 | cli := initTransactionClient("test") 57 | defer cli.DropDatabase(ctx) 58 | 59 | fn := func(sCtx context.Context) (interface{}, error) { 60 | if _, err := cli.InsertOne(sCtx, bson.D{{"abc", int32(1)}}); err != nil { 61 | return nil, err 62 | } 63 | if _, err := cli.InsertOne(sCtx, bson.D{{"xyz", int32(999)}}); err != nil { 64 | return nil, err 65 | } 66 | return nil, nil 67 | } 68 | tops := options.Transaction() 69 | op := &opts.TransactionOptions{TransactionOptions: tops} 70 | _, err := cli.DoTransaction(ctx, fn, op) 71 | ast.NoError(err) 72 | r := bson.M{} 73 | cli.Find(ctx, bson.M{"abc": 1}).One(&r) 74 | ast.Equal(r["abc"], int32(1)) 75 | 76 | cli.Find(ctx, bson.M{"xyz": 999}).One(&r) 77 | ast.Equal(r["xyz"], int32(999)) 78 | } 79 | 80 | func TestSession_AbortTransaction(t *testing.T) { 81 | ast := require.New(t) 82 | cli := initTransactionClient("test") 83 | 84 | defer cli.DropCollection(context.Background()) 85 | sOpts := options.Session().SetSnapshot(false) 86 | o := &opts.SessionOptions{sOpts} 87 | s, err := cli.Session(o) 88 | ast.NoError(err) 89 | ctx := context.Background() 90 | defer s.EndSession(ctx) 91 | 92 | callback := func(sCtx context.Context) (interface{}, error) { 93 | if _, err := cli.InsertOne(sCtx, bson.D{{"abc", int32(1)}}); err != nil { 94 | return nil, err 95 | } 96 | if _, err := cli.InsertOne(sCtx, bson.D{{"xyz", int32(999)}}); err != nil { 97 | return nil, err 98 | } 99 | err = s.AbortTransaction(sCtx) 100 | 101 | return nil, nil 102 | } 103 | 104 | _, err = s.StartTransaction(ctx, callback) 105 | ast.NoError(err) 106 | 107 | r := bson.M{} 108 | err = cli.Find(ctx, bson.M{"abc": 1}).One(&r) 109 | ast.Error(err) 110 | // abort the already worked operation, can't abort the later operation 111 | // it seems a mongodb-go-driver bug 112 | err = cli.Find(ctx, bson.M{"xyz": 999}).One(&r) 113 | ast.Error(err) 114 | } 115 | 116 | func TestSession_Cancel(t *testing.T) { 117 | ast := require.New(t) 118 | cli := initTransactionClient("test") 119 | 120 | defer cli.DropCollection(context.Background()) 121 | s, err := cli.Session() 122 | ast.NoError(err) 123 | ctx := context.Background() 124 | defer s.EndSession(ctx) 125 | 126 | callback := func(sCtx context.Context) (interface{}, error) { 127 | if _, err := cli.InsertOne(sCtx, bson.D{{"abc", int32(1)}}); err != nil { 128 | return nil, err 129 | } 130 | if _, err := cli.InsertOne(sCtx, bson.D{{"xyz", int32(999)}}); err != nil { 131 | return nil, err 132 | } 133 | return nil, errors.New("cancel operations") 134 | } 135 | _, err = s.StartTransaction(ctx, callback) 136 | ast.Error(err) 137 | r := bson.M{} 138 | err = cli.Find(ctx, bson.M{"abc": 1}).One(&r) 139 | ast.True(IsErrNoDocuments(err)) 140 | err = cli.Find(ctx, bson.M{"xyz": 999}).One(&r) 141 | ast.True(IsErrNoDocuments(err)) 142 | } 143 | 144 | func TestSession_RetryTransAction(t *testing.T) { 145 | ast := require.New(t) 146 | cli := initTransactionClient("test") 147 | defer cli.DropCollection(context.Background()) 148 | s, err := cli.Session() 149 | ast.NoError(err) 150 | ctx := context.Background() 151 | defer s.EndSession(ctx) 152 | 153 | count := 0 154 | callback := func(sCtx context.Context) (interface{}, error) { 155 | if _, err := cli.InsertOne(sCtx, bson.D{{"abc", int32(1)}}); err != nil { 156 | return nil, err 157 | } 158 | if _, err := cli.InsertOne(sCtx, bson.D{{"xyz", int32(999)}}); err != nil { 159 | return nil, err 160 | } 161 | if count == 0 { 162 | count++ 163 | return nil, ErrTransactionRetry 164 | } 165 | return nil, nil 166 | } 167 | _, err = s.StartTransaction(ctx, callback) 168 | ast.NoError(err) 169 | r := bson.M{} 170 | cli.Find(ctx, bson.M{"abc": 1}).One(&r) 171 | ast.Equal(r["abc"], int32(1)) 172 | cli.Find(ctx, bson.M{"xyz": 999}).One(&r) 173 | ast.Equal(r["xyz"], int32(999)) 174 | ast.Equal(count, 1) 175 | } 176 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "math" 18 | "strconv" 19 | "strings" 20 | "time" 21 | 22 | "go.mongodb.org/mongo-driver/bson/primitive" 23 | ) 24 | 25 | // Now return Millisecond current time 26 | func Now() time.Time { 27 | return time.Unix(0, time.Now().UnixNano()/1e6*1e6) 28 | } 29 | 30 | // NewObjectID generates a new ObjectID. 31 | // Watch out: the way it generates objectID is different from mgo 32 | func NewObjectID() primitive.ObjectID { 33 | return primitive.NewObjectID() 34 | } 35 | 36 | // SplitSortField handle sort symbol: "+"/"-" in front of field 37 | // if "+", return sort as 1 38 | // if "-", return sort as -1 39 | func SplitSortField(field string) (key string, sort int32) { 40 | sort = 1 41 | key = field 42 | 43 | if len(field) != 0 { 44 | switch field[0] { 45 | case '+': 46 | key = strings.TrimPrefix(field, "+") 47 | sort = 1 48 | case '-': 49 | key = strings.TrimPrefix(field, "-") 50 | sort = -1 51 | } 52 | } 53 | 54 | return key, sort 55 | } 56 | 57 | // CompareVersions compares two version number strings (i.e. positive integers separated by 58 | // periods). Comparisons are done to the lesser precision of the two versions. For example, 3.2 is 59 | // considered equal to 3.2.11, whereas 3.2.0 is considered less than 3.2.11. 60 | // 61 | // Returns a positive int if version1 is greater than version2, a negative int if version1 is less 62 | // than version2, and 0 if version1 is equal to version2. 63 | func CompareVersions(v1 string, v2 string) (int, error) { 64 | n1 := strings.Split(v1, ".") 65 | n2 := strings.Split(v2, ".") 66 | 67 | for i := 0; i < int(math.Min(float64(len(n1)), float64(len(n2)))); i++ { 68 | i1, err := strconv.Atoi(n1[i]) 69 | if err != nil { 70 | return 0, err 71 | } 72 | i2, err := strconv.Atoi(n2[i]) 73 | if err != nil { 74 | return 0, err 75 | } 76 | difference := i1 - i2 77 | if difference != 0 { 78 | return difference, nil 79 | } 80 | } 81 | 82 | return 0, nil 83 | } 84 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Qmgo Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package qmgo 15 | 16 | import ( 17 | "fmt" 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | func TestNow(t *testing.T) { 25 | t1 := time.Unix(0, time.Now().UnixNano()/1e6*1e6) 26 | t2 := Now() 27 | fmt.Println(t1, t2) 28 | } 29 | 30 | func TestNewObjectID(t *testing.T) { 31 | objId := NewObjectID() 32 | objId.Hex() 33 | } 34 | 35 | func TestCompareVersions(t *testing.T) { 36 | ast := require.New(t) 37 | i, err := CompareVersions("4.4.0", "3.0") 38 | ast.NoError(err) 39 | ast.True(i > 0) 40 | i, err = CompareVersions("3.0.1", "3.0") 41 | ast.NoError(err) 42 | ast.True(i == 0) 43 | i, err = CompareVersions("3.1.5", "4.0") 44 | ast.NoError(err) 45 | ast.True(i < 0) 46 | } 47 | -------------------------------------------------------------------------------- /validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "time" 7 | 8 | "github.com/go-playground/validator/v10" 9 | "github.com/qiniu/qmgo/operator" 10 | ) 11 | 12 | // use a single instance of Validate, it caches struct info 13 | var validate = validator.New() 14 | 15 | // SetValidate let validate can use custom rules 16 | func SetValidate(v *validator.Validate) { 17 | validate = v 18 | } 19 | 20 | // validatorNeeded checks if the validator is needed to opType 21 | func validatorNeeded(opType operator.OpType) bool { 22 | switch opType { 23 | case operator.BeforeInsert, operator.BeforeUpsert, operator.BeforeReplace: 24 | return true 25 | } 26 | return false 27 | } 28 | 29 | // Do calls validator check 30 | // Don't use opts here 31 | func Do(ctx context.Context, doc interface{}, opType operator.OpType, opts ...interface{}) error { 32 | if !validatorNeeded(opType) { 33 | return nil 34 | } 35 | to := reflect.TypeOf(doc) 36 | if to == nil { 37 | return nil 38 | } 39 | switch reflect.TypeOf(doc).Kind() { 40 | case reflect.Slice: 41 | return sliceHandle(doc, opType) 42 | case reflect.Ptr: 43 | v := reflect.ValueOf(doc).Elem() 44 | switch v.Kind() { 45 | case reflect.Slice: 46 | return sliceHandle(v.Interface(), opType) 47 | default: 48 | return do(doc) 49 | } 50 | default: 51 | return do(doc) 52 | } 53 | } 54 | 55 | // sliceHandle handles the slice docs 56 | func sliceHandle(docs interface{}, opType operator.OpType) error { 57 | // []interface{}{UserType{}...} 58 | if h, ok := docs.([]interface{}); ok { 59 | for _, v := range h { 60 | if err := do(v); err != nil { 61 | return err 62 | } 63 | } 64 | return nil 65 | } 66 | // []UserType{} 67 | s := reflect.ValueOf(docs) 68 | for i := 0; i < s.Len(); i++ { 69 | if err := do(s.Index(i).Interface()); err != nil { 70 | 71 | return err 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | // do check if opType is supported and call fieldHandler 78 | func do(doc interface{}) error { 79 | if !validatorStruct(doc) { 80 | return nil 81 | } 82 | return validate.Struct(doc) 83 | } 84 | 85 | // validatorStruct check if kind of doc is validator supported struct 86 | // same implement as validator 87 | func validatorStruct(doc interface{}) bool { 88 | val := reflect.ValueOf(doc) 89 | if val.Kind() == reflect.Ptr && !val.IsNil() { 90 | val = val.Elem() 91 | } 92 | if val.Kind() != reflect.Struct || val.Type() == reflect.TypeOf(time.Time{}) { 93 | return false 94 | } 95 | return true 96 | } 97 | -------------------------------------------------------------------------------- /validator/validator_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator/v10" 6 | "github.com/qiniu/qmgo/operator" 7 | "github.com/stretchr/testify/require" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "testing" 10 | ) 11 | 12 | // User contains user information 13 | type User struct { 14 | FirstName string `bson:"fname"` 15 | LastName string `bson:"lname"` 16 | Age uint8 `bson:"age" validate:"gte=0,lte=130"` 17 | Email string `bson:"e-mail" validate:"required,email"` 18 | FavouriteColor string `bson:"favouriteColor" validate:"hexcolor|rgb|rgba"` 19 | Addresses []*Address `bson:"addresses" validate:"required,dive,required"` // a person can have a home and cottage... 20 | } 21 | 22 | // Address houses a users address information 23 | type Address struct { 24 | Street string `validate:"required"` 25 | City string `validate:"required"` 26 | Planet string `validate:"required"` 27 | Phone string `validate:"required"` 28 | } 29 | 30 | // CustomRule use custom rule 31 | type CustomRule struct { 32 | Name string `validate:"required,foo"` 33 | } 34 | 35 | func TestValidator(t *testing.T) { 36 | ast := require.New(t) 37 | ctx := context.Background() 38 | 39 | user := &User{} 40 | // not need validator op 41 | ast.NoError(Do(ctx, user, operator.BeforeRemove)) 42 | ast.NoError(Do(ctx, user, operator.AfterInsert)) 43 | // check success 44 | address := &Address{ 45 | Street: "Eavesdown Docks", 46 | Planet: "Persphone", 47 | Phone: "none", 48 | City: "Unknown", 49 | } 50 | 51 | user = &User{ 52 | FirstName: "", 53 | LastName: "", 54 | Age: 45, 55 | Email: "1234@gmail.com", 56 | FavouriteColor: "#000", 57 | Addresses: []*Address{address, address}, 58 | } 59 | ast.NoError(Do(ctx, user, operator.BeforeInsert)) 60 | ast.NoError(Do(ctx, user, operator.BeforeUpsert)) 61 | ast.NoError(Do(ctx, *user, operator.BeforeUpsert)) 62 | 63 | users := []*User{user, user, user} 64 | ast.NoError(Do(ctx, users, operator.BeforeInsert)) 65 | 66 | // check failure 67 | user.Age = 150 68 | ast.Error(Do(ctx, user, operator.BeforeInsert)) 69 | user.Age = 22 70 | user.Email = "1234@gmail" // invalid email 71 | ast.Error(Do(ctx, user, operator.BeforeInsert)) 72 | user.Email = "1234@gmail.com" 73 | user.Addresses[0].City = "" // string tag use default value 74 | ast.Error(Do(ctx, user, operator.BeforeInsert)) 75 | 76 | // input slice 77 | users = []*User{user, user, user} 78 | ast.Error(Do(ctx, users, operator.BeforeInsert)) 79 | 80 | useris := []interface{}{user, user, user} 81 | ast.Error(Do(ctx, useris, operator.BeforeInsert)) 82 | 83 | user.Addresses[0].City = "shanghai" 84 | users = []*User{user, user, user} 85 | ast.NoError(Do(ctx, users, operator.BeforeInsert)) 86 | 87 | us := []User{*user, *user, *user} 88 | ast.NoError(Do(ctx, us, operator.BeforeInsert)) 89 | ast.NoError(Do(ctx, &us, operator.BeforeInsert)) 90 | 91 | // all bson type 92 | mdoc := []interface{}{bson.M{"name": "", "age": 12}, bson.M{"name": "", "age": 12}} 93 | ast.NoError(Do(ctx, mdoc, operator.BeforeInsert)) 94 | adoc := bson.A{"Alex", "12"} 95 | ast.NoError(Do(ctx, adoc, operator.BeforeInsert)) 96 | edoc := bson.E{"Alex", "12"} 97 | ast.NoError(Do(ctx, edoc, operator.BeforeInsert)) 98 | ddoc := bson.D{{"foo", "bar"}, {"hello", "world"}, {"pi", 3.14159}} 99 | ast.NoError(Do(ctx, ddoc, operator.BeforeInsert)) 100 | 101 | // nil ptr 102 | user = nil 103 | ast.NoError(Do(ctx, user, operator.BeforeInsert)) 104 | ast.NoError(Do(ctx, nil, operator.BeforeInsert)) 105 | 106 | // use custom rules 107 | customRule := &CustomRule{Name: "bar"} 108 | v := validator.New() 109 | _ = v.RegisterValidation("foo", func(fl validator.FieldLevel) bool { 110 | return fl.Field().String() == "bar" 111 | }) 112 | SetValidate(v) 113 | ast.NoError(Do(ctx, customRule, operator.BeforeInsert)) 114 | } 115 | -------------------------------------------------------------------------------- /validator_test.go: -------------------------------------------------------------------------------- 1 | package qmgo 2 | 3 | import ( 4 | "context" 5 | "go.mongodb.org/mongo-driver/bson" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // User contains user information 13 | type User struct { 14 | FirstName string `bson:"fname"` 15 | LastName string `bson:"lname"` 16 | Age uint8 `bson:"age" validate:"gte=0,lte=130" ` // Age must in [0,130] 17 | Email string `bson:"e-mail" validate:"required,email"` // Email can't be empty string, and must has email format 18 | CreateAt time.Time `bson:"createAt" validate:"lte"` // CreateAt must lte than current time 19 | Relations map[string]string `bson:"relations" validate:"max=2"` // Relations can't has more than 2 elements 20 | } 21 | 22 | func TestValidator(t *testing.T) { 23 | ast := require.New(t) 24 | cli := initClient("test") 25 | ctx := context.Background() 26 | defer cli.Close(ctx) 27 | defer cli.DropCollection(ctx) 28 | 29 | user := &User{ 30 | FirstName: "", 31 | LastName: "", 32 | Age: 45, 33 | Email: "1234@gmail.com", 34 | } 35 | _, err := cli.InsertOne(ctx, user) 36 | ast.NoError(err) 37 | 38 | user.Age = 200 // invalid age 39 | _, err = cli.InsertOne(ctx, user) 40 | ast.Error(err) 41 | 42 | users := []*User{user, user, user} 43 | _, err = cli.InsertMany(ctx, users) 44 | ast.Error(err) 45 | 46 | user.Age = 20 47 | user.Email = "1234@gmail" // email tag, invalid email 48 | err = cli.ReplaceOne(ctx, bson.M{"age": 45}, user) 49 | ast.Error(err) 50 | 51 | user.Email = "" // required tag, invalid empty string 52 | _, err = cli.Upsert(ctx, bson.M{"age": 45}, user) 53 | ast.Error(err) 54 | 55 | user.Email = "1234@gmail.com" 56 | user.CreateAt = time.Now().Add(1 * time.Hour) // lte tag for time, time must lte current time 57 | _, err = cli.Upsert(ctx, bson.M{"age": 45}, user) 58 | ast.Error(err) 59 | 60 | user.CreateAt = time.Now() 61 | user.Relations = map[string]string{"Alex": "friend", "Joe": "friend"} 62 | _, err = cli.Upsert(ctx, bson.M{"age": 45}, user) 63 | ast.NoError(err) 64 | 65 | user.Relations = map[string]string{"Alex": "friend", "Joe": "friend", "Bob": "sister"} // max tag, numbers of map 66 | _, err = cli.Upsert(ctx, bson.M{"age": 45}, user) 67 | ast.Error(err) 68 | } 69 | --------------------------------------------------------------------------------