Original article: Introduction to Mongoose for MongoDB

Mongoose es una biblioteca de modelado de datos orientada a objetos (ODM) para MongoDB y Node.js. Administra las relaciones entre los datos, proporciona validación de esquemas y se utiliza para traducir entre objetos en el código y la representación de esos objetos en MongoDB.

image
Mapeo de objetos entre Node y MongoDB administrado a través de Mongoose

MongoDB es una base de datos de documentos NoSQL sin esquema. Significa que puede almacenar documentos JSON en él, y la estructura de estos documentos puede variar,  porque que no se aplica como las bases de datos SQL. Esta es una de las ventajas de usar NoSQL, ya que acelera el desarrollo de aplicaciones y reduce la complejidad de las implementaciones.

A continuación se muestra un ejemplo de cómo se almacenan los datos en Mongo vs. SQL Database:

image-1
image-2
Documentos NoSQL vs. Tablas Relacionales en SQL

Terminologías

Colecciones

Las "colecciones" en Mongo son equivalentes a las tablas en las bases de datos relacionales. Pueden contener varios documentos JSON.

Documentos

Los "documentos" son equivalentes a registros o filas de datos en SQL. Si bien una fila de SQL puede hacer referencia a datos en otras tablas, los documentos de Mongo generalmente combinan eso en un documento.

Campos

Los "campos" o atributos son similares a las columnas de una tabla SQL.

Esquema

Si bien Mongo no tiene esquema, SQL define un esquema a través de la definición de la tabla. Un "esquema" de Mongoose es una estructura de datos del documento (o la forma del documento) que se aplica a través de la capa de aplicación.

Modelos

Los "modelos" son constructores de orden superior que toman un esquema y crean una instancia de un documento equivalente a los registros en una base de datos relacional.

Ejemplo
Aquí hay un pequeño fragmento de código para ilustrar parte de la terminología anterior:

const puppySchema = new mongoose.Schema({
  name: {
    type: String,
    required: true
  },
  age: Number
});

const Puppy = mongoose.model('Puppy', puppySchema);

En el código anterior, puppySchema se define la forma del documento. Este tiene dos campos nombre y edad, name ,age.

Usamos SchemaType para name como un String mientras que para la variable age empleamos Number (debido a que la edad denota un número específico). Observa que cuando definimos SchemaType para un campo, por medio de usar un objeto con la propiedad  type igual a la de name. O bien, puedes aplicar SchemaType directamente al campo age.

También observa que SchemaType para name tiene la opción required ajustada a un valor booleano true. Para usar opciones como required y lowercase en un campo, necesitas emplear un objeto específico que te ayude a definir el SchemaType.

En la parte inferior del fragmento, puppySchema se compila en un modelo llamado  Puppy, que luego se puede usar para construir documentos en una aplicación.

Empezando

Instalación de Mongodb

Antes de comenzar, configuremos Mongo. Puedes elegir entre una de las siguientes opciones (estamos usando la opción n.º 1 para este artículo):

1.Descarga la versión más apropiada de MongoDB version para tu sistema operativo del Website de MongoDB, y sigue las correspondientes instrucciones de instalación.

2.Crea una base de datos sandbox gratuita mediante una suscripción en mLab.

3. Instala Mongo usando Docker si prefieres usar Docker.

Naveguemos a través de algunos de los conceptos básicos de Mongoose implementando un modelo que representa datos para una libreta de direcciones simplificada.
Estoy usando Visual Studio Code, Node 8.9 y NPM 5.6. Enciende tu IDE favorito, crea un proyecto en blanco y ¡comencemos! Usaremos la sintaxis ES6 limitada en Node, por lo que no configuraremos Babel.

NPM Install

Vayamos al archivo del proyecto para darle un inicio.

npm init -y

Instalamos Mongoose junto con una biblioteca de validación con el siguiente comando:

npm install mongoose validator

El comendo anterior instalará la última versión de las bibliotecas. La sintaxis de Mongoose en este artículo es específica desde Mongoose v5 en adelante.

Conección a la Base de Datos

Crea un archivo  ./src/database.js bajo la raíz del proyecto.

A continuación, añadiremos una clase simple con un método que se conecta a la base de datos.
La cadena de conexión variará según los detalles de tu instalación.

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()

La llamada require(‘mongoose’) retorna un objeto Singleton. Lo que significa que la primera vez que llames require(‘mongoose’), está creando una instancia de la clase Mongoose y devolviéndola. En llamadas posteriores, devolverá la misma instancia que se creó y se le devolvió la primera vez debido a cómo funciona la importación/exportación de módulos en ES6.

0*RvVsD_byUakUzuCj
Module import/require work-flow

De manera similar, hemos convertido nuestra clase de base de datos en un singleton al devolver una instancia de la clase en la declaración module.exports porque solo necesitamos una única conexión a la base de datos.

ES6 hace que sea muy fácil para nosotros crear un patrón singleton (instancia única) debido a cómo funciona el cargador de módulos al almacenar en caché la respuesta de un archivo previamente importado.

Esquema Mongoose vs. Modelo

Un modelo Mongoose es un contenedor en el esquema Mongoose. Un esquema Mongoose define la estructura del documento, valores predeterminados, validadores, etc., mientras que un modelo Mongoose proporciona una interfaz a la base de datos para crear, consultar, actualizar, eliminar registros, etc.

La creación de un modelo Mongoose consta principalmente de tres partes:

1. Referenciando Mongoose

let mongoose = require('mongoose')

Esta referencia será la misma que se devolvió cuando nos conectamos a la base de datos, lo que significa que las definiciones de esquema y modelo no necesitarán conectarse explícitamente a la base de datos.

2. Definiendo el esquema

Un esquema define las propiedades del documento a través de un objeto donde el nombre de la clave corresponde al nombre de la propiedad en la colección.

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

Aquí definimos una propiedad llamada correo electrónico con un tipo de esquema String que se asigna a un validador interno que se activará cuando el modelo se guarde en la base de datos. Fallará si el tipo de datos del valor no es un String.

A nivel general, los siguientes tipos de esquemas están permitidos en Mogoose:

  • Array (Arreglo)
  • Boolean (Booleano)
  • Buffer
  • Date (Fecha)
  • Mixto (Genérico/ tipo de dato flexible)
  • Number (Número)
  • ObjectId
  • String

Mixed y ObjectId están definidas mediante require(‘mongoose’).Schema.Types.

3. Exportando un modelo

Necesitamos llamar al constructor del modelo en la instancia de Mongoose y pasarle el nombre de la colección y una referencia a la definición del esquema.

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

Resumamos el código de arriba en ./src/models/email.js . Esto a fin de definir los contenidos de un modelo de email básico:

let mongoose = require('mongoose')

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

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

La definición de un esquema debe ser simple, pero su complejidad generalmente se basa en los requisitos de la aplicación. Los esquemas se pueden reutilizar y también pueden contener varios esquemas secundarios.

En el ejemplo anterior, el valor de la propiedad de correo electrónico es un tipo de valor simple. Sin embargo, también puede ser un tipo de objeto con propiedades adicionales.
Podemos crear una instancia del modelo que definimos anteriormente y llenarlo usando la siguiente sintaxis:

let EmailModel = require('./email')

let msg = new EmailModel({
  email: 'ada.lovelace@gmail.com'
})

Mejoremos el esquema de correo electrónico para hacer que la propiedad de correo electrónico sea un campo único y obligatorio y convertir el valor a minúsculas antes de guardarlo. También podemos agregar una función de validación que garantizará que el valor sea una dirección de correo electrónico válida. Haremos referencia y usaremos la biblioteca de validación instalada anteriormente.

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)

Operaciones Básicas

Mongoose tiene una API flexible y proporciona muchas formas de realizar una tarea. No nos centraremos en las variaciones porque están fuera del alcance de este artículo, pero recuerde que la mayoría de las operaciones se pueden realizar de más de una forma, ya sea sintácticamente o mediante la arquitectura de la aplicación.

Crear un registro

Vamos a crear una instancia del modelo de correo electrónico y guardarlo en la base de datos:

let EmailModel = require('./email')

let msg = new EmailModel({
  email: 'ADA.LOVELACE@GMAIL.COM'
})

msg.save()
   .then(doc => {
     console.log(doc)
   })
   .catch(err => {
     console.error(err)
   })

El resultado es un documento que se devuelve al guardar correctamente:

{ 
  _id: 5a78fe3e2f44ba8f85a2409a,
  email: 'ada.lovelace@gmail.com',
  __v: 0 
}

Se devuelven los siguientes campos (los campos internos tienen un prefijo con un guión bajo):

  1. El campo _id es generado automáticamente por Mongo y es una clave principal de la colección. Su valor es un identificador único para el documento.
  2. El valor del campo email es devuelto. Observa que está en minúsculas porque especificamos el lowercase:true atribuido en el esquema.
  3. __v es la propiedad versionKey establecida en cada documento cuando Mongoose lo crea por primera vez. Su valor contiene la revisión interna del documento.

Si intentas repetir la operación de guardar anterior, obtendrás un error porque hemos especificado que el campo de correo electrónico debe ser único.

Obtener un registro

Intentemos recuperar el registro que guardamos en la base de datos anteriormente. La clase modelo expone varios métodos estáticos y de instancia para realizar operaciones en la base de datos. Ahora intentaremos encontrar el registro que creamos previamente usando el método de búsqueda y pasar el correo electrónico como término de búsqueda.

EmailModel
  .find({
    email: 'ada.lovelace@gmail.com'   // search query
  })
  .then(doc => {
    console.log(doc)
  })
  .catch(err => {
    console.error(err)
  })

El documento devuelto será similar al que se mostró cuando creamos el registro:

{ 
  _id: 5a78fe3e2f44ba8f85a2409a,
  email: 'ada.lovelace@gmail.com',
  __v: 0 
}

Actualizar Registro

Modifiquemos el registro anterior cambiando la dirección de correo electrónico y agregando otro campo, todo en una sola operación. Por motivos de rendimiento, Mongoose no devolverá el documento actualizado, por lo que debemos pasar un parámetro adicional para solicitarlo:

EmailModel
  .findOneAndUpdate(
    {
      email: 'ada.lovelace@gmail.com'  // search query
    }, 
    {
      email: 'theoutlander@live.com'   // field:values to update
    },
    {
      new: true,                       // return updated doc
      runValidators: true              // validate before update
    })
  .then(doc => {
    console.log(doc)
  })
  .catch(err => {
    console.error(err)
  })

El documento devuelto contendrá el correo electrónico actualizado:

{ 
  _id: 5a78fe3e2f44ba8f85a2409a,
  email: 'theoutlander@live.com',
  __v: 0 
}

Borrar registro Record

Usaremos findOneAndRemove para borrar un registro. Este retorna el documento original que ha sido removido:

EmailModel
  .findOneAndRemove({
    email: 'theoutlander@live.com'
  })
  .then(response => {
    console.log(response)
  })
  .catch(err => {
    console.error(err)
  })

Ayudas

Hemos analizado algunas de las funciones básicas anteriores conocidas como operaciones CRUD (Crear, Leer, Actualizar, Eliminar), pero Mongoose también brinda la capacidad de configurar varios tipos de métodos auxiliares y propiedades. Estos se pueden utilizar para simplificar aún más el trabajo con datos.

Creemos un esquema ./src/models/user.js con los camposfirstName y lastName:

let mongoose = require('mongoose')

let userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String
})

module.exports = mongoose.model('User', userSchema)

Propiedad Virtual

Una propiedad virtual no se conserva en la base de datos. Podemos agregarlo a nuestro esquema como ayuda para obtener y establecer valores.

Creemos una propiedad virtual fullName que puede ser empleada para asignar valores en firstName y lastName y recuperarlos como un valor combinado cuando se lea:

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]
})

Las devoluciones de llamada para get y set (en el código) deben usar la palabra clave function, ya que necesitamos acceder al modelo a través de la palabra clave this. El uso de funciones de flecha gruesa cambiará a lo que thisse refiere.

Ahora podemos definir firstName y lastName asignándole un valor a 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

El resultado del código anterior es el siguiente:

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

Métodos de instancia

Podemos crear métodos auxiliares personalizados en el esquema y acceder a ellos a través de la instancia del modelo. Estos métodos tendrán acceso al objeto modelo y se pueden usar de manera bastante creativa. Por ejemplo, podríamos crear un método para encontrar a todas las personas que tienen el mismo nombre que la instancia actual.

En este ejemplo, vamos a crear una función para devolver las iniciales del usuario actual. Agreguemos un método auxiliar personalizado llamado getInitials al esquema:

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

Se podrá acceder a este método a través de una instancia de modelo:

let model = new UserModel({
  firstName: 'Thomas',
  lastName: 'Anderson'
})

let initials = model.getInitials()

console.log(initials) // This will output: TA

Métodos estáticos

Similar a los métodos de instancia, podemos crear métodos estáticos en el esquema. Vamos a crear un método para recuperar todos los usuarios en la base de datos:

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

Usando getUsers en la clase Modelo devolverá todos los usuarios en la base de datos:

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

Agregar instancias y métodos estáticos es un buen enfoque para implementar una interfaz para las interacciones de la base de datos en colecciones y registros.

Middleware

En este contexto Middleware son funciones que se ejecutan en etapas específicas de una canalización. Mongoose admite middleware para las siguientes operaciones:Aggregate

  • Document
  • Model
  • Query

Por ejemplo, los modelos tienen funciones pre y post que toman dos parámetros:

  1. Tipo de evento (‘init’, ‘validate’, ‘save’, ‘remove’)
  2. Una devolución de llamada que se ejecuta con la referencia this a la instancia del modelo.
0*iZwmyy25FSxuxXlH
Ejemplo de Middleware (También conocidos como "ganchos" pre y post )

Probemos un ejemplo agregando dos campos llamados createdAt y updatedAt a nuestro esquema:

let mongoose = require('mongoose')

let userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  createdAt: Date,
  updatedAt: Date
})

module.exports = mongoose.model('User', userSchema)

Cuando model.save() es llamado, existe un evento pre(‘save’, …) y un evento post(‘save’, …) que es activado. Para el segundo parámetro, puedes pasar una función que se llama cuando se activa el evento. Estas funciones llevan un parámetro a la siguiente función en la cadena de middleware.

Agreguemos un gancho pre-guardado y establezcamos valores para 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()    
})

Creemos y guardemos nuestro modelo:

let UserModel = require('./user')

let model = new UserModel({
  fullName: 'Thomas Anderson'
}

msg.save()
   .then(doc => {
     console.log(doc)
   })
   .catch(err => {
     console.error(err)
   })

Deberías ver los valores para createdAt y updatedAt cuando el registro que has creado se imprime:

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

Complementos (Plugins)

Supongamos que queremos rastrear cuándo se creó un registro y cuándo se actualizó por última vez en cada colección de nuestra base de datos. En lugar de repetir el proceso anterior, podemos crear un complemento y aplicarlo a cada esquema.

Creemos un archivo ./src/model/plugins/timestamp.js replicando la funcionalidad anterior como un módulo reusable:

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()    
  })
}

Para usar este complemento, simplemente lo pasamos a los esquemas que debería tener esta funcionalidad:

let timestampPlugin = require('./plugins/timestamp')

emailSchema.plugin(timestampPlugin)
userSchema.plugin(timestampPlugin)

Construcción de Consultas

Mongoose tiene una API muy abundante que maneja muchas operaciones complejas compatibles con MongoDB. Considera una consulta en la que podamos crear componentes de consulta de forma incremental.
En este ejemplo, vamos a:

1.Buscar todos los usuarios
2.Omitir los primeros 100 registros
3.Limitar los resultados a 10 registros
4.Ordenar los resultados por el campo firstName
5.Seleccionar el nombre
6.Ejecutar esa consulta

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)
          })

Cierre

Apenas hemos arañado la superficie explorando algunas de las capacidades de Mongoose. Es una abundante biblioteca llena de funciones útiles y potentes que hacen que sea un placer trabajar con modelos de datos en la capa de aplicación.
Si bien puede interactuar con Mongo directamente usando Mongo Driver, Mongoose simplificará esa interacción al permitirle modelar relaciones entre datos y validarlos fácilmente.

Dato curioso: Mongoose fue creado por Valeri Karpov, ¡Un ingeniero increíblemente talentoso! Él acuñó el término The MEAN Stack.

Si este artículo te ha sido útil, sígueme en Twitter:

image-3

También te puede gustar mi taller en youtube: Cómo construir una API REST con Node | expreso | Mongo