Try   HackMD

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Mongoose | Schemas, Models & Documents

Learning Goals

After this lesson you will be able to:

  • Implement more complex schemas
  • Create, read, update and delete documents using Mongoose models
  • Manipulate with Mongoose documents

Schemas

In the previous learning unit, we created a Cat model like this:

Dans notre exemple précédent, nous définission le modèle de chat suivant :

mongoose.model('Cat', {
  name: String
})

Le 2e paramètre est ce que l'on appelle le schéma. Il nous permet de donner de la structure à nos documents et évite :

  • d'ajouter un champ non-défini dans le schéma
  • d'oublier un champ obligatoire (nous verrons comment)
  • d'utiliser le mauvais type pour un champ

The second argument that we pass is the object that will have all the fields that the documents built based on our Cat model will have. This example is a simple version of a Schema.

In short, a schema gives our database structure. It helps us ensure consistency in our database, and we are less likely to:

  • Add fields that shouldn’t exist (because you did define it in your schema).
  • Forget fields that are required.
  • Use the wrong type in a field (i.e., a number inside a date field or a boolean in a number field).

Let’s take a look at how to define a Schema by adding some fields to our Cat model. Let's create folder models and add a file Cat.js inside models folder. Let's add this code snippet into it:

Afin de créer des schémas plus avancés, nous allons avoir recours à la class mongoose.Schema :

// models/Cat.js

const mongoose = require('mongoose');
const Schema   = mongoose.Schema;

const catSchema = new Schema({
  name : String,
  color: String,
  age  : Number
});

const Cat = mongoose.model('Cat', catSchema);
module.exports = Cat;

Notice how we specify our Schema, and then pass it in as the second argument to mongoose.model. We will see this in more depth later on.

To have Cat model available in some other file (part of the app), you need to navigate to this file from the place where you are currently. Example:

const Cat = require('./models/Cat.js');

Schema Data Types

Les différents types possibles :

Our schema is just an object with keys for each of the fields’ names and values for each fields’ types. There are many schema types available in Mongoose, but here’s a quick list of common types:

  • String (ex: Ironhack)
  • Number (ex: 42)
  • Date (ex: Date('2020-12-25'))
  • Boolean (ex: true)
  • Schema.Types.ObjectId: to store ids of other collections (ex: Ironhack) - this one we will use a lot, so keep it in your mind
  • Schema.Types.Mixed: to store anything
  • Array or []: to store an array of anything (ex: ['foo', 42])
    • [String]: to store an array of strings (ex: ['foo', 'bar'])
    • [Number]: to store an array of numbers (ex: [42, -6])

Schema default value

On peut également renseigner une valeur par défaut :

Often we want a default value for some field otherwise, it will be saved empty. For instance, when a user registers on a website and doesn't provide an image, you can set the image to some default one so the user will get the default avatar image. You can accomplish this using the keyword default.

const userSchema = new Schema({
  email    : { type: String},
  username : { type: String},
  avatarUrl: { type: String, default: 'images/default-avatar.png' }
});

Notez que la valeur est maintenant un objet !

Schema Data Types Validation

Les schémas nous permettent de faire de la validation :

Besides specifying the type of the fields or its default value, we can set more detailed validation by defining more properties:

Field property Possible values Description
type String, Number, Sets a type for the field
default Anything Sets a default value
required true Adds a required validator
unique true Declares an unique index
enum An array Adds an enum validator
min A number Sets a minimum number validator
max A number Sets a maximum number validator
minlength A number Sets a minimum length validator
maxlength A number Sets a maximum length validator
trim true Adds a trim setter
lowercase true Adds an lowercase setter
match A regex Sets a regex validator
validate An object Adds a custom validator (see next part)
set A function Adds a custom setter (see next part)

Example

Un ex concret :

const catSchema = new Schema({
  name: { type: String, required: true },
  age: { type: Number, min: 0, max: 30 },
  color: { type: String, enum: ['white', 'black', 'brown'] },
  avatarUrl: { type: String, default: 'images/default-avatar.png' },
  location: {
    address: String,
    city: String
  },
  countryCode: { type: String, match: /^[A-Z]{2}$/ },
  created: { 
    type: Date,
    default: Date.now()
  }
});

Schema custom validation and custom setter

If you don't have the right validator, Mongoose gives you the opportunity to write your own. For that, you will need to add validate and precise a validator function and an error message.

Et même nos propres validateurs :

// Example of custom validation

const userSchema = new Schema({
  linkedinProfile: {
    type: String,
    validate: {
      validator: (text) => {
        return text.indexOf('https://www.linkedin.com/') === 0;
      },
      message: "linkedinProfile must start with 'https://www.linkedin.com/'"
    }
  }
};

Mongoose models

Create

Comme new + save()

First, let's see how we can insert (create) some documents. If you take a look at the Mongoose documentation, you will see that you can use the function Model.create().
Let's see how that looks if we want to create a new user based on the already defined User model. For this demo, we'll create User.js inside models folder:

// models/User.js

const mongoose = require('mongoose');
const Schema   = mongoose.Schema;

const userSchema = new Schema({
  name: { type: String },
  password: { type: String },
  job: { type: String }
}, {
  timestamps: true
});

const User = mongoose.model('User', userSchema);
module.exports = User;

In the following example, we use the User model to create a new document:

User.create({ name: 'Alice', password:"ironhack2018", job: 'Architect' }, function (err, user) {
  if (err) {
      console.log('An error happened:', err);
  } else {
      console.log('The user is saved and its value is: ', user);
  }
});

// The same code as above but with a Promise version
User.create({ name: 'Alice', password:"ironhack2018", job: 'Architect' })
  .then(user => { console.log('The user is saved and its value is: ', user) })
  .catch(err => { console.log('An error happened:', err) });

If you want to save several documents in your database, you can either use create or insertMany with an array as a first parameter.

Avantage : insertMany

Read

Les paramètres de .find() :

To retrieve multiple documents from a database, we can use Model.find() and these are some of the specifics of its syntax:

  • The first parameter is the query represented by an object. If you don't put it you will find all the documents of your collection;
  • The second parameter is optional and is the projection represented by an object or a string with all the fields to display;
  • The third parameter is optional and is the options, represented by an object;
  • The last parameter is the callback function executed when the query is finished. If you don't put any function as a parameter, it will return a Promise.

At this point, for us the most important ones are the first and the last.

// Please keep in mind that the point here is to understand what is the order 
// and posiible structure of the arguments of .find() Mongoose method

let callback = (err, users) => {});

// Find all users and execute the callback
User.find({}, callback);

// Find all users where (the name is 'Alice' and the age is >= 18) and execute the callback
User.find({ name: 'Alice', age: { $gte: 18 }}, callback);

// Find all users where the name is 'Alice' and only selecting the "name" and "age" fields, and afterwards, execute the callback
User.find({ name: 'Alice' }, 'name age', callback);

// Find all users and sort them by name descending
User.find({}, null, {sort: { name: -1 }}, callback);

// Find all users where the name contains 'alice' (insensitive case) and execute the callback
User.find({ name: /alice/i}, callback);

// Promise version
User.find({ name: 'Alice' })
  .then(successCallback)
  .catch(errorCallback);

In the case we want to retrieve only one specific document, we can either use findOne or findById.

User.findOne({ name: "Alice" })
  .then(successCallback)
  .catch(errorCallback);

User.findById("someMongoIdGoesHere129")
  .then(successCallback)
  .catch(errorCallback);

Update

To update a field, you can either use updateMany, updateOne or findByIdAndUpdate. In each case, you need to define first the condition (a query or an id) and then the update, like in the next example. We decided here to only use the Promise syntax.

// For all users that as "@ironhack\.com" in its email, change the company to "Ironhack"
User.updateMany({ email: /@ironhack\.com/}, { company: "Ironhack" })
  .then(successCallback)
  .catch(errorCallback);

// For the first "Alice" found, change the company to "Ironhack"
User.updateOne({ name: "Alice"}, { company: "Ironhack" })
  .then(successCallback)
  .catch(errorCallback);

// For the user with _id "5a3a7ecbc6ca8b9ce68bd41b", increment the salary by 4200
User.findByIdAndUpdate("5a3a7ecbc6ca8b9ce68bd41b", { $inc: {salary: 4200} })
  .then(successCallback)
  .catch(errorCallback);

Delete

To delete a document, you can either use deleteMany, deleteOne or findByIdAndRemove, like in the next example.

// Delete all the users that have "@ironhack.com" in their email
User.deleteMany({ email: /@ironhack\.com/})
  .then(successCallback)
  .catch(errorCallback);

// Delete the first "Alice" found
User.deleteOne({ name: "Alice"})
  .then(successCallback)
  .catch(errorCallback);

// Delete the user with _id "5a3a7ecbc6ca8b9ce68bd41b"
User.findByIdAndRemove("5a3a7ecbc6ca8b9ce68bd41b")
  .then(successCallback)
  .catch(errorCallback);

Utils

Mongoose gives also some utils methods. The one you will probably use the most is the countDocuments method, which lets you count the number of documents that match a specific condition.

User.countDocuments({ type: 'student' })
  .then(count => { console.log(`There are ${count} students`) })
  .catch(err => { console.log(err) });

Mongoose Documents

Mongoose gives another way of manipulating data using the documents. We can use this approach with:

  • Mongoose models, which are constructor classes we can use to create new instances of the model;
  • Some methods such as Model.find() in its all forms (.findOne(), .findById()).

On these documents, we can use the method .save() to save a document in the database. We can use .save() for creating a new document and updating one.

// -------------------- CREATE --------------------
// Based on already defined User model, create a user Alice ('Architect')
var myUser = new User({ 
    name: 'Alice', 
    password: 'ironhack2018',
    job: 'Architect'
});

myUser.save() // Create a new user and return a promise
  .then(user => { console.log('The user was created') })
  .catch(err => { console.log('An error occured', err) });

// -------------------- UPDATE --------------------
// Find the user with id '5a3a7ecbc6ca8b9ce68bd41b' and update its job and salary
User.findById('5a3a7ecbc6ca8b9ce68bd41b')
  .then(user => {
    user.job = 'Developer';
    user.salary += 10000;
    return user.save(); // Update the user '5a3a7ecbc6ca8b9ce68bd41b' and return a promise
  })
  .then(user => { console.log('The user was updated: ' + user )})
  .catch(err => { console.log('An error occured:', err) });

Mongoose methods cheat sheet

Cheatsheet

Model method Description
create Create document
insertMany Create many documents
find Finds documents
findOne Finds one document
findById Finds a single document by its _id field
updateOne Updates one document
updateMany Updates many documents
findByIdAndUpdate Updates a single document based on its _id field
deleteOne Deletes one document
deleteMany Deletes many documents

findByIdAndRemove | Removes a single document based on its _id field
countDocuments | Counts number of matching documents in a database collection

Document method Description
save Saves this document
toObject Converts this document into a plain javascript object
toString Helper for console.log

Summary

In this lesson, we learned how to build more complex schemas, with different types, validators and setters.

We have also seen Mongoose methods to perform all sorts of operation in the database and to discover new interesting things about them you can take a look into the Mongoose API documentation.

Extra Resources