You are on page 1of 10

RoboMongo provide a visual experience to deal with Mongo databases

Mongo will be separated from the outside world (Internet) by a web server (Node),
so this web server will filter any request before it hosts our database

Mongoose is a ORM/ODM (Object Relational/Data Mapper), an industry standard to


help us work with our database

Mongo allows us to have multiple internal databases within a single Mongo instance
to be able to work on multiple projects with no additional setups

Each database could have more than one Collection, its the core unit of what
stores data inside of a Mongo database, each collection is related to single type
of resources that will be available in our app (no mixing of type of data)

The core concepts of Mongo/Mongoose are CRUD operations

Mocha is the most popular framework for doing any type of javascript testing
around the Node.JS environment

Mongoose won't try to find any instance of Mongo in our local machine, instead we
need to hard-code it: mongoose.connect('mongodb://localhost:port/db_name');
if 'db_name' does not exist, it will be created (node test/test_helper.js)

Model represents all of the records that sit inside of the related collection
Schema describe exact properties of each record in the collection and their types

const mongoose = require("mongoose");


const UserSchema = new mongoose.Schema({ name: String });
module.exports = mongoose.model("User", UserSchema);
==> name of collection created in our db is users: lowercase 'User' + 's', we can
override this behaviour: ... = new Schema({ ... }, { collection: "alt-name" });

Mocha gives us global access to describe() & it() functions (unlike assert()):
const assert = require("assert");
describe("test name", () => { it("instruction", () => { assert(1+1 === 2); }); });

describe() could include many calls for it()


test result depend on assert() return value, if there's no assertion, test is
considered "passing"
to run the test: package.json --> scripts --> "test": "mocha" then npm run test

let joe = new User({ name: 'joe' });


==> create an instance of User model without adding it to the collection
joe.save() ==> add instance as a record to the collection related to User model
added record has an _id of type object containg a string representing the id.
only when adding at least 1 record to a collection for its db to be displayed

a hook is a function that will be executed before any test get executed:
we need to use hook to clean-up database before any test:
beforeEach((done) => {mongoose.connection.collections.users.drop(() => {done()})})
must call for done() to indicate that drop operation is done & can pass to test

each interaction with the db (mongoose to mongo) is asynchronous, so it take some


time and we're going to deal with it as a promise:
it('test', (done) => { ... joe.save().then(() => { assert(!joe.isNew); done(); })
isNew mean that instance does exist in Node but not in database
mongo promises (mpromise) are deprecated ==> mongoose.Promise = global.Promise;
better solution is to use async await syntax with try catch statement
must wrap mongoose connection order by: before(done => { once open => done(); })
to make sure that all tests get executed after connection has been made

using beforeEach() INSIDE describe() make it run only before each test inside it.

1 bodies of describe tests (ignore it() functions) + successor of describes


2 execute before() (regardless of names of files)
3 clg name of 1st describe > beforeEach
4 wait for body of before to be executed > run 1st test (all steps of it())
5 clg name of 2nd describe > 1st beforeEach > 2nd beforeEach inside of describe
6 run 2nd test (all steps of it()) ...

an instance of a model have an id even before it's been saved (unlike other ORMs)

it("finds all users with a name of joe", (done) => {


User.find({ name: "Joe" }).then((results) => {
assert(joe._id.toString() === results[0]._id.toString()); done(); }); });
==> results are an array of objects representing records matching the criteria

to work with mocha along with nodemon (auto refresh):


"test": "nodemon --exec mocha -R min" (or \"mocha -R min\" >> windows problem)
"mocha --watch" not 100% compatible with mongoose

it("using create()", (done) => { joe = new User({ name: "Joe" });
User.create(joe).then((res) => { assert(res.name === "Joe"); done(); }); });
==> alternative way of adding a record using collection.create(instance)

User.findOne({ _id: x._id }).then(user => { assert(user.name === 'x'); done; })


==> find 1st user matching the given criteria
with async await syntax we say: const user = User.findOne( ...

it("model instance remove", (done) => {


joe.remove().then(() => User.findOne({ name: "Joe" }))
.then((user) => { assert(user === null); done(); }); });

it("model instance remove", async () => {


await joe.remove();
const user = await User.findOne({ name: "Joe" });
assert(user === null); });

==> old vs new syntax of removing a record using an instance need to get rid of
done() to avoid error: Specify a callback (done) *or* return a Promise; not both.

it("class method remove", (done) => { // collection.remove() is deprecated


User.deleteOne({ name: "Joe" }).then( ... ).then((user) ... }); });
==> remove 1 record (vs ALL records using deleteMany()) with the given criteria

User.findOneAndDelete({ name: 'joe' }).then( ... ==> remove only 1st occurence
User.findByIdAndDelete(joe._id).then( ... ==> remove by id

it("instance update using set n save", (done) => { joe.set("name", "Alex");


joe.save().then(() => User.find({}))
.then((users) => { assert(users.length === 1); assert(users[0].name === "Alex");
done(); });
==> updating record using an instance, if we have multiple update functions better
not include save in each one: update1(user); update2(user); user.save();
this method prevent us from touching the database twice
User.updateOne({name: "joe"}, { name: "Alex" })
User.updateMany({}, { name: "Alex" })
==> update one record vs many records that suit the criteria
==> instance.update(), collection.update(), collection.findOneAndUpdate() &
findByIdAndUpdate() are deprecated, to make it work add this option:
mongoose.connect("url", { ..., useFindAndModify: false });

avoid loading data from Mongo into our server as much as possible to enhance
performance, in particular cases we will rely on Mongo to make changes instead of
our server using Mongo update operators

after adding new field called 'count' set initially to 1:


it("inc update", (done) => {
User.updateOne({ name: "Joe" }, { $inc: { count: 2 } })
.then(.. findOne.. ).then(user => { assert(user.count === 3); done(); }); });
==> we incremented value of count by 2 to become 3.

other update operators: docs.mongodb.com/manual/reference/operator/update/

mongoose provide validating functionalities: validateSync() return a validation


result better used to validate Model instance whilst validate() we get access to
result in its callback function which used when we need an interaction with db

... = new Schema({ name: { type: String, required: [true, "required"] }, ...
it("requires a user's name", () => {
const user = new User({ name: undefined });
const validationResult = user.validateSync();
const { message } = validationResult.errors.name;
assert(message === "Name is required"); });

... = new Schema({ name: { type: String,


validate: { validator: (name) => name.length > 2, message: "Name length" }, ...
it("length", () => { ... assert(message === "Name length") }

it("prevent saving invalid record", (done) => {


... user.save().catch(validationResult => {
assert(validationResult.errors.name.message === "Name length"); done(); }); });

when embedding a collection as a new field in a Model we don't don't need to make
another Model for it, we only need to make a Schema (array of nested resource)

module.exports = PostSchema;
PostSchema = require('./post');
... = new Schema({ ..., posts: [PostSchema]};
==> import the sub-document and used as value of a new field

it('create a document having a subdocument', (done) => {


const joe = new User({ ..., posts: [{ title: 'title' }] });
joe.save().. .then(user => { assert(user.posts[0].title === 'title'); done(); });

it('adding subdocument to existing record', (done) => {


const joe = new User({ ..., posts: [] });
joe.save().. .then(user => { user.posts.push({ title: 'title' });
return user.save(); }).then(... findOne ...).then(user => {
assert(user.posts[0].title === 'title'); done(); });
==> save entire record after creating subdoc & cannot save subdoc by itself

it('remove a subdocument'), (done) => {


... = new User(... ); joe.save().then(findOne ...)
.then(user => { user.posts[0].remove(); return user.save(); })
.then(... ).then(user => { assert(user.posts.length === 0); done(); }) });

Virtual type is any field on a Model that does not get persistant over MongoDB
database it does not get saved to db, its a derivative of other field(s)

UserSchema.virtual('postCount').get(function() { return this.posts.length });


console.log(joe.postCount); ==> return posts.length of joe
==> 'this' keyword referring the instance of the Model we're working on, that's
why it only works using function() syntax (context problem)

to make mocha ignore a test we rename it() to become xit() so it won't be executed

when its not necessary true that an embedded document is related directly to the
main document (think about a comment made by user1 on post made by user2), maybe
its better to use a separated collection (related by ids):
pros: easy to find list of embedded collection related to specific main collection
cons: touch db multiple times (no JOIN operation in MongoDB)

const BlogPostSchema = new Schema({ ..., comments: [{


type: Schema.Types.ObjectId, ref: 'Comment' }] });
module.exports = mongoose.model('blogPost', BlogPostSchema);
const CommentSchema = new Schema({ ...,
user: { type: Schema.Types.ObjectId, ref: 'User' } })
module.exports = mongoose.model('comment', CommentSchema);
const UserSchema = new mongoose.Schema({ ..., blogPosts: [{
type: Schema.Types.ObjectId, ref: 'BlogPost' }] });
==> ref should match concerned model's exact name [:'(]

beforeEach(done => { // refactor drop to include all collections


const { users, comments, blogposts } = mongoose.connection.collections;
users.drop(() => { comments.drop(() => { blogposts.drop(() => { done(); } }} });

require User, Comment & BlogPost


describe('Associations', () => { let joe, joePost, joeComment;
befreEach(done => { joe = new User({ name: 'Joe' });
joePost = new blogPost({ title: 'title', content: 'joe\'s post' });
joeComment = new Comment({ content: '7assa biya naziha?' });
joe.blogPosts.push(joePost); // even that blogPosts field is of type ObjectId
joePost.comments.push(joeComment);
joeComment.user = joe; // no need to push since its not an array
// if we want to save() multiple instances we'll have multiple promises when we
// actually need 1 end-point (last executed promise) to make .then(done) ==>
Promise.all([joe.save(), joePost.save(), joeComment.save()])
.then(() => done()); }); // promises get executed in parallel
it('save relation between user & blogPost', done => {
User.findOne({ name: 'Joe' }).then(user => { done(); } }); });

to run a single test & ignore the rest replace conserned it() to it.only()

to load particular nested data of a Model instance we use the "modifier" populate:
Model.query( criteria ).populate('name_of_field_as_string').then()
User.findOne({ name: 'Joe' }}).populate('blogPosts').then(user => {
assert(user.blogPosts[0].title = 'title'); done(); });

we can only load ALL nested data EXPLICITLY, Mongo don't provide a way for it:

it('saves a full relation graph', done => {


User.findOne({ name: 'Joe' }).populate(
{ path: 'blogPosts', populate: { path: 'blogPosts', populate: {
path: 'comments', populate: { path: 'user' } } } })
.then(user => {
assert(user.name === 'Joe'); assert(user.blogPosts[0].title === 'title');
assert(... comments); assert(... user.name); done(); }); )};
==> adding model property to popuplate is only necessary when we want to link data
from multiple databases

to delete all related data including subdocument to a collection we use middleware


Middlewares in Mongo are pre & post hooks: functions that get executed before and
after an event (save, remove, init & validate events)

UserSchema.pre('remove', function(next) {
const BlogPost = mongoose.model('blogPost');
BlogPost.remove({ _id: { $in: this.blogPosts } })
.then(() => next()); });
we didn't require the subdocument's related Model in current Model file to prevent
cyclic requires, instead we load it inside a callback function that in theory will
be called after the app first loaded.
'this' refer to instance of the Model, $in emulating loops but out of our server.
all different Models can have their own middlewares.
next() indicate that current middleware is finished executing and call for next
one if there's one otherwise run the event (similar for .post())

describe('Middleware', () => {
beforeEach(done => { ... Promise.all([joe.save(), blogPost.save()]) then done})
it('clining up dangling data on remove', done => {
joe.remove().then(() => BlogPost.count())
.then(count => { assert(count === 0); done(); }) }); });

we use skip() & limit() when we want to query a particular number of records from
a collection strating from a particular index:

Promise.all([alex.save(), joe.save(), maria.save(), zach.save()]).then(done(););


it('can skip and limit the result set', (done) => {
User.find({}).sort({ name: 1 }).skip(1).limit(2).then(users => {
assert(users.length === 2); assert(users[0].name === 'Joe');
assert(users[1].name === 'Maria'); done(); } }); });

==> since its not guaranteed that index of records follow the order which they get
odered to be saved by the server, we need to sort all of the records before using
skip() function, its object parameter has the field for records to be sorted by &
its value describe the way (ascending > 0 vs descending < 0) its gonna be sorted

... = new Schema({ date: Date, retired: Boolean });

better use Number type for money units for calculation purpose

TODO: /** general Description of operation to be done


* @param {type} x - description of parameter x
* @return {type} return description
*/

module.exports = _id => C.findById(_id); // return query that return a record

const minQuery = C.find({}).sort({x: 1}).limit(1).then(results => results[0].x)


return Promise.all([minQuery, maxQuery]).then(res => ({min: res[0], max: res[1]}))

module.exports = (criteria, sortProperty, offset = 0, limit = 0) => {


const query = C.find(buikdQuery(criteria)).sort({ [sortProperty]: 1 })
.skip(offset).limit(limit);
return Promise.all([query, ... ]).then(results =>
return { all: results[0], count: result[1], offset, limit }; ); }

const buildQuery = criteria => {


if(criteria.name) { // after adding index*
query.$text = { $search: criteria.name} }
if (criteria.age) {
query.age = { $gte: criteria.age.min, $gte: criteria.age.max } }
return query; }

* Index is a system that Mongo uses to make very efficient queries whenever we're
looking for dara (by default an index is automatically created for any new added
collection thus findById() will run faster)

all shell commands are listed at the end of this file (including indexes)
db.artists.createIndex({ name: "text" })
it works when searching an entire word as part of text but not with a part of word

since MongoDB 4.4 version it is possible to create a compound index:


db.products.createIndex( { "item": 1, "stock": 1 } )
==> docs.mongodb.com/manual/core/index-compound/

before updateOne() & updateMany() we used to add extra option to tell Mongo that
we need to update multiple records:
(_ids) => C.update({ _id: { $in: _ids } }, { retired: true }, { multi: true } )};

cannot use count() before skip() & limit() (for pagination), so we 2 diff. queries
==> return Promise.all([query, C.find(buikdQuery(criteria)).count()]).then( ...

# MongoDB Shell

## Show All Databases

```
show dbs
```

## Show Current Database

```
db
```

## Create Or Switch Database

```
use acme
```

## Drop

```
db.dropDatabase()
```
## Create Collection

```
db.createCollection('posts')
```

## Show Collections

```
show collections
```

## Insert Row

```
db.posts.insert({
title: 'Post One',
body: 'Body of post one',
category: 'News',
tags: ['news', 'events'],
user: {
name: 'John Doe',
status: 'author'
},
date: Date()
})
```

## Insert Multiple Rows

```
db.posts.insertMany([
{
title: 'Post Two',
body: 'Body of post two',
category: 'Technology',
date: Date()
},
{
title: 'Post Three',
body: 'Body of post three',
category: 'News',
date: Date()
},
{
title: 'Post Four',
body: 'Body of post three',
category: 'Entertainment',
date: Date()
}
])
```

## Get All Rows

```
db.posts.find()
```
## Get All Rows Formatted

```
db.posts.find().pretty()
```

## Find Rows

```
db.posts.find({ category: 'News' })
```

## Sort Rows

```
# asc
db.posts.find().sort({ title: 1 }).pretty()
# desc
db.posts.find().sort({ title: -1 }).pretty()
```

## Count Rows

```
db.posts.find().count()
db.posts.find({ category: 'news' }).count()
```

## Limit Rows

```
db.posts.find().limit(2).pretty()
```

## Chaining

```
db.posts.find().limit(2).sort({ title: 1 }).pretty()
```

## Foreach

```
db.posts.find().forEach(function(doc) {
print("Blog Post: " + doc.title)
})
```

## Find One Row

```
db.posts.findOne({ category: 'News' })
```

## Find Specific Fields

```
db.posts.find({ title: 'Post One' }, {
title: 1,
author: 1
})
```

## Update Row
#upsert option: if doc not fount, create it
```
db.posts.update({ title: 'Post Two' },
{
title: 'Post Two',
body: 'New body for post 2',
date: Date()
},
{
upsert: true
})
```

## Update Specific Field

```
db.posts.update({ title: 'Post Two' },
{
$set: {
body: 'Body for post 2',
category: 'Technology'
}
})
```

## Increment Field (\$inc)

```
db.posts.update({ title: 'Post Two' },
{
$inc: {
likes: 5
}
})
```

## Rename Field

```
db.posts.update({ title: 'Post Two' },
{
$rename: {
likes: 'views'
}
})
```

## Delete Row

```
db.posts.remove({ title: 'Post Four' })
```
## Sub-Documents

```
db.posts.update({ title: 'Post One' },
{
$set: {
comments: [
{
body: 'Comment One',
user: 'Mary Williams',
date: Date()
},
{
body: 'Comment Two',
user: 'Harry White',
date: Date()
}
]
}
})
```

## Find By Element in Array (\$elemMatch)

```
db.posts.find({
comments: {
$elemMatch: {
user: 'Mary Williams'
}
}
}
)
```

## Add Index

```
db.posts.createIndex({ title: 'text' })
```

## Text Search
output: Post One
```
db.posts.find({
$text: {
$search: "\"Post O\""
}
})
```

## Greater & Less Than

```
db.posts.find({ views: { $gt: 2 } })
db.posts.find({ views: { $gte: 7 } })
db.posts.find({ views: { $lt: 7 } })
db.posts.find({ views: { $lte: 7 } })
```

You might also like