├── .gitignore └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # IDE 3 | .idea/* 4 | .vscode 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Cheatsheet for Mongoose Query Methods

2 |

A cheatsheet of methods you can use to implement common patterns in mongoose data-fetching(eg pagination, filtering etc).

3 | 4 |

5 | Screenshot-2021-09-22-at-17-39-15 6 |

7 | 8 | ### Base Example 9 | We'll work with a couple of queries which have not been resolved. 10 | Since each method returns the overall instance, we can chain methods 11 | 12 | ```javascript 13 | const todosQuery = Todo.find({}); 14 | const userQuery = User.find({}); 15 | ``` 16 | 17 | ### Filtering By Value 18 | Using the `where()` method in conjugation with some other methods, based on value we can filter out certain results. 19 | 20 | The following methods are usually used with the `where()` method: 21 | 22 | * `equals()` - equal to 23 | ```javascript 24 | userQuery.where('age').equals(13); // results where user age is 18 25 | ``` 26 | * `gt()` - greater than 27 | ```javascript 28 | userQuery.where('age').equals(13); // results where user age is more than 18 29 | ``` 30 | * `lt()` - less than 31 | ```javascript 32 | userQuery.where('age').equals(13); // results where user age is less than 18 33 | ``` 34 | * `gte()` - greater than or equal to 35 | ```javascript 36 | userQuery.where('age').gte(13); // results where user age is greater than or equal to 18 37 | ``` 38 | * `lte()` - less than or equal to 39 | ```javascript 40 | userQuery.where('age').lte(18); // results where user age is less than or equal to 18 41 | ``` 42 | * `mod()` - reminder 43 | ```javascript 44 | userQuery.where('age').mod([2, 1]); // all users where add is odd 45 | ``` 46 | * `ne()` - not equal to 47 | ```javascript 48 | userQuery.where('age').ne(13); // results where age is not 13 49 | ``` 50 | * `size()` - size of array 51 | ```javascript 52 | userQuery.where('projects').size(); // 3 53 | ``` 54 | * `regex()` - use regex 55 | ```javascript 56 | userQuery.where('name').regex(/abc/i); // results where the name matches the pattern 57 | ``` 58 | * `slice()` - JavaScript slice on the array of results 59 | ```javascript 60 | // projects is an array 61 | userQuery.where('projects').slice(2); // results after doing the slice on each result 62 | ``` 63 | * `all()` - all elements in the argument array is present 64 | ```javascript 65 | // projects is an array 66 | userQuery.where('projects').all(['p1', 'p2', 'p3']); // results where the field has all the specified elements 67 | ``` 68 | 69 | ### Sorting 70 | #### sort() 71 | The field name is the key, and the value states whether it's ascending or descending. There are different ways to implement this: 72 | ```javascript 73 | // -1 or 1 74 | todosQuery.sort({ title: -1, description: 1 }); 75 | 76 | // 'ascending' or 'descending' 77 | todosQuery.sort({ title: 'descending', description: 'ascending' }); 78 | 79 | // 'asc' or 'desc' 80 | todosQuery.sort({ title: 'desc', description: 'asc' }); 81 | 82 | // shorthand - title is ascending and description is descending 83 | todosQuery.sort('title -description'); 84 | ``` 85 | 86 | ### Pagination 87 | We can use a combination of `limit` and `skip` to implement pagination easily. 88 | Before that, let's look at how these two methods work. 89 | #### limit() 90 | ```javascript 91 | todosQuery.limit(100); 92 | ``` 93 | 94 | #### skip() 95 | This will skip the first `x` results of the `find` query. 96 | ```javascript 97 | todosQuery.skip(20); 98 | ``` 99 | 100 | #### Combining To Implement Pagination 101 | Let's say that we are on page `2` and each page contains 10 results. 102 | We'll create a variable called `page` which is `2` and `number` which is `10`. 103 | 104 | So, we'll skip the first `(page - 1) * number` which will send the results 11 and above. 105 | 106 | ```javascript 107 | const page = 2; 108 | const number = 10; 109 | 110 | todosQuery.skip((page - 1) * number); 111 | ``` 112 | 113 | Until now, we've skipped the first 10 results, but we still show all of remaining results. 114 | Therefore, let's use the `limit()` method to limit the number of results. 115 | 116 | ```javascript 117 | const page = 2; 118 | const number = 10; 119 | 120 | // skip = 2-1 * 10 = first 10 results 121 | // limit = 2 * 10 = first 20 results 122 | // the results are limited to the first 11 to 20 results 123 | todosQuery.skip((page - 1) * number).limit(page * 10); 124 | ``` 125 | 126 | ### Performance 127 | #### explain() 128 | This method returns execution stats instead of the data. 129 | It can be useful when comparing different approaches and analyzing which one is more performative. 130 | 131 | ```javascript 132 | const stats = await todosQuery.explain(); 133 | ``` 134 | 135 | #### lean() 136 | Lean removes all the `getters`, `setters` and the `virtuals` from the document that is returned by mongoose. 137 | The object returned is a plain JavaScript object and not a mongoose query compared to other query methods. 138 | 139 | ```javascript 140 | const user = await userQuery.lean(); 141 | ``` 142 | 143 | >Chaining further query methods will not work as the mongoose query is not returned, the result is. 144 | > It's like calling the `exec()` method. 145 | 146 |
147 | #### cursor() 148 | Cursors are the native way mongodb navigates through the database. 149 | With mongoose, an array is returned as a result of `find()` but the native driver returns a `Cursor`. 150 | 151 | Mongoose allows us to get the data in the form a cursor/stream because it may be more performant in some cases. 152 | 153 | ```javascript 154 | // There are 2 ways to use a cursor. First, as a stream: 155 | Thing. 156 | find({ name: /^hello/ }). 157 | cursor(). 158 | on('data', function(doc) { console.log(doc); }). 159 | on('end', function() { console.log('Done!'); }); 160 | 161 | // Or you can use `.next()` to manually get the next doc in the stream. 162 | // `.next()` returns a promise, so you can use promises or callbacks. 163 | const cursor = Thing.find({ name: /^hello/ }).cursor(); 164 | cursor.next(function(error, doc) { 165 | console.log(doc); 166 | }); 167 | 168 | // Because `.next()` returns a promise, you can use co 169 | // to easily iterate through all documents without loading them 170 | // all into memory. 171 | const cursor = Thing.find({ name: /^hello/ }).cursor(); 172 | for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) { 173 | console.log(doc); 174 | } 175 | ``` 176 | 177 | ### Schema 178 | 179 | A Schema is a central concept in Mongoose. A Schema is mapped to a MongoDB collection; it defines the shape of the documents in that collection. 180 | You can read about different [types you can use in Schema here](https://mongoosejs.com/docs/schematypes.html). 181 | 182 | #### Base Example 183 | You will work on a collection that stores the number of hits of a web page. Each document in the collection stores the page path (URL) and the number of hits the page got. 184 | 185 | **Example:** 186 | 187 | ``` 188 | { 189 | pagePath: '/contacts' 190 | hits: 10 191 | } 192 | ``` 193 | 194 | **Define a `Schema` and export it as a `Model`:** 195 | 196 | ``` 197 | // PageHits.js 198 | 199 | const pageHitsSchema = new mongoose.Schema({ 200 | pagePath: String, 201 | hits: number, 202 | }); 203 | 204 | export { mongoose.model("PageHits", pageHitsSchema) }; 205 | ``` 206 | 207 | **Instantiate a `document` using a `Model`:** 208 | 209 | ``` 210 | import { PageHits } from 'PageHits'; 211 | 212 | async function recordPageHits(pageHit) { 213 | const john = await PageHits.create(pageHit); 214 | } 215 | 216 | const pageHit = { 217 | pagePath: '/contact', 218 | hits: 8, 219 | }; 220 | 221 | recordPageHits(pageHit); 222 | ``` 223 | 224 | ### Validation 225 | 226 | You can set the data to be automatically validated before the save so data integrity is maintained. 227 | 228 | **Make `hits` a mandatory value:** 229 | 230 | ``` 231 | const pageHitsSchema = new mongoose.Schema({ 232 | pagePath: String, 233 | hits: { 234 | type: Number, 235 | required: true, 236 | }, 237 | }); 238 | ``` 239 | 240 | ### Querying 241 | 242 | You can use queries on instantiated documents. 243 | 244 | **`find()` - Find all pages with page hits higher than `n`:** 245 | 246 | ``` 247 | async function getPageHitsHigherThan(n) { 248 | const result = await PageHits.find({ 249 | hits: {$gt: n}, 250 | }); 251 | } 252 | ``` 253 | 254 | **`deleteOne()` - Delete the "page hit" record of a page:** 255 | 256 | ``` 257 | async function deletePageHitRecord(page) { 258 | const result = await PageHits.deleteOne({ 259 | pagePath: page, 260 | }); 261 | } 262 | ``` 263 | 264 | 265 | ### Instance methods 266 | 267 | You can define methods on **document instances**. 268 | 269 | **Write an instance method to find all the pages with same number of hits as the current page:** 270 | 271 | ``` 272 | const pageHitsSchema = new mongoose.Schema({ 273 | pagePath: String, 274 | hits: { 275 | type: Number, 276 | required: true, 277 | }, 278 | methods: { 279 | pagesWithSameHits() { 280 | return mongoose.model('PageHits').find({ 281 | hits: {eq: this.hits} 282 | }); 283 | }, 284 | }, 285 | }); 286 | 287 | export { mongoose.model("PageHits", pageHitsSchema) }; 288 | ``` 289 | 290 | *Tip: Don't use arrow functions when defining instance methods, `this` will not work.* 291 | 292 | **Now, you can find all the pages that have the same number of hits are the current page:** 293 | 294 | ``` 295 | const aboutPageHits = new PageHits({ 296 | pagePath: '/about', 297 | hits: 40, 298 | }); 299 | 300 | aboutPageHits.pagesWithSameHits((err, allPagesWithSameHits) => { 301 | console.log(allPagesWithSameHits); 302 | }) 303 | ``` 304 | 305 | ### Static methods 306 | 307 | You can define static methods on the **Model**. 308 | 309 | **Write a static method, `pagesWithAtleastHits()`, to find all the pages with at least `n` hits:** 310 | 311 | ``` 312 | const pageHitsSchema = new mongoose.Schema({ 313 | pagePath: String, 314 | hits: { 315 | type: Number, 316 | required: true, 317 | }, 318 | statics: { 319 | pagesWithAtleastHits() { 320 | return this.find({ 321 | hits: {gte: n} 322 | }); 323 | }, 324 | }, 325 | }); 326 | 327 | export { mongoose.model("PageHits", pageHitsSchema) }; 328 | ``` 329 | 330 | *Tip: Don't use arrow functions when defining static methods, `this` will not work.* 331 | 332 | **Find all the pages with at least `n` hits:** 333 | 334 | ``` 335 | const pages = await PageHits.pagesWithAtleastHits(n); 336 | ``` 337 | 338 | 339 | ### Instance vs Static methods 340 | 341 | Remember, you use a Schema to create a Model. Then the you can use the Model to create a Document instance. Schema relates to the whole collection whereas a Document instance relates to a particular document in the collection. 342 | 343 | |Instance Methods|Static Methods| 344 | |:---|:---| 345 | |Instance methods work on the Document instance.|Static methods work on the Model.| 346 | |Use an instance method if you want to do something in relation to a particular document in the collection.|Use a static method if you want to operate on the entire collection. | 347 | 348 | 349 | ### Query Helpers 350 | 351 | You can add query helper functions which let you extend mongoose's [chainable query builder API](https://mongoosejs.com/docs/queries.html). 352 | 353 | You need to use either `find()` or `where()` before you call the query helper method. You can also extend it like you would a normal query chain. 354 | 355 | **Define a query helper method, `pagesWithNHits()`, to find all pages with `n` page hits:** 356 | 357 | ``` 358 | const pageHitsSchema = new mongoose.Schema({ 359 | pagePath: String, 360 | hits: { 361 | type: Number, 362 | required: true, 363 | }, 364 | query: { 365 | pagesWithNHits(n) { 366 | return this.where({ 367 | hits: {eq: n} 368 | }); 369 | }, 370 | }, 371 | }); 372 | 373 | export { mongoose.model("PageHits", pageHitsSchema) }; 374 | ``` 375 | 376 | 377 | Use the query helper, `pagesWithNHits()`, to find all pages with `n` page hits. 378 | 379 | ``` 380 | PageHits.find().pagesWithNHits(n).exec((arr, pages) => { 381 | console.log(pages); 382 | }); 383 | ``` 384 | 385 | ### Virtual properties 386 | 387 | Virtual properties are not stored in MongoDB. They only exist in Mongoose. 388 | 389 | Virtuals provide `get` to retrieve values for further manipulation and `set` to manipulates values and then store it in the document. 390 | 391 | 392 | **Now `pageHitInfo()` can be used to print information about a current PageHit document:** 393 | 394 | ``` 395 | const pageHitsSchema = new mongoose.Schema({ 396 | pagePath: String, 397 | hits: { 398 | type: Number, 399 | required: true, 400 | }, 401 | virtuals: { 402 | pageHitInfo: { 403 | get() { 404 | return `${this.pagePath} has ${} hit(s).`; 405 | } 406 | set(hits) { 407 | this.hits = hits; 408 | } 409 | }, 410 | } 411 | }); 412 | 413 | export { mongoose.model("PageHits", pageHitsSchema) }; 414 | ``` 415 | 416 | **Use the setter `pageHitInfo`:** 417 | 418 | ``` 419 | const aboutPageHits = new PageHits({ 420 | pagePath: '/about', 421 | pageHitInfo: 40, 422 | }); 423 | ``` 424 | 425 | **Use the getter `pageHitInfo`:** 426 | 427 | ``` 428 | console.log(aboutPageHits.pageHitInfo); 429 | // /about has 40 hit(s). 430 | 431 | }) --------------------------------------------------------------------------------