DEV Community

Elham Najeebullah
Elham Najeebullah

Posted on

Mongoose ODM Best Practices Part One.

1. A better way to connect to the database

To connect the latest version of Mongoose with an Express app and use the separation of concern design principle, you can do the following:

Create a separate module for your database connection and Mongoose models:

// models/index.js

const mongoose = require('mongoose');

async function connectToDatabase() {
  try {
    await mongoose.connect('mongodb://localhost/my-app', {
      useCreateIndex: true,
      useFindAndModify: false,
    });
    console.log('Successfully connected to database');
  } catch (error) {
    console.error(error);
  }
}

module.exports = {
  connectToDatabase,
};

Enter fullscreen mode Exit fullscreen mode

Then in your Express app, require the models module and call the connectToDatabase function when the app starts:

const express = require('express');
const models = require('./models');

const app = express();

models.connectToDatabase();

// ... your app routes and middleware go here

app.listen(3000, () => {
  console.log('Express app listening on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

This code separates the database connection and Mongoose models into a separate models module, which can be imported and used in the main Express app. This follows the separation of concern design principle, as it allows you to keep your database logic and app logic separate and modular.

2. Use the power of Index

In a database, an index is a data structure that improves the speed of data access operations on a table. Indexes allow the database to quickly find and retrieve specific rows of data based on the values in one or more columns.

Here is an example of how you might use an index in Mongoose, a popular ODM (Object Document Mapper) for MongoDB:

const UserSchema = new mongoose.Schema({
  name: String,
  email: { type: String, index: true },
});

Enter fullscreen mode Exit fullscreen mode

In this example, we are creating a new Mongoose schema for a User model with a name field and an email field. We are also adding an index to the email field by setting the index property to true. This will create an index on the email field in the underlying MongoDB collection, allowing the database to quickly find and retrieve documents based on the value of the email field.

There are several benefits to using indexes in Mongoose:

  • Improved performance: Indexes can significantly improve the performance of read operations, such as queries and sorts, by allowing the database to quickly locate the desired documents.

  • Unique constraints: Indexes can be used to enforce unique constraints on a field, ensuring that no two documents in a collection have the same value for that field.

 const UserSchema = new mongoose.Schema({
   name: String,
   email: { type: String, unique: true },
 });
Enter fullscreen mode Exit fullscreen mode

This will create a unique index on the email field in the underlying MongoDB collection, ensuring that no two documents in the users collection have the same value for the email field.

If you try to insert a document with the same value for the email field as an existing document, Mongoose will return a validation error indicating that the value is not unique.

const user1 = await User.create({ name: 'John', email: 'john@example.com' });
// success

const user2 = await User.create({ name: 'Jane', email: 'john@example.com' });
// throws an error: "E11000 duplicate key

Enter fullscreen mode Exit fullscreen mode
  • Text search: Indexes can be used to support text search operations, allowing you to search for documents that contain specific words or phrases. You can create a text index on the fields you want to search and use the $text operator in your queries.
const BlogPostSchema = new mongoose.Schema({
  title: String,
  content: String,
});

BlogPostSchema.index({ title: 'text', content: 'text' });

const BlogPost = mongoose.model('BlogPost', BlogPostSchema);

async function searchBlogPosts(searchTerm) {
  const results = await BlogPost.find({
    $text: { $search: searchTerm },
  });
  console.log(results);
}

searchBlogPosts('MongoDB');

Enter fullscreen mode Exit fullscreen mode

This code defines a BlogPostSchema with a title field and a content field, and creates a text index on both fields. It then defines a BlogPost model using this schema.

The searchBlogPosts function demonstrates how to use the $text operator in a query to search for documents that contain the specified search term in either the title or content field. When the searchBlogPosts function is called with the search term "MongoDB", it will find all blog posts that contain the word "MongoDB" in either the title or content field.

  • Geospatial queries: Indexes can be used to support geospatial queries, allowing you to find documents based on their location relative to a given point or shape.

3. Create Relationship

To create a one-to-many relationship in Mongoose, you will need to use a ref property to reference the related documents.

Here is an example of how you might set up a one-to-many relationship between a User model and a Task model in Mongoose:

const TaskSchema = new mongoose.Schema({
  description: String,
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
});

const UserSchema = new mongoose.Schema({
  name: String,
  email: String,
  tasks: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Task' }],
});

const User = mongoose.model('User', UserSchema);
const Task = mongoose.model('Task', TaskSchema);

Enter fullscreen mode Exit fullscreen mode

In this example, we are defining a TaskSchema with a user field that references a User document, and a UserSchema with a tasks field that is an array of references to Task documents. This sets up a one-to-many relationship between users and tasks, where a user can have many tasks, but each task is associated with a single user.

To create a new task for a user, you can use the populate method to retrieve the related user documents:

const task = await Task.create({ description: 'Finish homework', user: userId });
const populatedTask = await Task.findById(task.id).populate('user');

console.log(populatedTask.user.name); // prints the name of the user associated with the task

Enter fullscreen mode Exit fullscreen mode

Here is a more complete example of how you might set up a one-to-many relationship between a User model and a Task model in Mongoose:

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/my-app', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

const TaskSchema = new mongoose.Schema({
  description: String,
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
});

const UserSchema = new mongoose.Schema({
  name: String,
  email: String,
  tasks: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Task' }],
});

const User = mongoose.model('User', UserSchema);
const Task = mongoose.model('Task', TaskSchema);

async function createUserAndTask() {
  const user = await User.create({ name: 'John', email: 'john@example.com' });
  const task = await Task.create({ description: 'Finish homework', user: user._id });
  console.log(task);
}

createUserAndTask();

async function getUserAndTasks() {
  const user = await User.findOne({ name: 'John' }).populate('tasks');
  console.log(user);
}

getUserAndTasks();

Enter fullscreen mode Exit fullscreen mode

This code defines a TaskSchema with a user field that references a User document, and a UserSchema with a tasks field that is an array of references to Task documents. It also defines User and Task models using these schemas, and creates a new user and task in the createUserAndTask function.

The getUserAndTasks function demonstrates how to use the populate method to retrieve the related task documents for a user. When the getUserAndTasks function is called, it will find the user with the name "John" and populate the tasks field with the associated task documents, resulting in a single query to the database.

To create multiple tasks for a single user in Mongoose, you can simply push the new task documents to the tasks array of the user document. Here is an example of how you might do this:

async function createTasksForUser(userId) {
  const user = await User.findById(userId);
  const task1 = await Task.create({ description: 'Finish homework', user: user._id });
  const task2 = await Task.create({ description: 'Buy groceries', user: user._id });
  user.tasks.push(task1, task2);
  await user.save();
}

createTasksForUser(userId);

Enter fullscreen mode Exit fullscreen mode

This code finds the user with the specified userId, creates two new tasks, and then pushes the tasks to the tasks array of the user document. It then saves the updated user document back to the database.

To retrieve all the tasks associated with a user, you can use the populate method as shown in the previous example:

async function getTasksForUser(userId) {
  const user = await User.findById(userId).populate('tasks');
  console.log(user.tasks);
}

getTasksForUser(userId);

Enter fullscreen mode Exit fullscreen mode

More to come in Part 2 :)

Top comments (1)

Collapse
 
ayansh03 profile image
Ayan Sheikh

waiting for part two ๐Ÿ‘