Use the database Mongoose

Express Database

Express apps can use all databases supported by Node (Express itself doesn’t support any specific behaviors/needs for database management). There are many popular choices, including PostgreSQL, MySQL, Redis, SQLite, and MongoDB.

There are two ways to interact with the database:

  • Use the database’s native query language (such as SQL)

  • Use Object Data Model (ODM) or Object Relational Model (ORM). ODM/ORM can represent the data in the website as JavaScript objects, and then map them to the underlying database. Some ORMs are only suitable for certain databases, while others are generally applicable.

Best performance is achieved using SQL or another supported query language. ODM is usually slower because there is a layer of translation code between objects and database formats for mapping, making it not necessarily the most performant make greater compromises).

The advantage of using an ORM is that programmers can continue to think in terms of JavaScript objects without turning to database semantics. Especially when using different databases (same or different websites). Validation and inspection of data is also easier with an ORM.

There are many ODM/ORM solutions on the NPM site (see also the list of odm and orm tags on the NPM site).

Here are a few popular solutions:

  • Mongoose: A MongoDB object modeling tool designed for asynchronous work environments.

  • Waterline: An ORM extracted from the Express-based Sails framework. It provides a unified API to access many different databases, including Redis, mySQL, LDAP, MongoDB, and Postgres.

  • Bookshelf: Provides both promise-based and traditional callback interfaces, supports transaction processing, eager/nested eager relationship loading, polymorphic association, and one-to-one, one-to-many and many-to-many relationships. Supports PostgreSQL, MySQL, and SQLite3.

  • Objection: Use the full power of SQL and the underlying database engine in the easiest possible way (supports SQLite3, Postgres and MySQL).

  • Sequelize: A promise-based ORM for Node.js that supports PostgreSQL, MySQL, MariaDB, SQLite, and MSSQL, and provides robust transaction support, relationships, replica reads, and more.

  • Node ORM2: A Node.js object-relational management system. Support MySQL, SQLite and Progress, can help you operate the database with the object-oriented method.

Mongoose is the most popular ODM and is a reasonable choice when choosing a MongoDB database.

Using Mongoose

Mongoose is a MongoDB object modeling tool designed for asynchronous environments. Mongoose supports Node.js and Deno (alpha).

The official documentation site is mongosejs.com.

Install Mongoose and MongoDB

Mongoose is installed in your project (package.json) using NPM like any other dependency. Please run the following command in the project folder to complete the installation:

npm install mongoose
npm install mongoose

added 24 packages, and audited 150 packages in 20s

10 packages are looking for funding
  run `npm fund` for details

7 vulnerabilities (2 low, 5 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues, run:
  npm audit fix --force

Run `npm audit` for details.

Installing Mongoose adds all dependencies, including the MongoDB database driver, but does not install MongoDB itself. To install the MongoDB server, you can click to download the installer for each operating system to install locally. You can also use a cloud MongoDB instance.

Connect to MongoDB

Connect to MongoDB

Mongoose needs to connect to a MongoDB database. You can require() it and connect to the local database through mongoose.connect(), as follows.

// import mongoose module
const mongoose = require('mongoose');

// Set default mongoose connection
const mongoDB = 'mongodb://127.0.0.1/my_database';
mongoose. connect(mongoDB);
// Let mongoose use the global Promise library
mongoose. Promise = global. Promise;
// get the default connection
const db = mongoose.connection;

// Bind the connection to the error event (to get a hint of a connection error)
db.on('error', console.error.bind(console, 'MongoDB connection error:'));

You can use mongoose.connection to get the default Connection object. Once connected, the Connection instance will fire an open event.

Additional connections can be created using mongoose.createConnection() . This function is consistent with the parameters of connect() (database URI, including host address, database name, port, options, etc.), and returns a Connection object.

Define and add models

Models are defined using the Schema interface. Schema can define the fields stored in each document, as well as the validation requirements and default values of the fields. You can also work with various types of data more easily by defining static and instance helper methods, and you can use virtual properties that don’t exist in the database just like ordinary fields.

The mongoose.model() method “compiles” a schema into a model. Models can then be used to find, create, update, and delete objects of a particular type.

In a MongoDB database, each model maps to a set of documents. These documents contain the field names/schema types defined by the Schema model.

const mongoose = require('mongoose')

const schema = new mongoose.Schema({<!-- -->
    name: {<!-- --> type: String },
    avatar: {<!-- --> type: String },
    banner: {<!-- --> type: String },
    title: {<!-- --> type: String },
    categories: [{<!-- --> type: mongoose.SchemaTypes.ObjectId, ref: 'Category' }],
    scores: {<!-- -->
        difficult: {<!-- --> type: Number },
        skills: {<!-- --> type: Number },
        attack: {<!-- --> type: Number },
        survive: {<!-- --> type: Number },
    },
    skills: [{<!-- -->
        icon: {<!-- --> type: String },
        name: {<!-- --> type: String },
        delay: {<!-- --> type: String },
        cost: {<!-- --> type: String },
        description: {<!-- --> type: String },
        tips: {<!-- --> type: String },
    }],
    items1: [{<!-- --> type: mongoose.SchemaTypes.ObjectId, ref: 'Item' }],
    items2: [{<!-- --> type: mongoose.SchemaTypes.ObjectId, ref: 'Item' }],
    usageTips: {<!-- --> type: String },
    battleTips: {<!-- --> type: String },
    teamTips: {<!-- --> type: String },
    partners: [{<!-- -->
        hero: {<!-- --> type: mongoose.SchemaTypes.ObjectId, ref: 'Hero' },
        description: {<!-- --> type: String },
    }],
})

module.exports = mongoose.model('Hero', schema, 'heroes')

Definition pattern

A simple schema is defined in the code snippet below. First require() mongoose, then use the Schema constructor to create a new schema instance, and use the constructor’s object parameter to define the fields.

// Get Mongoose
const mongoose = require('mongoose');

// define a pattern
var Schema = mongoose. Schema;

var SomeModelSchema = new Schema({<!-- -->
    a_string: String,
    a_date: Date
});

The above example has only two fields (a string and a date), other field types, validation and other methods will be shown next.

Create a model

Create a model from a schema using the mongoose.model() method:

//Definition mode
const Schema = mongoose.Schema;

const SomeModelSchema = new Schema({<!-- -->
    a_string: String,
    a_date: Date
});

// "compile" the model with the schema
const SomeModel = mongoose. model('SomeModel', SomeModelSchema);

The first parameter is the alias of the collection created for the model (Mongoose will create the database collection for the SomeModel model), and the second parameter is the schema used when creating the model.

Once model classes are defined, they can be used to create, update, or delete records, and to query to get all records or a specific subset. We’ll show that in the “Using the Model” section below, including the case of creating a view.

Schema type (field)

A schema can contain any number of fields, each field representing a segment of storage in a MongoDB document. Here is an example of a schema with many common field types and declarations:

const schema = new Schema(
{<!-- -->
  name: String,
  binary: Buffer,
  living: Boolean,
  updated: {<!-- --> type: Date, default: Date.now },
  age: {<!-- --> type: Number, min: 18, max: 65, required: true },
  mixed: Schema.Types.Mixed,
  _someId: Schema.Types.ObjectId,
  array: [],
  ofString: [String], // other types can also use arrays
  nested: {<!-- --> stuff: {<!-- --> type: String, lowercase: true, trim: true } }
})
  • ObjectId: Represents a specific instance of a model in the database. For example, a book might use this to represent its author object. It actually only contains the unique ID (_id) of the specified object. Relevant information can be extracted when needed using the populate() method.

  • Mixed: Any pattern type.

  • []: Array of objects. to perform JavaScript array operations (push, pop, unshift, etc.) on such models. In the above example, there is an array of objects with no specified type and an array of String objects, and the objects in the array can be of any type.

The code also shows two ways of declaring fields:

  • Field names and type names as key-value pairs (just like name, binary, and living).

  • The field name is followed by an object in which to define the type and other options for the field, which can be:
    Defaults.
    Built-in validators (e.g. max/min) and custom validation functions.
    Whether this field is required.
    Whether to automatically convert String fields to lowercase, uppercase, or truncate spaces at both ends

Example {<!-- --> type: String, lowercase: true, trim: true }

Authentication

Mongoose provides built-in and custom validators, as well as synchronous and asynchronous validators. You can specify acceptable ranges or values, and error messages for validation failures, in all cases.

Built-in validators include:

  • All schema types have a built-in required validator. Used to specify whether the current field is required to save the document.
  • Number has numeric range validators min and max.
  • Strings are:
    enum: Specifies the set of allowed values for the current field.
    match: Specifies the regular expression that the string must match.
    The maximum length maxlength and the minimum length minlength of the string

Here’s how to set the type validator and error message

const breakfastSchema = new Schema({<!-- -->
  eggs: {<!-- -->
    type: Number,
    min: [6, 'too few eggs'],
    max: 12
  },
  drink: {<!-- -->
    type: String,
    enum: ['coffee', 'tea']
  }
});

Virtual attribute

Virtual properties are document properties that can be gotten and set, but are not saved to MongoDB. Getters can be used to format or combine fields, while setters can be used to break a single value into multiple values for easy storage. The example in the docs, constructs (and deconstructs) a fullname virtual property from the firstname and lastname fields, which is simpler and cleaner than using the fullname every time in the template.

We’ll use a virtual property in the library to define a unique URL for each model record with the path and the record’s _id.

schema.virtual('children', {<!-- -->
    localField: '_id',
    foreignField: 'parent',
    justOne: false,
    ref: 'Category'
})

Method and query helpers

The pattern supports instance methods, static methods and query helpers. Instance methods and static methods are very similar in appearance, but there are essential differences. Instance methods target specific records and can access the current object. Query helpers can be used to extend Mongoose’s chained query API (for example, you can add a “byName” query outside the find(), findOne(), and findById() methods).

Create and modify documents

Records can be created by defining an instance of the model and calling save(). The following example assumes that SomeModel is a model created with an existing schema (only one field “name” ):

// Create an instance of the SomeModel model
const awesome_instance = new SomeModel({<!-- --> name: 'Awesome' });

// pass callback to save this newly created model instance
awesome_instance. save( function (err) {<!-- -->
  if (err) {<!-- -->
    return handleError(err);
  } // Saved
});

Record creation (and update, delete, and query) operations are asynchronous, and a callback function can be provided to call when the operation completes. Because the API follows the convention that error parameters take precedence, the first argument to the callback must be an error value (or null). If the API needs to return some results, pass the result as the second parameter.

You can also save a model instance while defining it, using create(). The first parameter of the callback returns an error, and the second parameter returns the newly created model instance.

SomeModel.create(
  {<!-- --> name: 'Also great' },
  function(err, awesome_instance) {<!-- -->
    if (err) {<!-- -->
      return handleError(err);
    } // Saved
  }
);

Each model has an associated connection (which is the default when using mongoose.model() ). A document can be created on another database by creating a new connection and calling .model() on it.

Fields in the new record can be accessed and modified using the “dot” followed by the field name. You must call save() or update() after the operation to save the changes back to the database.

// Use dots to access model field values
console.log(awesome_instance.name); // console will display 'Awesome too'

// Modify the field content and call save() to modify the record
awesome_instance.name = "Awesome Instance";
awesome_instance. save( function(err) {<!-- -->
   if (err) {<!-- -->
     return handleError(err);
   } // Saved
});

Search History

Records can be searched using the query method, and the query criteria can be listed in the JSON document. The following code shows how to find all tennis players in the database and return the player name and age fields. Here only one match field (sport, sport) is specified, you can add more conditions, specify regular expressions, or remove all conditions to return all athletes.

const Athlete = mongoose. model('Athlete', yourSchema);

// SELECT name, age FROM Athlete WHERE sport='Tennis'
Athlete. find(
  {<!-- --> 'sport': 'Tennis' },
  'name age',
  function (err, athletes) {<!-- -->
    if (err) {<!-- -->
      return handleError(err);
    } // 'athletes' holds a list of eligible athletes
  }
);

If a callback is specified like in the code above, the query will be executed immediately. The callback will be called when the search is complete.

All callbacks in Mongoose use the callback(error, result) pattern. If an error occurred while querying, the parameter error will contain the error document and result will be null. If the query is successful, error will be null and the query result will be filled into result .

If no callback is specified, the API will return a variable of type Query. This query object can be used to build a query, which is then executed (using a callback) using the exec() method.

http://mongoosejs.com/docs/api.html#query-js

// find all tennis players
const query = Athlete. find({<!-- --> 'sport': 'Tennis' });

// Find name, age two fields
query. select('name age');

// only find the first 5 records
query. limit(5);

// sort by age
query.sort({<!-- --> age: -1 });

// Run the query sometime later
query.exec(function (err, athletes) {<!-- -->
  if (err) {<!-- -->
    return handleError(err);
  } // save the list of tennis players in athletes, sorted by age, 5 records in total
})

The above query conditions are defined in the find() method. You can also use the where() function to do this, and you can use the dot operator (.) to chain all queries together. The following code is basically the same query as above, with the additional condition of age range added.

Athlete.
  find().
  where('sport').equals('Tennis').
  where('age').gt(17).lt(50). // additional WHERE query
  limit(5).
  sort({<!-- --> age: -1 }).
  select('name age').
  exec(callback); // The name of the callback function is callback

The find() method will get all matching records, but usually you only want to get one. The following methods can query a single record:

findById(): Finds a document with the specified id (each document has a unique id).

findOne(): Finds the first document that matches the specified criteria.

findByIdAndRemove(), findByIdAndUpdate(), findOneAndRemove(), findOneAndUpdate(): Find a single document by id or condition, and update or delete it. The above are convenience functions for updating and deleting records.

Collaboration between documents – population

ObjectId schema fields can be used to create one-to-one references between two document/model instances, (a set of ObjectIds can create one-to-many references). This field stores the id of the related model. If you need the actual content of the related document, you can use the populate() method in the query, replacing the id with the actual data.

For example, the following schema defines author and bio. Each author can have multiple profiles, which we represent as an array of ObjectIds. Each profile corresponds to only one author. “ref” tells the schema which model to assign to the field.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const authorSchema = Schema({<!-- -->
  name : String,
  stories : [{<!-- --> type: Schema.Types.ObjectId, ref: 'Story' }]
});

const storySchema = Schema({<!-- -->
  author : {<!-- --> type: Schema.Types.ObjectId, ref: 'Author' },
  title : String
});

const Story = mongoose.model('Story', storySchema);
const Author = mongoose. model('Author', authorSchema);

References to related documents can be kept by assigning an _id value. Below we create an author, a profile, and set the author field of the new profile to the id of the new author.

const wxm = new Author({<!-- --> name: 'Sima Qian' });

wxm. save(function (err) {<!-- -->
  if (err) {<!-- -->
    return handleError(err);
  }

  // Now that the author Sima Qian is in the library, let's create a new profile
  const story = new Story({<!-- -->
    title: "Sima Qian is a historian",
    author: wxm._id // author is set to the _id of the author Sima Qian. IDs are created automatically.
  });

  story.save(function (err) {<!-- -->
    if (err) {<!-- -->
      return handleError(err);
    } // Sima Qian has a profile
  });
});

The profile document now references the author by the ID of the author document. The author information can be obtained in the introduction using populate() as shown below.

Story
  .findOne({<!-- --> title: 'Sima Qian is a historian' })
  .populate('author') // Populate the actual author information with the author id
  .exec(function (err, story) {<!-- -->
    if (err) {<!-- -->
      return handleError(err);
    }
    console.log('The author is %s', story.author.name);
    // The console will print "The author is Sima Qian"
  });

One schema (model) one file

While there are no file structure restrictions for creating schemas and models, it is strongly recommended to define a single schema in a single module (file) and create the model via the export method.

// file: ./models/somemodel.js

// Require Mongoose
const mongoose = require('mongoose');

// define a pattern
const Schema = mongoose.Schema;

const SomeModelSchema = new Schema({<!-- -->
    a_string : String,
    a_date : Date
});

// export function to create "SomeModel" model class
module.exports = mongoose.model('SomeModel', SomeModelSchema );

You can then require and use that model in other files. Here’s how to get all instances from the SomeModel module.

// Create the SomeModel model through the require module
const SomeModel = require('../models/somemodel')

// Use the SomeModel object (model) to find all SomeModel records
SomeModel. find(callback_function);