-
Notifications
You must be signed in to change notification settings - Fork 0
Database (R8, R7)
| Install | MongoDB | Mongoose | Configuration | Models | Mongoose Schemas |
|---|
As far as installation is concerned, in this section there are very few things to install to get the database up and running.
- install MongoDB
- install MongoDB Compass
MongoDB Compass is the GUI for MongoDB. Compass allows you to analyze and understand the contents of your data without formal knowledge of MongoDB query syntax. In addition to exploring your data in a visual environment, you can also use Compass to optimize query performance, manage indexes, and implement document validation.
For deployment purposes, we set up a cloud database with MongoDB Atlas. MongoDB Atlas is the global cloud database service for modern applications. It provides an easy way to host and manage your data in the cloud. If you don't feel like creating a MongoDB Atlas account, we can give you read access to our cloud database already in place.
(R7)
MongoDB is an open-source document database that provides high performance, high availability, and automatic scaling.
In simple words, you can say that - Mongo DB is a document-oriented database. It is an open source product, developed and supported by a company named 10gen.
We chose to work with MongoDB over relational database like SQL or PostgreSQL because of the relationship between our entities. We only have a many to one relationship in our database and we thought using a relational database would be over-engineering the application.
We could have connected our application directly to MongoDB and have the two interact by using the native driver. Although the native MongoDB driver is powerful, it isn’t particularly easy to work with. It also doesn’t offer a built-in way of defining and maintaining data structures. Mongoose exposes most of the functionality of the native driver, but in a more convenient way, designed to fit into the flow of application development.
Where Mongoose really excels is in the way it enables you to define data structures and models, maintain them, and use them to interact with your database, all from the comfort of your application code. As part of this approach, Mongoose includes the ability to add validation to your data definitions, meaning that you don’t have to write validation code in every place in your application where you send data back to the database.
MongoDB talks only to Mongoose, and Mongoose in turn talks to Node and Express. Angular won’t talk directly to MongoDB or Mongoose only to the Express application. You should already have MongoDB installed on your system, but not Mongoose. Mongoose isn’t installed globally but is instead added directly to your application.
Mongoose is available as an npm module. The easiest way to install an npm module is through the command line. You can install Mongoose and add it to your list of dependencies in package.json with one command. Head over to terminal, and make sure that the prompt is at the root folder of the application, where the package.json file is. Then run the following command:
npm install mongooseMongoose opens a pool of five reusable connections when it connects to a MongoDB database. This pool of connections is shared among all requests. Five is the default number; you can increase or decrease the connection options if you need to but we went to the default number.
Opening and closing connections to databases can take a little bit of time, especially if your database is on a separate server or service. It’s best to run these operations only when you need to. The best practice is to open the connection when your application starts and to leave it open until your application restarts or shuts down. This approach is the one we chose to take.
This database file is can be found here.
Creating a Mongoose connection can be as simple as declaring the URI for your data-base and passing it to Mongoose’s connect method. A database URI is a string following this construct:
The username, password, and port are optional. On our local machine, our database URI is simple : mongodb://localhost:27017/web_app. The following code snippet is shows how we created a database connection:
const mongoose = require('mongoose');
const {success, info, error, debug} = require('consola');
const host = process.env.DB_HOST;
const dbURL = `mongodb://${host}/web_app`;
//connect with the database
mongoose.connect(dbURL,
{useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true,
useFindAndModify : false})
.then(() => {
success({message: `Database connected successfully to ${dbURL}`,
badge : true});
}).catch(err => {
error({message: `Mongoose connection error: ${err}`, badge : true});
});The mongoose.connect() tells Mongoose to use its new internal URLparser, which avoids deprecation warnings due to MongoDB deprecating but leaves available the older connection string parser. The useUnifiedTopology option removes support for several connection options that are no longer relevant with the new topology engine.
So how do we know that our connection is working correctly? The answer lies in connection events.
Mongoose publishes events based on the status of the connection, and these events are easy to hook into so that we can see what’s going on. We used events to see when the connection is made, when there’s an error, and when the connection is disconnected. When any one of these events occurs, we log a message to the console. The following code snippet shows the required code:
// Monitors for a successful connection through Mongoose
mongoose.connection.on('connected', () => {
console.log(`Mongoose connected to ${dbURI}`);
});
// Checks for a connection error
mongoose.connection.on('error', err => {
console.log('Mongoose connection error:', err);
});
// Checks for a disconnection event
mongoose.connection.on('disconnected', () => {
console.log('Mongoose disconnected');
});Closing the Mongoose connection when the application stops is as much a part of best practices as opening the connection when it starts. The connection has two ends: one in your application and one in MongoDB. MongoDB needs to know when you want to close the connection so that it doesn’t keep redundant connections open.
To monitor when the application stops, we needed to listen to the Node.js process for an event called SIGINT
Since we use nodemon to automatically restart the application, we also needed to listen to a second event on the Node process: SIGUSR2. Heroku uses a different event, SIGTERM, so we needed to listen for that event as well.
Capturing these events prevents the default behavior from happening. We need to make sure that we manually restart the behavior required after closing the Mongoose connection.
To do this, we needed three event listeners and one function to close the database connection. Closing the database is an asynchronous activity, so we needed to pass through whatever function is required to restart or end the Node process as a callback. While we were at it, we chose to display a message to the console confirming that the connection is closed and the reason why. We wrapped all this in a function called gracefulShutdown in the database config file:
const gracefulShutdown = (msg, callback) => {
mongoose.connection.close(() => {
console.log(`Mongoose disconnected through ${msg}`);
callback();
});
};This function is called when the application terminates or when nodemon restarts it. The following code snippet shows the two event listeners we needed to add to the database config file for this to happen:
process.once('SIGUSR2', () => {
gracefulShutdown('nodemon restart', () => {
process.kill(process.pid, 'SIGUSR2');
});
});
process.on('SIGINT', () => {
gracefulShutdown('app termination', () => {
process.exit(0);
});
});
process.on('SIGTERM', () => {
gracefulShutdown('Heroku app shutdown', () => {
process.exit(0);
});
});Now when the application terminates, it gracefully closes the Mongoose connection before it ends. Similarly, when nodemon restarts the application due to changes in the source files, the application closes the current Mongoose connection first. The nodemon listener is using process.once as opposed to process.on, as we want to listen for the SIGUSR2 event only once. nodemon also listens for the same event, and we didn't want to capture it each time, preventing nodemon from working.
That’s quite a lot of stuff in to the database config file, so take a moment to recap. So far, we’ve :
- Defined a database connection strings
- Opened a Mongoose connection at application startup
- Monitored the Mongoose connection events
- Monitored some Node process events so that you can close the Mongoose connection when the application ends
(R8)
All models in the database can be found in the API documentation.
When we talk about modeling data, we’re describing how we want the data to be structured. In our application, we could have created and managed the definitions manually and did the heavy lifting ourselves, but we used Mongoose and let it do the hard work.
Mongoose was built specifically as a MongoDB Object Document Modeler (ODM) for Node applications. One key principle is that you can manage your data model from within your application. You don’t have to mess around directly with databases or external frameworks or relational mappers; you can define your data model in the comfort of your application.
Let's get some naming conventions out of the way:
- In MongoDB, each entry in a database is called a document.
- In MongoDB, a group of documents is called a collection. (Think table if you’re used to relational databases.)
- In Mongoose, the definition of a document is called a schema.
- Each individual data entity defined in a schema is called a path.
One final definition is for models. A model is the compiled version of a schema. All data interactions using Mongoose go through the model.
In our application we have 3 schemas in other words, we have 3 entities :
- Question Entity
const questionSchema = new mongoose.Schema({
type: {
type: String,
required: true,
enum: ["multiple", "boolean", "fill in"]
},
question: {
type: String,
required: true,
},
answers: [
{
option: {
type: String,
required: true
},
isCorrect: {
type: String,
required: true,
default: false
}
}
]
});
questionSchema.plugin(uniqueValidator);
module.exports = mongoose.model('Question', questionSchema, 'questions');- Review Entity
Reviews are sub-documents of the user document. Which means reviews have a many to one relationship with the user document.
const reviewSchema = new mongoose.Schema({
author: {
type: String,
required: true
},
rating: {
type: Number,
required: true,
min: 0,
max: 10
},
reviewText: {
type: String,
required: true
},
created: {
type: String
},
updated: {
type: String
}
});- User Entity
const userSchema = new mongoose.Schema({
username: {
type: String,
require: true,
unique: true
},
email: {
type: String,
required: true,
unique: true,
match: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
},
password: {
type: String,
require: true
},
score: {
type: Number,
default: 0,
min: 0,
max: 10
},
level: {
type: String,
default : "A1",
enum: ["A1", "A2", "B1", "B2", "C1", "C2"]
},
role: {
type: String,
default: 'student',
enum: ["student", "teacher", "admin"]
},
reviews: [reviewSchema]
});
userSchema.plugin(uniqueValidator);
module.exports = mongoose.model('User', userSchema);The application doesn’t interact with the schema directly when working with data; data interaction is done through models. In Mongoose, a model is a compiled version of the schema. When it’s compiled, a single instance of the model maps directly to a single document in your database. It’s through this direct one-to-one relationship that the model can create, read, save, and delete data.
Compiling a Mongoose model from a schema is a simple one-line task :
mongoose.model('User', userSchema);Autome Edwin | Daniel Olivier | Morgan Valentin