Mongoose 101: Εισαγωγή στα βασικά, δευτερεύοντα έγγραφα και πληθυσμός

Το Mongoose είναι μια βιβλιοθήκη που διευκολύνει τη χρήση του MongoDB. Κάνει δύο πράγματα:

  1. Δίνει δομή στις συλλογές MongoDB
  2. Σας δίνει χρήσιμες μεθόδους χρήσης

Σε αυτό το άρθρο, θα εξετάσουμε:

  1. Τα βασικά της χρήσης του Mongoose
  2. Υπο-έγγραφα του Mongoose
  3. Πληθυσμός μαγκούζης

Μέχρι το τέλος του άρθρου, θα πρέπει να μπορείτε να χρησιμοποιείτε το Mongoose χωρίς προβλήματα.

Προαπαιτούμενα

Υποθέτω ότι έχετε κάνει τα εξής:

  1. Έχετε εγκαταστήσει το MongoDB στον υπολογιστή σας
  2. Ξέρετε πώς να ρυθμίσετε μια τοπική σύνδεση MongoDB
  3. Ξέρετε πώς να βλέπετε τα δεδομένα που έχετε στη βάση δεδομένων σας
  4. Ξέρετε τι είναι οι "συλλογές" στο MongoDB

Εάν δεν γνωρίζετε κανένα από αυτά, διαβάστε "Πώς να ρυθμίσετε μια τοπική σύνδεση MongoDB" προτού συνεχίσετε.

Υποθέτω επίσης ότι γνωρίζετε πώς να χρησιμοποιήσετε το MongoDB για να δημιουργήσετε μια απλή εφαρμογή CRUD. Εάν δεν ξέρετε πώς να το κάνετε αυτό, διαβάστε "Πώς να δημιουργήσετε μια εφαρμογή CRUD με Node, Express και MongoDB" προτού συνεχίσετε.

Βασικά Mongoose

Εδώ, θα μάθετε πώς να:

  1. Συνδεθείτε στη βάση δεδομένων
  2. Δημιουργήστε ένα μοντέλο
  3. Δημιουργήστε ένα έγγραφο
  4. Βρείτε ένα έγγραφο
  5. Ενημέρωση εγγράφου
  6. Διαγράψτε ένα έγγραφο

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

Πρώτα, πρέπει να κατεβάσετε το Mongoose.

npm install mongoose --save 

Μπορείτε να συνδεθείτε σε μια βάση δεδομένων με τη connectμέθοδο. Ας πούμε ότι θέλουμε να συνδεθούμε με μια βάση δεδομένων που ονομάζεται street-fighters. Εδώ είναι ο κωδικός που χρειάζεστε:

const mongoose = require('mongoose') const url = 'mongodb://127.0.0.1:27017/street-fighters' mongoose.connect(url, { useNewUrlParser: true }) 

Θέλουμε να μάθουμε αν η σύνδεσή μας πέτυχε ή απέτυχε. Αυτό μας βοηθά με τον εντοπισμό σφαλμάτων.

Για να ελέγξουμε εάν η σύνδεση πέτυχε, μπορούμε να χρησιμοποιήσουμε το openσυμβάν. Για να ελέγξουμε εάν η σύνδεση απέτυχε, χρησιμοποιούμε το errorσυμβάν.

const db = mongoose.connection db.once('open', _ => { console.log('Database connected:', url) }) db.on('error', err => { console.error('connection error:', err) }) 

Δοκιμάστε να συνδεθείτε στη βάση δεδομένων. Θα πρέπει να δείτε ένα αρχείο καταγραφής ως εξής:

Συνδέθηκε σε μια βάση δεδομένων.

Δημιουργία μοντέλου

Στο Mongoose, πρέπει να χρησιμοποιήσετε μοντέλα για να δημιουργήσετε, να διαβάσετε, να ενημερώσετε ή να διαγράψετε στοιχεία από μια συλλογή MongoDB.

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

Δείτε πώς δημιουργείτε ένα σχήμα:

const mongoose = require('mongoose') const Schema = mongoose.Schema const schema = new Schema({ // ... }) 

Μπορείτε να χρησιμοποιήσετε 10 διαφορετικά είδη τιμών σε ένα σχήμα. Τις περισσότερες φορές, θα χρησιμοποιήσετε αυτά τα έξι:

  • Σειρά
  • Αριθμός
  • Boolean
  • Πίνακας
  • Ημερομηνία
  • Αντικείμενο

Ας το εφαρμόσουμε στην πράξη.

Ας πούμε ότι θέλουμε να δημιουργήσουμε χαρακτήρες για τη βάση δεδομένων του Street Fighter.

Στο Mongoose, είναι φυσιολογική πρακτική να τοποθετείτε κάθε μοντέλο στο δικό του αρχείο. Έτσι θα δημιουργήσουμε ένα Character.jsαρχείο πρώτα. Αυτό το Character.jsαρχείο θα τοποθετηθεί στο modelsφάκελο.

project/ |- models/ |- Character.js 

Σε Character.js, δημιουργούμε ένα characterSchema.

const mongoose = require('mongoose') const Schema = mongoose.Schema const characterSchema = new Schema({ // ... }) 

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

  1. Όνομα του χαρακτήρα
  2. Όνομα της απόλυτης κίνησής τους

Και οι δύο μπορούν να αναπαρασταθούν με χορδές.

const mongoose = require('mongoose') const Schema = mongoose.Schema const characterSchema = new Schema({ name: String, ultimate: String }) 

Μόλις δημιουργήσουμε characterSchema, μπορούμε να χρησιμοποιήσουμε τη modelμέθοδο mongoose για να δημιουργήσουμε το μοντέλο.

module.exports = mongoose.model('Character', characterSchema) 

Δημιουργία εγγράφου

Ας υποθέσουμε ότι έχετε ένα αρχείο που ονομάζεται index.js. Εδώ θα εκτελέσουμε λειτουργίες Mongoose για αυτό το σεμινάριο.

project/ |- index.js |- models/ |- Character.js 

Αρχικά, πρέπει να φορτώσετε το μοντέλο χαρακτήρων. Μπορείτε να το κάνετε με require.

const Character = require('./models/Character') 

Let's say you want to create a character called Ryu. Ryu has an ultimate move called "Shinku Hadoken".

To create Ryu, you use the new, followed by your model. In this case, it's new Character.

const ryu = new Character ({ name: 'Ryu', ultimate: 'Shinku Hadoken' }) 

new Character creates the character in memory. It has not been saved to the database yet. To save to the database, you can run the save method.

ryu.save(function (error, document) { if (error) console.error(error) console.log(document) }) 

If you run the code above, you should see this in the console.

Ο Ryu αποθηκεύτηκε στη βάση δεδομένων.

Promises and Async/await

Mongoose supports promises. It lets you write nicer code like this:

// This does the same thing as above function saveCharacter (character) { const c = new Character(character) return c.save() } saveCharacter({ name: 'Ryu', ultimate: 'Shinku Hadoken' }) .then(doc => { console.log(doc) }) .catch(error => { console.error(error) }) 

You can also use the await keyword if you have an asynchronous function.

If the Promise or Async/Await code looks foreign to you, I recommend reading "JavaScript async and await" before continuing with this tutorial.

async function runCode() { const ryu = new Character({ name: 'Ryu', ultimate: 'Shinku Hadoken' }) const doc = await ryu.save() console.log(doc) } runCode() .catch(error => { console.error(error) }) 

Note: I'll use the async/await format for the rest of the tutorial.

Uniqueness

Mongoose adds a new character to the database each time you use new Character and save. If you run the code(s) above three times, you'd expect to see three Ryus in the database.

Three Ryus στη βάση δεδομένων.

We don't want to have three Ryus in the database. We want to have ONE Ryu only. To do this, we can use the unique option.

const characterSchema = new Schema({ name: { type: String, unique: true }, ultimate: String }) 

The unique option creates a unique index. It ensures that we cannot have two documents with the same value (for name in this case).

For unique to work properly, you need to clear the Characters collection. To clear the Characters collection, you can use this:

await Character.deleteMany({}) 

Try to add two Ryus into the database now. You'll get an E11000 duplicate key error. You won't be able to save the second Ryu.

Διπλότυπο σφάλμα κλειδιού.

Let's add another character into the database before we continue the rest of the tutorial.

const ken = new Character({ name: 'Ken', ultimate: 'Guren Enjinkyaku' }) await ken.save() 
Η βάση δεδομένων περιέχει δύο χαρακτήρες.

Finding a document

Mongoose gives you two methods to find stuff from MongoDB.

  1. findOne: Gets one document.
  2. find: Gets an array of documents

findOne

findOnereturns the first document it finds. You can specify any property to search for. Let's search for Ryu:

const ryu = await Character.findOne({ name: 'Ryu' }) console.log(ryu) 
Βρέθηκε ο Ryu από τη βάση δεδομένων.

find

findreturns an array of documents. If you specify a property to search for, it'll return documents that match your query.

const chars = await Character.find({ name: 'Ryu' }) console.log(chars) 
Πέρασε τη βάση δεδομένων και βρήκε έναν χαρακτήρα με το όνομα Ryu

If you did not specify any properties to search for, it'll return an array that contains all documents in the collection.

const chars = await Character.find() console.log(chars) 
Βρέθηκαν δύο χαρακτήρες στη βάση δεδομένων.

Updating a document

Let's say Ryu has three special moves:

  1. Hadoken
  2. Shoryuken
  3. Tatsumaki Senpukyaku

We want to add these special moves into the database. First, we need to update our CharacterSchema.

const characterSchema = new Schema({ name: { type: String, unique: true }, specials: Array, ultimate: String }) 

Then, we use one of these two ways to update a character:

  1. Use findOne, then use save
  2. Use findOneAndUpdate

findOne and save

First, we use findOne to get Ryu.

const ryu = await Character.findOne({ name: 'Ryu' }) console.log(ryu) 

Then, we update Ryu to include his special moves.

const ryu = await Character.findOne({ name: 'Ryu' }) ryu.specials = [ 'Hadoken', 'Shoryuken', 'Tatsumaki Senpukyaku' ] 

After we modified ryu, we run save.

const ryu = await Character.findOne({ name: 'Ryu' }) ryu.specials = [ 'Hadoken', 'Shoryuken', 'Tatsumaki Senpukyaku' ] const doc = await ryu.save() console.log(doc) 
Ενημερώθηκε ο Ryu.

findOneAndUpdate

findOneAndUpdate is the same as MongoDB's findOneAndModify method.

Here, you search for Ryu and pass the fields you want to update at the same time.

// Syntax await findOneAndUpdate(filter, update) 
// Usage const doc = await Character.findOneAndUpdate( { name: 'Ryu' }, { specials: [ 'Hadoken', 'Shoryuken', 'Tatsumaki Senpukyaku' ] }) console.log(doc) 
Ενημερώθηκε ο Ryu.

Difference between findOne + save vs findOneAndUpdate

Two major differences.

First, the syntax for findOne` + `save is easier to read than findOneAndUpdate.

Second, findOneAndUpdate does not trigger the save middleware.

I'll choose findOne + save over findOneAndUpdate anytime because of these two differences.

Deleting a document

There are two ways to delete a character:

  1. findOne + remove
  2. findOneAndDelete

Using findOne + remove

const ryu = await Character.findOne({ name: 'Ryu' }) const deleted = await ryu.remove() 

Using findOneAndDelete

const deleted = await Character.findOneAndDelete({ name: 'Ken' }) 

Subdocuments

In Mongoose, subdocuments are documents that are nested in other documents. You can spot a subdocument when a schema is nested in another schema.

Note: MongoDB calls subdocuments embedded documents.

const childSchema = new Schema({ name: String }); const parentSchema = new Schema({ // Single subdocument child: childSchema, // Array of subdocuments children: [ childSchema ] }); 

In practice, you don't have to create a separate childSchema like the example above. Mongoose helps you create nested schemas when you nest an object in another object.

// This code is the same as above const parentSchema = new Schema({ // Single subdocument child: { name: String }, // Array of subdocuments children: [{name: String }] }); 

In this section, you will learn to:

  1. Create a schema that includes a subdocument
  2. Create documents that contain subdocuments
  3. Update subdocuments that are arrays
  4. Update a single subdocument

Updating characterSchema

Let's say we want to create a character called Ryu. Ryu has three special moves.

  1. Hadoken
  2. Shinryuken
  3. Tatsumaki Senpukyaku

Ryu also has one ultimate move called:

  1. Shinku Hadoken

We want to save the names of each move. We also want to save the keys required to execute that move.

Here, each move is a subdocument.

const characterSchema = new Schema({ name: { type: String, unique: true }, // Array of subdocuments specials: [{ name: String, keys: String }] // Single subdocument ultimate: { name: String, keys: String } }) 

You can also use the childSchema syntax if you wish to. It makes the Character schema easier to understand.

const moveSchema = new Schema({ name: String, keys: String }) const characterSchema = new Schema({ name: { type: String, unique: true }, // Array of subdocuments specials: [moveSchema], // Single subdocument ultimate: moveSchema }) 

Creating documents that contain subdocuments

There are two ways to create documents that contain subdocuments:

  1. Pass a nested object into new Model
  2. Add properties into the created document.

Method 1: Passing the entire object

For this method, we construct a nested object that contains both Ryu's name and his moves.

const ryu = { name: 'Ryu', specials: [{ name: 'Hadoken', keys: '↓ ↘ → P' }, { name: 'Shoryuken', keys: '→ ↓ ↘ → P' }, { name: 'Tatsumaki Senpukyaku', keys: '↓ ↙ ← K' }], ultimate: { name: 'Shinku Hadoken', keys: '↓ ↘ → ↓ ↘ → P' } } 

Then, we pass this object into new Character.

const char = new Character(ryu) const doc = await char.save() console.log(doc) 
Εικόνα του εγγράφου του Ryu.

Method 2: Adding subdocuments later

For this method, we create a character with new Character first.

const ryu = new Character({ name: 'Ryu' }) 

Then, we edit the character to add special moves:

const ryu = new Character({ name: 'Ryu' }) const ryu.specials = [{ name: 'Hadoken', keys: '↓ ↘ → P' }, { name: 'Shoryuken', keys: '→ ↓ ↘ → P' }, { name: 'Tatsumaki Senpukyaku', keys: '↓ ↙ ← K' }] 

Then, we edit the character to add the ultimate move:

const ryu = new Character({ name: 'Ryu' }) // Adds specials const ryu.specials = [{ name: 'Hadoken', keys: '↓ ↘ → P' }, { name: 'Shoryuken', keys: '→ ↓ ↘ → P' }, { name: 'Tatsumaki Senpukyaku', keys: '↓ ↙ ← K' }] // Adds ultimate ryu.ultimate = { name: 'Shinku Hadoken', keys: '↓ ↘ → ↓ ↘ → P' } 

Once we're satisfied with ryu, we run save.

const ryu = new Character({ name: 'Ryu' }) // Adds specials const ryu.specials = [{ name: 'Hadoken', keys: '↓ ↘ → P' }, { name: 'Shoryuken', keys: '→ ↓ ↘ → P' }, { name: 'Tatsumaki Senpukyaku', keys: '↓ ↙ ← K' }] // Adds ultimate ryu.ultimate = { name: 'Shinku Hadoken', keys: '↓ ↘ → ↓ ↘ → P' } const doc = await ryu.save() console.log(doc) 
Εικόνα του εγγράφου του Ryu.

Updating array subdocuments

The easiest way to update subdocuments is:

  1. Use findOne to find the document
  2. Get the array
  3. Change the array
  4. Run save

For example, let's say we want to add Jodan Sokutou Geri to Ryu's special moves. The keys for Jodan Sokutou Geri are ↓ ↘ → K.

First, we find Ryu with findOne.

const ryu = await Characters.findOne({ name: 'Ryu' }) 

Mongoose documents behave like regular JavaScript objects. We can get the specials array by writing ryu.specials.

const ryu = await Characters.findOne({ name: 'Ryu' }) const specials = ryu.specials console.log(specials) 
Ημερολόγιο ειδικών.

This specials array is a normal JavaScript array.

const ryu = await Characters.findOne({ name: 'Ryu' }) const specials = ryu.specials console.log(Array.isArray(specials)) // true 

We can use the push method to add a new item into specials,

const ryu = await Characters.findOne({ name: 'Ryu' }) ryu.specials.push({ name: 'Jodan Sokutou Geri', keys: '↓ ↘ → K' }) 

After updating specials, we run save to save Ryu to the database.

const ryu = await Characters.findOne({ name: 'Ryu' }) ryu.specials.push({ name: 'Jodan Sokutou Geri', keys: '↓ ↘ → K' }) const updated = await ryu.save() console.log(updated) 
Ο Ryu ενημερώθηκε με τον Jodan Sokutou Geri

Updating a single subdocument

It's even easier to update single subdocuments. You can edit the document directly like a normal object.

Let's say we want to change Ryu's ultimate name from Shinku Hadoken to Dejin Hadoken. What we do is:

  1. Use findOne to get Ryu.
  2. Change the name in ultimate
  3. Run save
const ryu = await Characters.findOne({ name: 'Ryu' }) ryu.ultimate.name = 'Dejin Hadoken' const updated = await ryu.save() console.log(updated) 
Έγγραφο Ryu με τον Dejin Hadoken.

Population

MongoDB documents have a size limit of 16MB. This means you can use subdocuments (or embedded documents) if they are small in number.

For example, Street Fighter characters have a limited number of moves. Ryu only has 4 special moves. In this case, it's okay to use embed moves directly into Ryu's character document.

Το έγγραφο του Ryu.

But if you have data that can contain an unlimited number of subdocuments, you need to design your database differently.

One way is to create two separate models and combine them with populate.

Creating the models

Let's say you want to create a blog. And you want to store the blog content with MongoDB. Each blog has a title, content, and comments.

Your first schema might look like this:

const blogPostSchema = new Schema({ title: String, content: String, comments: [{ comment: String }] }) module.exports = mongoose.model('BlogPost', blogPostSchema) 

There's a problem with this schema.

A blog post can have an unlimited number of comments. If a blog post explodes in popularity and comments swell up, the document might exceed the 16MB limit imposed by MongoDB.

This means we should not embed comments in blog posts. We should create a separate collection for comments.

const comments = new Schema({ comment: String }) module.exports = mongoose.model('Comment', commentSchema) 

In Mongoose, we can link up the two models with Population.

To use Population, we need to:

  1. Set type of a property to Schema.Types.ObjectId
  2. Set ref to the model we want to link too.

Here, we want comments in blogPostSchema to link to the Comment collection. This is the schema we'll use:

const blogPostSchema = new Schema({ title: String, content: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }] }) module.exports = mongoose.model('BlogPost', blogPostSchema) 

Creating a blog post

Let's say you want to create a blog post. To create the blog post, you use new BlogPost.

const blogPost = new BlogPost({ title: 'Weather', content: `How's the weather today?` }) 

A blog post can have zero comments. We can save this blog post with save.

const doc = await blogPost.save() console.log(doc) 
Δημιουργήθηκε ένα έγγραφο ανάρτησης ιστολογίου χωρίς σχόλια.

Creating comments

Now let's say we want to create a comment for the blog post. To do this, we create and save the comment.

const comment = new Comment({ comment: `It's damn hot today` }) const savedComment = await comment.save() console.log(savedComment) 
Δημιουργήθηκε και αποθηκεύτηκε ένα σχόλιο.

Notice the saved comment has an _id attribute. We need to add this _id attribute into the blog post's comments array. This creates the link.

// Saves comment to Database const savedComment = await comment.save() // Adds comment to blog post // Then saves blog post to database const blogPost = await BlogPost.findOne({ title: 'Weather' }) blogPost.comments.push(savedComment._id) const savedPost = await blogPost.save() console.log(savedPost) 

Searching blog posts and their comments

If you tried to search for the blog post, you'll see the blog post has an array of comment IDs.

const blogPost = await BlogPost.findOne({ title: 'Weather' }) console.log(blogPost) 
Η ανάρτηση ιστολογίου που βρέθηκε περιέχει αναγνωριστικά σχολίων.

There are four ways to get comments.

  1. Mongoose population
  2. Manual way #1
  3. Manual way #2
  4. Manual way #3

Mongoose Population

Mongoose allows you to fetch linked documents with the populate method. What you need to do is call .populate when you execute with findOne.

When you call populate, you need to pass in the key of the property you want to populate. In this case, the key is comments. (Note: Mongoose calls this key a "path").

const blogPost = await BlogPost.findOne({ title: 'Weather' }) .populate('comments') console.log(blogPost) 
Τα σχόλια συμπληρώθηκαν από το Mongoose.

Manual way (method 1)

Without Mongoose Populate, you need to find the comments manually. First, you need to get the array of comments.

const blogPost = await BlogPost.findOne({ title: 'Weather' }) .populate('comments') const commentIDs = blogPost.comments 

Then, you loop through commentIDs to find each comment. If you go with this method, it's slightly faster to use Promise.all.

const commentPromises = commentIDs.map(_id => { return Comment.findOne({ _id }) }) const comments = await Promise.all(commentPromises) console.log(comments) 
Βρέθηκαν σχόλια.

Manual way (method 2)

Mongoose gives you an $in operator. You can use this $in operator to find all comments within an array. This syntax takes a little effort to get used to.

If I had to do the manual way, I'd prefer Manual #1 over this.

const commentIDs = blogPost.comments const comments = await Comment.find({ '_id': { $in: commentIDs } }) console.log(comments) 
Βρέθηκαν σχόλια.

Manual way (method 3)

For the third method, we need to change the schema. When we save a comment, we link the comment to the blog post.

// Linking comments to blog post const commentSchema = new Schema({ comment: String blogPost: [{ type: Schema.Types.ObjectId, ref: 'BlogPost' }] }) module.exports = mongoose.model('Comment', commentSchema) 

You need to save the comment into the blog post, and the blog post id into the comment.

const blogPost = await BlogPost.findOne({ title: 'Weather' }) // Saves comment const comment = new Comment({ comment: `It's damn hot today`, blogPost: blogPost._id }) const savedComment = comment.save() // Links blog post to comment blogPost.comments.push(savedComment._id) await blogPost.save() 

Once you do this, you can search the Comments collection for comments that match your blog post's id.

// Searches for comments const blogPost = await BlogPost.findOne({ title: 'Weather' }) const comments = await Comment.find({ _id: blogPost._id }) console.log(comments) 
Βρέθηκαν σχόλια.

I'd prefer Manual #3 over Manual #1 and Manual #2.

And Population beats all three manual methods.

Quick Summary

You learned to use Mongoose on three different levels in this article:

  1. Basic Mongoose
  2. Mongoose subdocuments
  3. Mongoose population

That's it!

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