Building the Mongoose Schema - AdarshMaurya/mongodb-and-mogoose-for-node-getting-started GitHub Wiki
Welcome to this module on building a Mongoose model. In this module, we will take what was learned about laying out a Mongoose schema in the previous module and build our first model based on that schema. We will then discuss Mongoose documents and subdocuments and the relationship between these documents and MongoDB documents within a collection.
1. Build a model from the mongoose schema
2. Relationship between Mongoose and MongoDB documents
3. Construct and save documents to MongoDB
In the demo, we will build out our standup meeting notes model, construct a document from that model, and save it to MongoDB.
To illustrate building out our first model, let's start with one of the simple schema examples we looked at earlier, the customerSchema.
const mongoose = require('mongoose');
const customerSchema = new mongoose.Schema({
name: String,
address: String,
city: String,
state: String,
country: String,
zipCode: String,
createdOn: Date,
isActive: Boolean
});
// Build a model from the customer schema
let Customer = mongoose.model('Customer', customerSchema);
// let Customer = mongoose.model('Customer', customerSchema, 'Clients');
// Add a custom property to the schema
customerSchema.add({ discountCode: String });
const DiscountedCust = mongoose.model('DiscountedCust', customerSchema);
To build a Mongoose model from the schema, which is named customerSchema in this example, we simply need to pass into the mongoose.model method the name of the model and the schema we wish to build this model from. A third optional argument not shown here would specify the MongoDB collection name that you want your document instance of this model to be saved in.
Since I have not supplied that here, Mongoose will provide a collection name from the model name given. Mongoose will attempt to pluralize the model name, so we may end up with Customers with an S rather than Customer for our collection name.
If that's not the desired result or if the collection name already exists and perhaps you need to match that name, simply provide that third collection name argument, and you should be good to go.
Keep in mind that you can build out any number of models against the schema definition, and as shown here, we can append on additional key-value pairs to our schema to customize it even further. Here, we've taken our base schema and added on a discount code and then built a discounted customer model from that appended schema definition.
// standup.js model file
const mongoose = require('mongoose');
const standupSchema = new mongoose.Schema({
teamMemberId: mongoose.Schema.Types.ObjectId
teamMember: String,
project: String,
workYesterday: String,
workToday: String,
impediment: String,
createdOn: Date
});
// Expose (export) the model now
module.exports = mongoose.model('Standup', standupSchema)
Now let's take a look at where we left things off with our standup notes schema and what it would take to export a model from our standup.js file. As with the previous example, we're simply going to pass into the mongoose.model method two arguments. The first being what we want to name our model, standup in this case, and the second, the previously defined schema, standupSchema. The difference here is that we are not assigning this model to a variable, but rather exporting it, or if you will, exposing it or making it available to other files that may require or include this module. We'll see that in action here shortly in the demo.
As mentioned in the introduction, we're now going to discuss Mongoose documents and how they are constructed and the relationship to MongoDB documents.
Mongoose Document ======> MongoDB Document
A Mongoose document is a direct representation of the document saved in a MongoDB collection. That's a good thing for us actually because it eliminates the need for some type of object relational mapping between mismatched systems.
ODM(Object Document Modelling (Mapper))
Mongoose is really more of an object document modeling tool than a mapper. We've already learned that everything in Mongoose starts with a schema and that a model is built, or maybe we can say compiled, for my schema.
const personSchema = new mongoose.Schema({
firstName: String,
lastName: String
});
const Person = mongoose.model('Person', personSchema);
const zevia = new Person({
firstName: 'Zevia',
lastName: 'Cola'
});
So now let's talk in a little more detail about documents. A document in Mongoose is simply an instance of our model. In the code example shown here, we have a really simple schema named personSchema with first and lastName properties. Next, we build a Mongoose model from the personSchema, which is shown here as the second argument to the model method. Again, the first argument is simply the name we wish to give our model. We've seen this much in the previous example, so let's continue on with the code example. Here, we've created a single instance of our model and named this new document zevia and then supplied values to the firstName and lastName properties.
Save
Schema ===> Model ===> Document <=====> Database
Retrieve
Later, we'll see how to save and retrieve documents such as this to and from MongoDB through Mongoose.
Now let's talk about Mongoose subdocuments. This would be a good time to revisit the quiz schema example we looked at earlier in the course.
Now that you've learned about building a Mongoose model from the schema and that documents and subdocuments are instances of the model, it's now time to open Visual Studio Code back up and develop the code to save a Mongoose document to MongoDB.
- We'll start by building each model from its respective schema.
- Once that is done, we'll develop the code used to save the Mongoose document to MongoDB. This will be done in our Express API, and we'll test that using Postman.
1. Building a Model from a Schema
2. Documents and Sub-documents
3. Build the Models
- Standup
- Project
- TeamMember
4. Save Mongoose documents to MongoDB
- API routes
Let's get started on that now. Back in Visual Studio Code, and to start, we'll open the standup.js file. All we need to do to obtain a Mongoose model from the schema is to add this line of code, model.exports = mongoose.model, and the first property here is the name of the model, we'll name this one Standup, and the second property is where we pass in the schema this model is based off of, in this case, standupSchema as defined above.
const mongoose = require('mongoose')
const standupSchema = new mongoose.Schema({
teamMember: { type: String },
project: { type: String },
workYesterday: { type: String },
workToday: { type: String },
impedimentS: { type: String },
createdOn: { type: Date, default: Date.now }
})
module.exports = mongoose.model('Standup', standupSchema)
Save this file, and we'll do the same thing for the two remaining files. Project.js, adding in module.exports = mongoose.model, name this one project, and pass in projectSchema.
// Project Model
const mongoose = require('mongoose')
const projectSchema = new mongoose.Schema({
name: { type: String },
description: { type: String },
isActive: { type: Boolean, default: true }
})
module.exports = mongoose.model('Project', projectSchema)
And teamMember.js also adding in module.exports = mongoose.model with a name of TeamMember and passing in teamMemberSchema.
// TeamMember
const mongoose = require('mongoose')
const teamMemberSchema = new mongoose.Schema({
name: { type: String }
})
module.exports = mongoose.model('TeamMember', teamMemberSchema)
That was pretty easy, and we now have Mongoose models available for us to use, but where will we use these models at? Let's go back to the API routes we stubbed in earlier in the course and develop each of those out a little more now.
Starting with the standup route, we only have the GET route stubbed in right now, but since we're wanting to save a Mongoose document, we'll need a POST route here now. Router.post with the path of /standup and a callback function with two properties, request and response. Now on the body of this function is where we start to make use of the Mongoose model we just finished. Let note = new Standup and pass in the request body, which will be a JSON object.
const Standup = require('../../models/standup')
module.exports = function (router) {
// Get: the 12 newest stand-up meeting notes
router.get('/standup', function (req, res) {
})
// POST: Get new meeting note document
router.post('/standup', function (req, res) {
let note = new Standup(req.body)
note.save(function (err, note) {
if (err) {
return res.status(400).json(err)
}
res.status(200).json(note)
})
})
}
We need to go back to the top of this file and add a reference to this standup model, const Standup = require and move back a couple folders, models/standup. Back in the callback function, note here is the Mongoose document instance of the Standup model we just required, and as such, we now have access to the model's save method, note.save with a callback function where the first property is any errors that may be returned and the second the note document that was saved. We can test to see if there were any errors returned, and if so, notify the consumer of this API endpoint, returning a status of 400 and the err object as json otherwise we're good; and therefore, let's return a status of 200 and the actual note document as json.
const Project = require('../../models/project')
module.exports = function (router) {
// Get: List of active projects
router.get('/projects', function (req, res) {
})
// POST: Get new project...
router.post('/projects', function (req, res) {
let project = new Project(req.body)
project.save(function (err, project) {
if (err) {
return res.status(400).json(err)
}
res.status(200).json(project)
})
})
}
Just to save a little time, I'll paste in code for projects and team.
const TeamMembers = require('../../models/teamMember')
module.exports = function (router) {
// Get: List of Team Members
router.get('/team', function (req, res) {
})
// POST: Create TeamMembers...
router.post('/team', function (req, res) {
let member = new TeamMembers(req.body)
member.save(function (err, member) {
if (err) {
return res.status(400).json(err)
}
res.status(200).json(member)
})
})
}
The code is nearly identical to what we just looked at with the standup PUT endpoint, each requiring the respective model file and newing up an instance of the document from that model and then saving the JSON passed into the request body.
Earlier in the course when we started developing this API server, we only asked the Express application to listen on the configured port, but so far, we haven't connected to MongoDB. It's time to do that now, or we won't get too far saving documents to MongoDB through Mongoose. We'll make those changes in the app.js file.
const express = require('express')
const app = express()
const api = require('./api')
const morgan = require('morgan') // logger
const bodyParser = require('body-parser')
const cors = require('cors')
app.set('port', (process.env.PORT || 8081))
app.use(bodyParser.json)
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cors())
app.use('/api', api)
app.use(express.static('static'))
app.use(morgan('dev'))
app.use(function (req, res) {
const err = new Error('Not Found')
err.status = 404
res.json(err)
})
// Add MongoDB connection in later...first just run app.listen (below)
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/virtualstandups', { userNewUrlParser: true })
const db = mongoose.connection
db.on('error', console.error.bind(console, 'connection error:'))
db.once('open', function () {
app.listen(app.get('port'), function () {
console.log('API Server Listening on port ' + app.get('port') + '!')
})
})
First off, we need to bring in Mongoose, so const mongoose = require mongoose. Next, let's connect to Mongoose using the localhost connection string information as shown here. This information will need to change when we migrate to the cloud, by the way. Next is a reference to the mongoose.connection. It would be helpful to see if there were any errors upon connecting to MongoDB through Mongoose, and that's what this line does for us. Now once the database is open, in this callback function, we will logout to the console that we're connected to MongoDB, and then as before, as the Express app to start listening for the request on the port configured above.
Last login: Thu Dec 20 04:50:48 on ttys000
Adarsh:mongodb-osx-x86_64-4.0.4 adarshmaurya$
Adarsh:mongodb-osx-x86_64-4.0.4 adarshmaurya$ mongod --dbpath ./data/db
2018-12-20T21:33:12.823+0530 I CONTROL [main] Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify --sslDisabledProtocols 'none'
2018-12-20T21:33:12.859+0530 I CONTROL [initandlisten] MongoDB starting : pid=58797 port=27017 dbpath=./data/db 64-bit host=Adarsh.local
2018-12-20T21:33:12.859+0530 I CONTROL [initandlisten] db version v4.0.4
2018-12-20T21:33:12.859+0530 I CONTROL [initandlisten] git version: f288a3bdf201007f3693c58e140056adf8b04839
2018-12-20T21:33:12.859+0530 I CONTROL [initandlisten] allocator: system
2018-12-20T21:33:12.859+0530 I CONTROL [initandlisten] modules: none
2018-12-20T21:33:12.859+0530 I CONTROL [initandlisten] build environment:
2018-12-20T21:33:12.859+0530 I CONTROL [initandlisten] distarch: x86_64
2018-12-20T21:33:12.860+0530 I CONTROL [initandlisten] target_arch: x86_64
2018-12-20T21:33:12.860+0530 I CONTROL [initandlisten] options: { storage: { dbPath: "./data/db" } }
2018-12-20T21:33:12.861+0530 I STORAGE [initandlisten] Detected data files in ./data/db created by the 'wiredTiger' storage engine, so setting the active storage engine to 'wiredTiger'.
2018-12-20T21:33:12.862+0530 I STORAGE [initandlisten] wiredtiger_open config: create,cache_size=7680M,session_max=20000,eviction=(threads_min=4,threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000),statistics_log=(wait=0),verbose=(recovery_progress),
2018-12-20T21:33:13.444+0530 I STORAGE [initandlisten] WiredTiger message [1545321793:444575][58797:0x1124385c0], txn-recover: Main recovery loop: starting at 2/55424 to 3/256
2018-12-20T21:33:13.520+0530 I STORAGE [initandlisten] WiredTiger message [1545321793:520475][58797:0x1124385c0], txn-recover: Recovering log 2 through 3
2018-12-20T21:33:13.569+0530 I STORAGE [initandlisten] WiredTiger message [1545321793:569763][58797:0x1124385c0], txn-recover: Recovering log 3 through 3
2018-12-20T21:33:13.608+0530 I STORAGE [initandlisten] WiredTiger message [1545321793:608587][58797:0x1124385c0], txn-recover: Set global recovery timestamp: 0
2018-12-20T21:33:13.711+0530 I RECOVERY [initandlisten] WiredTiger recoveryTimestamp. Ts: Timestamp(0, 0)
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten]
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database.
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted.
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten]
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** WARNING: This server is bound to localhost.
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** Remote systems will be unable to connect to this server.
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** Start the server with --bind_ip <address> to specify which IP
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** addresses it should serve responses from, or with --bind_ip_all to
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** bind to all interfaces. If this behavior is desired, start the
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** server with --bind_ip 127.0.0.1 to disable this warning.
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten]
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten]
2018-12-20T21:33:13.787+0530 I CONTROL [initandlisten] ** WARNING: soft rlimits too low. Number of files is 256, should be at least 1000
2018-12-20T21:33:13.867+0530 I FTDC [initandlisten] Initializing full-time diagnostic data capture with directory './data/db/diagnostic.data'
2018-12-20T21:33:13.871+0530 I NETWORK [initandlisten] waiting for connections on port 27017
2018-12-20T21:33:32.236+0530 I NETWORK [listener] connection accepted from 127.0.0.1:58282 #1 (1 connection now open)
2018-12-20T21:33:32.236+0530 I NETWORK [listener] connection accepted from 127.0.0.1:58286 #2 (2 connections now open)
2018-12-20T21:33:32.237+0530 I NETWORK [conn1] Error receiving request from client: ProtocolError: Client sent an HTTP request over a native MongoDB connection. Ending connection from 127.0.0.1:58282 (connection id: 1)
2018-12-20T21:33:32.237+0530 I NETWORK [conn1] end connection 127.0.0.1:58282 (1 connection now open)
2018-12-20T21:33:32.456+0530 I NETWORK [conn2] Error receiving request from client: ProtocolError: Client sent an HTTP request over a native MongoDB connection. Ending connection from 127.0.0.1:58286 (connection id: 2)
2018-12-20T21:33:32.456+0530 I NETWORK [conn2] end connection 127.0.0.1:58286 (0 connections now open)
2018-12-20T21:37:37.202+0530 I NETWORK [listener] connection accepted from 127.0.0.1:58300 #3 (1 connection now open)
2018-12-20T21:37:37.202+0530 I NETWORK [conn3] received client metadata from 127.0.0.1:58300 conn3: { application: { name: "robo3t" }, driver: { name: "MongoDB Internal Client", version: "3.4.3-10-g865d2fb" }, os: { type: "Darwin", name: "Mac OS X", architecture: "x86_64", version: "18.2.0" } }
2018-12-20T21:37:37.241+0530 I NETWORK [listener] connection accepted from 127.0.0.1:58301 #4 (2 connections now open)
2018-12-20T21:37:37.241+0530 I NETWORK [conn4] received client metadata from 127.0.0.1:58301 conn4: { application: { name: "MongoDB Shell" }, driver: { name: "MongoDB Internal Client", version: "3.4.3-10-g865d2fb" }, os: { type: "Darwin", name: "Mac OS X", architecture: "x86_64", version: "18.2.0" } }
2018-12-20T21:37:41.925+0530 I NETWORK [listener] connection accepted from 127.0.0.1:58302 #5 (3 connections now open)
2018-12-20T21:37:41.925+0530 I NETWORK [conn5] received client metadata from 127.0.0.1:58302 conn5: { application: { name: "robo3t" }, driver: { name: "MongoDB Internal Client", version: "3.4.3-10-g865d2fb" }, os: { type: "Darwin", name: "Mac OS X", architecture: "x86_64", version: "18.2.0" } }
2018-12-20T21:37:41.957+0530 I NETWORK [listener] connection accepted from 127.0.0.1:58303 #6 (4 connections now open)
2018-12-20T21:37:41.958+0530 I NETWORK [conn6] received client metadata from 127.0.0.1:58303 conn6: { application: { name: "MongoDB Shell" }, driver: { name: "MongoDB Internal Client", version: "3.4.3-10-g865d2fb" }, os: { type: "Darwin", name: "Mac OS X", architecture: "x86_64", version: "18.2.0" } }
2018-12-20T21:46:27.868+0530 I NETWORK [listener] connection accepted from 127.0.0.1:58345 #7 (5 connections now open)
2018-12-20T21:46:27.872+0530 I NETWORK [conn7] received client metadata from 127.0.0.1:58345 conn7: { driver: { name: "nodejs", version: "3.1.10" }, os: { type: "Darwin", name: "darwin", architecture: "x64", version: "18.2.0" }, platform: "Node.js v11.5.0, LE, mongodb-core: 3.1.9" }
2018-12-20T21:47:19.818+0530 I NETWORK [conn7] end connection 127.0.0.1:58345 (4 connections now open)
2018-12-20T21:47:24.078+0530 I NETWORK [listener] connection accepted from 127.0.0.1:58351 #8 (5 connections now open)
2018-12-20T21:47:24.082+0530 I NETWORK [conn8] received client metadata from 127.0.0.1:58351 conn8: { driver: { name: "nodejs", version: "3.1.10" }, os: { type: "Darwin", name: "darwin", architecture: "x64", version: "18.2.0" }, platform: "Node.js v11.5.0, LE, mongodb-core: 3.1.9" }
With all of those changes saved now, let's move over to the terminal window. Make sure that your local instance of MongoDB is running as it is in this terminal tab now, make sure you're in the server folder, and then run this Express API. We do that by entering yarn start here, and after a moment, we can see that the server is running on localhost port 8081.
Adarsh:server adarshmaurya$ yarn start
yarn run v1.12.3
$ nodemon app.js
[nodemon] 1.18.9
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node app.js`
the options [userNewUrlParser] is not supported
(node:59099) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
API Server Listening on port 8081!
Now that the Express API server is running, we can test saving a document to it using Postman. Let's get that up and running now. Here in Postman, I have a test POST already set up in this Mongoose Course collection. Notice the URL is set to localhost port 8081/api/standup. On the Body tab here, notice the JSON object matches our Mongoose schema and therefore the model and documents built from that schema. I have some test data prefilled out here, and if we send this POST, we should see a status of 200 OK and the save document returned in the response body. To fully confirm this, let's open up Studio 3T and see if we can find this newly saved document. Once that opens and we get connected to local, we can open up the virtualstandups database and then the standups collection. This last entry should be the new one we just added via Postman, so we're good to go there.
- Build a model from the schema
- Model method arguments
- Name of the model
- Schema object
- Option argument - override collection
- Relationship between models and documents
- Quiz Example
- Documents and sub-documents
Time to query data (next chapter)
In this module, we looked at some examples of how to build a model from a previously defined schema and discussed the arguments we pass into the Mongoose model method, which were a string, which represents the name of the model, and the schema object used to build the model from. Also, there was a third optional argument, which allows you to specify and override the name of a MongoDB collection document instance will be saved to. Next, we examined the relationship between models and documents in Mongoose and how those documents relate to MongoDB collection documents. We then looked at a slightly more complex example of populating the example quiz document with subdocuments. In the demo, we picked up where we left off with the standupSchema by building a model from that schema and then creating a document instance from that model, testing that all out in Postman. In the next module, we will take things one step further and learn to query data from MongoDB using these same models. I'll see you in the next module.