Εισαγωγή στο Mongoose για το MongoDB

Το Mongoose είναι μια βιβλιοθήκη αντικειμένων μοντελοποίησης δεδομένων (ODM) για MongoDB και Node.js. Διαχειρίζεται σχέσεις μεταξύ δεδομένων, παρέχει επικύρωση σχήματος και χρησιμοποιείται για τη μετάφραση μεταξύ αντικειμένων σε κώδικα και την αναπαράσταση αυτών των αντικειμένων στο MongoDB.

Το MongoDB είναι μια βάση δεδομένων εγγράφων NoSQL χωρίς σχήμα. Αυτό σημαίνει ότι μπορείτε να αποθηκεύσετε έγγραφα JSON σε αυτό και η δομή αυτών των εγγράφων μπορεί να διαφέρει καθώς δεν επιβάλλεται όπως οι βάσεις δεδομένων SQL. Αυτό είναι ένα από τα πλεονεκτήματα της χρήσης του NoSQL καθώς επιταχύνει την ανάπτυξη εφαρμογών και μειώνει την πολυπλοκότητα των εφαρμογών.

Ακολουθεί ένα παράδειγμα του τρόπου αποθήκευσης των δεδομένων στη βάση δεδομένων Mongo εναντίον SQL:

Ορολογίες

Συλλογές

Οι «Συλλογές» στο Mongo είναι ισοδύναμες με πίνακες σε σχεσιακές βάσεις δεδομένων. Μπορούν να κρατήσουν πολλά έγγραφα JSON.

Εγγραφα

Τα «Έγγραφα» είναι ισοδύναμα με εγγραφές ή σειρές δεδομένων σε SQL. Ενώ μια σειρά SQL μπορεί να αναφέρει δεδομένα σε άλλους πίνακες, τα έγγραφα Mongo συνήθως τα συνδυάζουν σε ένα έγγραφο.

Πεδία

Τα πεδία ή τα χαρακτηριστικά είναι παρόμοια με τις στήλες σε έναν πίνακα SQL.

Σχέδιο

Ενώ το Mongo δεν έχει σχήμα, το SQL ορίζει ένα σχήμα μέσω του ορισμού του πίνακα. Ένα «σχήμα» Mongoose είναι μια δομή δεδομένων εγγράφου (ή σχήμα του εγγράφου) που επιβάλλεται μέσω του επιπέδου εφαρμογής.

Μοντέλα

Τα «Μοντέλα» είναι κατασκευαστές υψηλότερης τάξης που λαμβάνουν ένα σχήμα και δημιουργούν μια παρουσία ενός εγγράφου ισοδύναμου με εγγραφές σε μια σχεσιακή βάση δεδομένων.

Ξεκινώντας

Εγκατάσταση Mongo

Πριν ξεκινήσουμε, ας ρυθμίσουμε το Mongo. Μπορείτε να επιλέξετε μία από τις ακόλουθες επιλογές (χρησιμοποιούμε την επιλογή # 1 για αυτό το άρθρο):

  1. Πραγματοποιήστε λήψη της κατάλληλης έκδοσης MongoDB για το λειτουργικό σας σύστημα από τον ιστότοπο MongoDB και ακολουθήστε τις οδηγίες εγκατάστασης
  2. Δημιουργήστε μια δωρεάν συνδρομή βάσης δεδομένων sandbox στο mLab
  3. Εγκαταστήστε το Mongo χρησιμοποιώντας το Docker εάν προτιμάτε να χρησιμοποιήσετε το docker

Ας περιηγηθούμε σε μερικά από τα βασικά στοιχεία του Mongoose εφαρμόζοντας ένα μοντέλο που αντιπροσωπεύει δεδομένα για ένα απλοποιημένο βιβλίο διευθύνσεων.

Χρησιμοποιώ το Visual Studio Code, Node 8.9 και NPM 5.6. Ενεργοποιήστε το αγαπημένο σας IDE, δημιουργήστε ένα κενό έργο και ας ξεκινήσουμε! Θα χρησιμοποιήσουμε την περιορισμένη σύνταξη ES6 στον κόμβο, οπότε δεν θα διαμορφώσουμε το Babel.

Εγκατάσταση NPM

Ας πάμε στο φάκελο του έργου και αρχικοποιήσουμε το έργο μας

npm init -y

Ας εγκαταστήσουμε το Mongoose και μια βιβλιοθήκη επικύρωσης με την ακόλουθη εντολή:

npm install mongoose validator

Η παραπάνω εντολή εγκατάστασης θα εγκαταστήσει την τελευταία έκδοση των βιβλιοθηκών. Η σύνταξη Mongoose σε αυτό το άρθρο αφορά συγκεκριμένα το Mongoose v5 και μετά.

Σύνδεση βάσης δεδομένων

Δημιουργήστε ένα αρχείο ./src/database.jsκάτω από τη ρίζα του έργου.

Στη συνέχεια, θα προσθέσουμε μια απλή κλάση με μια μέθοδο που συνδέεται με τη βάση δεδομένων.

Η συμβολοσειρά σύνδεσης θα διαφέρει ανάλογα με την εγκατάστασή σας.

let mongoose = require('mongoose'); const server = '127.0.0.1:27017'; // REPLACE WITH YOUR DB SERVER const database = 'fcc-Mail'; // REPLACE WITH YOUR DB NAME class Database { constructor() { this._connect() } _connect() { mongoose.connect(`mongodb://${server}/${database}`) .then(() => { console.log('Database connection successful') }) .catch(err => { console.error('Database connection error') }) } } module.exports = new Database()

ο require(‘mongoose’)Η παραπάνω κλήση επιστρέφει ένα αντικείμενο Singleton. Αυτό σημαίνει ότι την πρώτη φορά που καλείτε require(‘mongoose’), δημιουργεί μια παρουσία της τάξης Mongoose και την επιστρέφει. Σε επόμενες κλήσεις, θα επιστρέψει την ίδια παρουσία που δημιουργήθηκε και θα σας επιστραφεί την πρώτη φορά λόγω του τρόπου λειτουργίας της εισαγωγής / εξαγωγής λειτουργικής μονάδας στο ES6.

Ομοίως, μετατρέψαμε την κλάση βάσης δεδομένων σε module.exportsαπλή επιστροφή μιας παρουσίας της τάξης στη δήλωση επειδή χρειαζόμαστε μόνο μία σύνδεση με τη βάση δεδομένων.

Το ES6 καθιστά πολύ εύκολο για εμάς να δημιουργήσουμε ένα μοτίβο singleton (απλής παρουσίας) λόγω του τρόπου λειτουργίας του module loader αποθηκεύοντας προσωρινά την απόκριση ενός αρχείου που είχε εισαχθεί προηγουμένως

Mongoose Schema εναντίον Μοντέλου

Ένα μοντέλο Mongoose είναι ένα περιτύλιγμα στο σχήμα Mongoose. Ένα σχήμα Mongoose καθορίζει τη δομή του εγγράφου, τις προεπιλεγμένες τιμές, τους επικυρωτές κ.λπ., ενώ ένα μοντέλο Mongoose παρέχει μια διεπαφή στη βάση δεδομένων για τη δημιουργία, την αναζήτηση, την ενημέρωση, τη διαγραφή εγγραφών κ.λπ.

Η δημιουργία ενός μοντέλου Mongoose περιλαμβάνει κυρίως τρία μέρη:

1. Αναφορά Mongoose

let mongoose = require('mongoose')

This reference will be the same as the one that was returned when we connected to the database, which means the schema and model definitions will not need to explicitly connect to the database.

2. Defining the Schema

A schema defines document properties through an object where the key name corresponds to the property name in the collection.

let emailSchema = new mongoose.Schema({ email: String })

Here we define a property called email with a schema type String which maps to an internal validator that will be triggered when the model is saved to the database. It will fail if the data type of the value is not a string type.

The following Schema Types are permitted:

  • Array
  • Boolean
  • Buffer
  • Date
  • Mixed (A generic / flexible data type)
  • Number
  • ObjectId
  • String

Mixed and ObjectId are defined under require(‘mongoose’).Schema.Types.

3. Exporting a Model

We need to call the model constructor on the Mongoose instance and pass it the name of the collection and a reference to the schema definition.

module.exports = mongoose.model('Email', emailSchema)

Let’s combine the above code into ./src/models/email.jsto define the contents of a basic email model:

let mongoose = require('mongoose') let emailSchema = new mongoose.Schema({ email: String }) module.exports = mongoose.model('Email', emailSchema)

A schema definition should be simple, but its complexity is usually based on application requirements. Schemas can be reused and they can contain several child-schemas too. In the example above, the value of the email property is a simple value type. However, it can also be an object type with additional properties on it.

We can create an instance of the model we defined above and populate it using the following syntax:

let EmailModel = require('./email') let msg = new EmailModel({ email: '[email protected]' })

Let’s enhance the Email schema to make the email property a unique, required field and convert the value to lowercase before saving it. We can also add a validation function that will ensure that the value is a valid email address. We will reference and use the validator library installed earlier.

let mongoose = require('mongoose') let validator = require('validator') let emailSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true, lowercase: true, validate: (value) => { return validator.isEmail(value) } } }) module.exports = mongoose.model('Email', emailSchema)

Basic Operations

Mongoose has a flexible API and provides many ways to accomplish a task. We will not focus on the variations because that is out of scope for this article, but remember that most of the operations can be done in more than one way either syntactically or via the application architecture.

Create Record

Let’s create an instance of the email model and save it to the database:

let EmailModel = require('./email') let msg = new EmailModel({ email: '[email protected]' }) msg.save() .then(doc => { console.log(doc) }) .catch(err => { console.error(err) })

The result is a document that is returned upon a successful save:

{ _id: 5a78fe3e2f44ba8f85a2409a, email: '[email protected]', __v: 0 }

The following fields are returned (internal fields are prefixed with an underscore):

  1. The _id field is auto-generated by Mongo and is a primary key of the collection. Its value is a unique identifier for the document.
  2. The value of the email field is returned. Notice that it is lower-cased because we specified the lowercase:true attribute in the schema.
  3. __v is the versionKey property set on each document when first created by Mongoose. Its value contains the internal revision of the document.

If you try to repeat the save operation above, you will get an error because we have specified that the email field should be unique.

Fetch Record

Let’s try to retrieve the record we saved to the database earlier. The model class exposes several static and instance methods to perform operations on the database. We will now try to find the record that we created previously using the find method and pass the email as the search term.

EmailModel .find({ email: '[email protected]' // search query }) .then(doc => { console.log(doc) }) .catch(err => { console.error(err) })

The document returned will be similar to what was displayed when we created the record:

{ _id: 5a78fe3e2f44ba8f85a2409a, email: '[email protected]', __v: 0 }

Update Record

Let’s modify the record above by changing the email address and adding another field to it, all in a single operation. For performance reasons, Mongoose won’t return the updated document so we need to pass an additional parameter to ask for it:

EmailModel .findOneAndUpdate( { email: '[email protected]' // search query }, { email: '[email protected]' // field:values to update }, { new: true, // return updated doc runValidators: true // validate before update }) .then(doc => { console.log(doc) }) .catch(err => { console.error(err) })

The document returned will contain the updated email:

{ _id: 5a78fe3e2f44ba8f85a2409a, email: '[email protected]', __v: 0 }

Delete Record

We will use the findOneAndRemove call to delete a record. It returns the original document that was removed:

EmailModel .findOneAndRemove({ email: '[email protected]' }) .then(response => { console.log(response) }) .catch(err => { console.error(err) })

Helpers

We have looked at some of the basic functionality above known as CRUD (Create, Read, Update, Delete) operations, but Mongoose also provides the ability to configure several types of helper methods and properties. These can be used to further simplify working with data.

Let’s create a user schema in ./src/models/user.js with the fieldsfirstName and lastName:

let mongoose = require('mongoose') let userSchema = new mongoose.Schema({ firstName: String, lastName: String }) module.exports = mongoose.model('User', userSchema)

Virtual Property

A virtual property is not persisted to the database. We can add it to our schema as a helper to get and set values.

Let’s create a virtual property called fullName which can be used to set values on firstName and lastName and retrieve them as a combined value when read:

userSchema.virtual('fullName').get(function() { return this.firstName + ' ' + this.lastName }) userSchema.virtual('fullName').set(function(name) { let str = name.split(' ') this.firstName = str[0] this.lastName = str[1] })

Callbacks for get and set must use the function keyword as we need to access the model via the thiskeyword. Using fat arrow functions will change what this refers to.

Now, we can set firstName and lastName by assigning a value to fullName:

let model = new UserModel() model.fullName = 'Thomas Anderson' console.log(model.toJSON()) // Output model fields as JSON console.log() console.log(model.fullName) // Output the full name

The code above will output the following:

{ _id: 5a7a4248550ebb9fafd898cf, firstName: 'Thomas', lastName: 'Anderson' } Thomas Anderson

Instance Methods

We can create custom helper methods on the schema and access them via the model instance. These methods will have access to the model object and they can be used quite creatively. For instance, we could create a method to find all the people who have the same first name as the current instance.

In this example, let’s create a function to return the initials for the current user. Let’s add a custom helper method called getInitials to the schema:

userSchema.methods.getInitials = function() { return this.firstName[0] + this.lastName[0] }

This method will be accessible via a model instance:

let model = new UserModel({ firstName: 'Thomas', lastName: 'Anderson' }) let initials = model.getInitials() console.log(initials) // This will output: TA

Static Methods

Similar to instance methods, we can create static methods on the schema. Let’s create a method to retrieve all users in the database:

userSchema.statics.getUsers = function() { return new Promise((resolve, reject) => { this.find((err, docs) => { if(err) { console.error(err) return reject(err) } resolve(docs) }) }) }

Calling getUsers on the Model class will return all the users in the database:

UserModel.getUsers() .then(docs => { console.log(docs) }) .catch(err => { console.error(err) })

Adding instance and static methods is a nice approach to implement an interface to database interactions on collections and records.

Middleware

Middleware are functions that run at specific stages of a pipeline. Mongoose supports middleware for the following operations:

  • Aggregate
  • Document
  • Model
  • Query

For instance, models have pre and post functions that take two parameters:

  1. Type of event (‘init’, ‘validate’, ‘save’, ‘remove’)
  2. A callback that is executed with this referencing the model instance

Let’s try an example by adding two fields called createdAt and updatedAt to our schema:

let mongoose = require('mongoose') let userSchema = new mongoose.Schema({ firstName: String, lastName: String, createdAt: Date, updatedAt: Date }) module.exports = mongoose.model('User', userSchema)

When model.save() is called, there is a pre(‘save’, …) and post(‘save’, …) event that is triggered. For the second parameter, you can pass a function that is called when the event is triggered. These functions take a parameter to the next function in the middleware chain.

Let’s add a pre-save hook and set values for createdAt and updatedAt:

userSchema.pre('save', function (next) { let now = Date.now() this.updatedAt = now // Set a value for createdAt only if it is null if (!this.createdAt) { this.createdAt = now } // Call the next function in the pre-save chain next() })

Let’s create and save our model:

let UserModel = require('./user') let model = new UserModel({ fullName: 'Thomas Anderson' } msg.save() .then(doc => { console.log(doc) }) .catch(err => { console.error(err) })

You should see values for createdAt and updatedAt when the record that is created is printed:

{ _id: 5a7bbbeebc3b49cb919da675, firstName: 'Thomas', lastName: 'Anderson', updatedAt: 2018-02-08T02:54:38.888Z, createdAt: 2018-02-08T02:54:38.888Z, __v: 0 }

Plugins

Suppose that we want to track when a record was created and last updated on every collection in our database. Instead of repeating the above process, we can create a plugin and apply it to every schema.

Let’s create a file ./src/model/plugins/timestamp.js and replicate the above functionality as a reusable module:

module.exports = function timestamp(schema) { // Add the two fields to the schema schema.add({ createdAt: Date, updatedAt: Date }) // Create a pre-save hook schema.pre('save', function (next) { let now = Date.now() this.updatedAt = now // Set a value for createdAt only if it is null if (!this.createdAt) { this.createdAt = now } // Call the next function in the pre-save chain next() }) }

To use this plugin, we simply pass it to the schemas that should be given this functionality:

let timestampPlugin = require('./plugins/timestamp') emailSchema.plugin(timestampPlugin) userSchema.plugin(timestampPlugin)

Query Building

Mongoose has a very rich API that handles many complex operations supported by MongoDB. Consider a query where we can incrementally build query components.

In this example, we are going to:

  1. Find all users
  2. Skip the first 100 records
  3. Limit the results to 10 records
  4. Sort the results by the firstName field
  5. Select the firstName
  6. Execute that query
UserModel.find() // find all users .skip(100) // skip the first 100 items .limit(10) // limit to 10 items .sort({firstName: 1} // sort ascending by firstName .select({firstName: true} // select firstName only .exec() // execute the query .then(docs => { console.log(docs) }) .catch(err => { console.error(err) })

Closing

We have barely scratched the surface exploring some of the capabilities of Mongoose. It is a rich library full of useful and and powerful features that make it a joy to work with data models in the application layer.

While you can interact with Mongo directly using Mongo Driver, Mongoose will simplify that interaction by allowing you to model relationships between data and validate them easily.

Fun Fact: Το Mongoose δημιουργήθηκε από τον Valeri Karpovπου είναι ένας απίστευτα ταλαντούχος μηχανικός! Επινόησε τον όρο The MEAN Stack .

Εάν αυτό το άρθρο ήταν χρήσιμο, ??? και ακολουθήστε με στο Twitter.