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.
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:
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.
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):
- 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. - El valor del campo
email
es devuelto. Observa que está en minúsculas porque especificamos ellowercase:true
atribuido en el esquema. __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 this
se 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:
- Tipo de evento (‘init’, ‘validate’, ‘save’, ‘remove’)
- Una devolución de llamada que se ejecuta con la referencia this a la instancia del modelo.
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:
También te puede gustar mi taller en youtube: Cómo construir una API REST con Node | expreso | Mongo