mongoose

Mongoose, al igual que sequelize es una herramienta ORM (Object–relational mapping, es una técnica de programación para convertir datos entre sistemas de tipos incompatibles usando lenguajes de programación orientados a objetos), basada en promesas de node.js para acceso a bases de datos MongoDB. MongoDB es un servidor de bases de datos NoSQL, basado en documentos. Los documentos relacionados se agrupan en las llamadas colecciones. La terminología es diferente a la usada en la base de datos relacionales. Allí las bases de datos estaban compuestas por tablas, las tablas por filas y cada fila tenía una colección de campos. Todas las filas de una tabla tenían la misma estructura de campos. En MongoDB las tablas son ahora colecciones formadas por documentos que serían el equivalente a las filas en los sistemas relacionales. Cada documento estará formado por una colección de pares clave-valor. A diferencia de las bases de datos relacionales, los documentos de una colección no tienen por qué tener la misma estructura de campos. Cada documento de una misma colección puede tener distintos conjuntos de pares clave-valor.

En mongoose, al igual que en sequelize, lo primero que debemos hacer es conectar con la base de datos.

const mongoose = require("mongoose");
(async() => {
    try {
        const conexion = await mongoose.connect("mongodb://127.0.0.1/biblioteca");
        console.log("conectado a biblioteca");
    } catch (error) {
        console.log("error en acceso a la base de datos: " + error);
    }
})();    

La cadena de conexión1 tiene el siguiente formato:

mongodb://[username:password@]host1[:port1][,...hostN[:portN]]
    [/[defaultauthdb][?options]]    

En mongoose, tras la conexión a la base de datos, las colecciones quedan representadas por un schema. Un schema representa por tanto la estructura de una colección en la base de datos. En el siguiente ejemplo vemos la definición de un schema para una colección de libros:

const librosSchema = new mongoose.Schema({
            titulo: String,
            autor: String,
            editorial: String,
            fechaPublicacion: { type: Date, default: Date.now },
        });    

Cada clave define una propiedad del documento que no es más que un objeto de tipo SchemaType que la define. Una clave es, por tanto, un objeto, que además de definir el tipo de dato que va a almacenar, permite también, por ejemplo, definir cual es el valor por defecto, añadirle una función de validación personalizada, indicar si el campo es requerido, definir getter y setter para el campo o crear índices para permitir que los datos se obtengan más rápido. También permite indicar en los campos de tipo String si se guarda en mayúsculas o minúsculas, en los campos de tipo Number o Date indicar un máximo y un mínimo, etc. Tenemos los siguientes SchemaTypes2

A partir del schema tenemos que definir un model que no es más que una clase que nos va a permitir construir document. Al instanciar el model indicamos, en el primer argumento del constructor, el nombre de la colección en la base de datos y como segundo argumento especificamos el schema que define la estructura de los documentos. Por ejemplo, para el schema anterior:

const libro = new mongoose.model("libros",librosSchema);    

A partir de ahora ya podemos crear nuevos documentos, o recuperar los que ya existan en la colección y modificarlos o borrarlos. Por ejemplo, el siguiente código crea un nuevo libro.

const mongoose = require("mongoose");
(async() => {
    try {
        const conexion = await mongoose.connect("mongodb://127.0.0.1/biblioteca");
        console.log("conectado a biblioteca");
        const librosSchema = new mongoose.Schema({
            titulo: String,
            autor: String,
            editorial: String,
            fechaPublicacion: { type: Date, default: Date.now },
        });

        const libro = new mongoose.model("libros", librosSchema);
        var ejemplar = new libro({
            titulo: "La vuelta al mundo en 80 días",
            autor: "Julio Verne",
            editorial: "Nueva Frontera",
            fechaPublicacion: new Date(1995, 0, 1)
        });
        await ejemplar.save();
        console.log("libro añadido");
    } catch (error) {
        console.log("error en acceso a la base de datos: " + error);
    }
})();    

Si ejecutamos el código anterior y veremos con un cliente de MongoDB (por ejemplo Robo 3T) que la base de datos biblioteca ya ha sido creada.

Veremos la colección libros, ya creada, y con un documento en su interior. Veremos que han aparecido dos campos nuevos que no estaban especificados en el schema. EL primero, _id, es el campo clave principal del documento que lo identifica de forma única. Los primeros dígitos de este campo indican el momento Unix en que fue creado por lo que si recuperamos los documentos ordenados por este campo los obtendremos en orden temporal. El segundo campo añadido, __v, es el campo de versión del documento y es gestionado internamente por MongoDB. A ninguno de los dos necesitaremos acceder directamente para su modificación.

La clase del modelo dispone de numerosos métodos, tanto estáticos de la clase, como de la instancia para realizar la gestión de los documentos.

El siguiente ejemplo utiliza el método estático create para añadir dos documentos. Este método es el equivalente a haber creado dos instancias del modelo y ejecutado el método save en cada una de ellas. El argumento puede ser un array de objetos o un simple objeto que se corresponda con el modelo.

libro.create([{
        titulo: "La Isla misteriosa",
        autor: "Julio Verne",
        editorial: "Nueva Vista"
    },
    {
        titulo: "10000 leguas de viaje submarino",
        autor: "Julio Verne",
        editorial: "Nueva Vista",
        fechaPublicacion: (new Date(2008, 0, 1))
    }
])    

Hay varios métodos para la búsqueda de documentos. El siguiente ejemplo recupera todos los documentos de la editorial Nueva Vista

const datos = await libro.find({ titulo: "Nueva Vista" });
datos.map((casilla => {
        console.log(casilla.titulo);
    }))    

Y el siguiente todos los documentos cuyo título empiece por C o mayor:

const datos = await libro.find({ titulo: {$gte: "C"} });
datos.map((casilla => {
      console.log(casilla.titulo);
}))    

Casi todos los métodos de la clase Model son solicitudes asíncronas, pero no son Promises. Tenemos dos alternativas, o utilizar la solicitud await, como se ha hecho en los ejemplos anteriores, o acompañar como argumento del método una función de callback que será invocada al recibir los datos. No se utilizará la metodología .then .error de las promesas.

El anterior ejemplo, haciendo uso de una función de callback quedaría:

libro.find({ titulo: { $gte: "C" } }, (err, datos) => {
datos.map((casilla => {
       console.log(casilla.titulo);
     }))
})    

La sintaxis para establecer los filtros en las búsquedas y aquellas otras operaciones CRUD que admiten filtros, por ejemplo, update o delete se rigen por la sintaxis de MongoDB. Los operadores que se pueden utilizar se pueden consultar en https://docs.mongodb.com/manual/reference/operator/query/.

El siguiente código modifica la editorial Nueva Vista por Nuevo Horizonte en todos los documentos:

let modificados = await libro.updateMany({ editorial: "Nueva Vista" }, 
                                            { editorial: "Nuevo Horizonte" });
console.log("modificados " + modificados.modifiedCount + " libros");    

Ya se ha comentado que mongoose admite la validación del contenido de los campos antes de ser almacenados en la base de datos. La validación se especifica en el schema, y se ejecuta antes del guardado físico. Se puede desactivar la validación automática modificando la opción validateBeforeSave

librosSchema.set('validateBeforeSave', false);    

Se puede invocar la validación de forma manual invocando el método validate o validateSync del documento.

libro.validate(function (err) {
  if (err) 
     	console.log(“error “ + err);
  else
 	console.log(“validación superada”)
});    

Se puede marcar un campo como inválido con el método del documento invalidate haciendo por tanto que la validación fracase.

libro.invalidate('titulo', debe tener al menos 3 caracteres, 14);    

mongoose dispone de varios validadores built-in. Todos los SchemaTypes disponen del validador required. Los Number y los Date tienen los validadores min y max. Los String< los validadores enum, match, minlength y maxlength. En el siguiente ejemplo se ven todas estas opciones.

const mongoose = require("mongoose");

(async() => {
    try {
        const conexion = await mongoose.connect("mongodb://127.0.0.1/biblioteca");

        const lectorSchema = new mongoose.Schema({
            nif: {
                type: String,
                required: true,
                validate: {
                    validator: (dato) => {
                     return /^(\d{8})([A-HJ-NP-TV-Z])$/.test(dato) && 
                     ("TRWAGMYFPDXBNJZSQVHLCKE" [(RegExp.$1 % 23)] == RegExp.$2);
                    },
                    message: "nif incorrecto. Debe contener 8 dígitos y 
                                                            una letra mayúscula"
                }
            },
            nombre: {
                type: String,
                required: true,
                minlength: [2, "longitud mínima 2"],
                maxlength: 30
            },
            apellido1: {
                type: String,
                required: [function() {
                    return ((this.apellido2) ? true : false)
                }, "se requiere si hay segundo apellido"],
                maxlength: 50
            },
            apellido2: {
                type: String,
                maxlength: 50,
            },
            tipo: {
                type: String,
                enum: { values: ["alumno", "profesor"], 
                        message: "Solo se permite alumno o profesor" }
            },
            grupo: {
                type: String,
                match: [/^(ESO|BCH)[1-4][A-D]$/, 
                        "debe seguir el formato ESO o BCH seguido del 
                                número de curso y la letra del grupo"],
                required: [function() {
                    return ((this.tipo == "alumno") ? true : false)
                }, "se requiere si el tipo de lector es alumno"],
            },
            fecha: {
                type: Date,
                min: [new Date(1900, 0, 1), 
                        "La fecha debe ser posterior al 1 de enero de 1900"],
                max: [new Date(2100, 11, 31),
                        "La fecha no puede ser posterior al 31 de diciembre de 2100"],
                default: new Date()
            }
        });
        const lector = new mongoose.model("lector", lectorSchema);
        try {
            await lector.create({
                nif: "00000000T",
                nombre: "Javier",
                apellido1: "Gómez",
                apellido2: "García",
                fecha: new Date(2100, 1, 20),
                tipo: "alumno",
                grupo: "ESO3B"
            });
            console.log("lector añadido");
        } catch (error) {
            console.log("error al añadir lector " + error.message);
        }

    } catch (error) {
        console.log("error en acceso a la base de datos: " + error);
    }
})();    

El nif debe ser un nif válido de un ciudadano español y es campo requerido. A través de validate y un validator se ejecuta una función que comprueba que el nif es correcto. Esta función devuelve true si es correcto con lo que el validator se dará por correcto y el contenido se almacenará en la base de datos. En caso contrario se desencadenará un error en tiempo de ejecución acompañado del mensaje expresado por message<, que podrá ser capturado en una estructura try… catch.

El nombre tiene restricciones de longitud máxima y mínima además de ser campo requerido. Nótese como se ha indicado el mensaje de error: minlength: [2, "longitud mínima 2"]

El apellido1 tiene también restricción de longitud máxima y será requerido si el campo apellido2 no está vacío, es decir, si hay apellido2 el campo apellido1 no puede estar vacío. Véase como en el validador required se ha indicado una función en lugar de un valor true o false. Si dentro de la función se va a hacer referencia a otro campo del modelo se utiliza la palabra reservada this. Se debe tener cuidado de no declarar la función como función arrow porque si no la referencia a this no será a la instancia del modelo.

tipo es un tipo enumerado con dos únicos valores posibles: alumno y profesor.

El grupo debe tener un contenido que se ajuste a la expresión regular que se indica y además es un campo requerido si en el campo tipo hay un alumno.

Por último, el campo fecha tiene un valor por defecto y unos valores máximo y mínimo. Al tener un valor por defecto este campo siempre aparecerá en todos los documentos de la colección. Aquellos campos que no son requeridos no aparecerán en los documentos en donde no se les dio contenido.

Almacén de sesiones en MongoDB con mongoose

Ya vimos que con la propiedad store del middleware express-session, podíamos indicar el almacén en donde guardar la información de las sesiones de los clientes. Por defecto es MemoryStore pero se puede indicar como almacén casi cualquier servidor de base de datos, incluido MongoDB. Para hacerlo fácilmente disponemos del middleware connect-mongo3 . Este middleware dispone del método create que nos devuelve directamente el almacén en el que guardar la información de las sesiones. Este método permite indicar la cadena de conexión a la base de datos y el nombre de la colección en la que guardará la información de las sesiones. Se creará una nueva conexión.

Normalmente no desearemos una nueva conexión al servidor, ya que tendremos ya creada una conexión a la base de datos para la funcionalidad de la propia aplicación en donde tendremos almacenada la información que se necesita. En este caso al método create del middleware connect-mongo, le deberemos pasar un Client de la conexión ya existente.

En el siguiente ejemplo se ve como se obtiene un Client de la conexión y se utiliza en connect-mongo. El ejemplo es el mismo que se vió en la explicación de las sesiones, que contaba el número de visitas durante la sesión. En este caso se hace funcionar el servidor en modo https con certificado y como almacén de la información de las sesiones se utiliza la colección sesiones en la base de datos datos que son creadas si no existieran.

const express = require('express');
const sesion = require('express-session');
const app = express();

const fs = require("fs");
const https = require("https");

const mongoose = require("mongoose");
const MongoStore = require("connect-mongo");

(async() => {
    try {
        const conexion = await mongoose.connect("mongodb://127.0.0.1/datos");
        const cliente = mongoose.connection.getClient();

        app.use(sesion({
            cookie: {
                signed: true,
                secure: true
            },
            secret: "a3A$d23jU@",
            name: "chj",
            resave: true,
            saveUninitialized: true,
            store: MongoStore.create({ client: cliente, collectionName: "sesiones" })
        }));

        app.get("/", (req, res) => {
            req.session.veces = (req.session.veces) ? (req.session.veces + 1) : 1;
            res.send("veces: " + req.session.veces);
        });

        app.use((req, res, next) => {
            res.send(404, "recurso no encontrado"); // termina
        });

        https.createServer({
            key: fs.readFileSync('certificado.key'),
            cert: fs.readFileSync('certificado.crt')
        }, app).listen(8080, function() {
            console.log("servidor HTTPS funcionando en puerto 8080 ...");
        });

    } catch (error) {
        console.log("error en acceso a la base de datos: " + error);
    }
})();    

(1)https://docs.mongodb.com/manual/reference/connection-string/

(2)https://mongoosejs.com/docs/schematypes.html#schematypes

(3)https://www.npmjs.com/package/connect-mongo

e-mail:manjarrés