├── .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 |
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 | })
--------------------------------------------------------------------------------