Skip to content

4 mongoose

Introducción

mongoose es una librería para javascript que nos permite trabajar con conexiones a una base de datos de forma más eficiente y sencilla que el driver nativo. Además, te permite crear esquemas para dicha base de datos lo que te facilita el hecho de salvaguardar la información. Podemos instalar dicha librería con npm:

npm install mongoose

Conexión a MongoDB

Para realizar una conexión a MongoDB se una el método connect(uri, options) que recibe la url de la conexión y opciones del cliente.


Esquemas

Una de las mayores ventajas a la usa mongoose, es que nos permite crear esquemas de los elementos que vamos almacenar. En cada esquema, debemos indicar el nombre de la propiedad así como su tipo:

new mongoose.Schema({
    title: String
})

También se puede indicar el tipo a través de un objeto con las opciones referidas a esa propiedad, para poder añadirle más opciones a parte del tipo, como por ejemplo, su valor por defecto:

new mongoose.Schema({
    title: {type: String, default: ''}
})

¡Importante!

Los esquemas son a nivel de aplicación, pero si se desea realizar una operación en otro back-end, se deberá volver a definir. Incluso, si se realiza operaciones desde el mismo servidor de MongoDB, no tiene porque cumplir el esquema y puedo tener propiedades de diferentes tipos.

Opciones para todos los tipos

Podemos encontrar diferentes tipos de opciones para usar en nuestros esquemas:

  • required: Hace que la propiedad sea requerida. Puede ser de tipo booleano, o de una función que devuelva un booleano.
  • default: Indica un valor por defecto para la propiedad.
  • validate: Es una función que se añade para poder validar la propiedad.
  • get: Define un getter personalizado para la propiedad.
  • set: Define un setter personalizado para la propiedad.
  • alias: Define un alias para poder acceder a la propiedad. Es de tipo String.
  • immutable: Indica que la propiedad es inmutable, por lo que mongoose impedirá su modificación a menos que el documento principal tenga la opción isNew: true
new Schema({
  integerOnly: {
    type: Number,
    get: v => Math.round(v),
    set: v => Math.round(v),
    alias: 'i'
  }
})

Indices

También se pueden definir indices de MongoDB:

  • index: Indica si la propiedad es un índice.
  • unique: Indica si la propiedad es única.

Opciones para cadenas

  • lowercase: si se debe invocar .toLowerCase() para el valor de la propiedad.
  • uppercase: si siempre se debe invocar .toUpperCase() para el valor de la propiedad.
  • trim: si siempre se debe invocar .trim() para el valor de la propiedad.
  • match: verifica si el valor coincide con la expresión regular dada.
  • enum: verifica si el valor está en el array indicado.
  • minLength: verifica si la longitud del valor no es menor que el número dado.
  • maxLength: verifica si la longitud del valor no es mayor que el número dado.

Opciones para números

  • min: verifica si el valor es mayor o igual al mínimo dado.
  • max: verifica si el valor es menor o igual al máximo dado.
  • enum: verifica si el valor es estrictamente igual a uno de los valores en el array indicado.

Opciones para fechas

  • min: verifica si el valor es mayor o igual al mínimo dado.
  • max: verifica si el valor es menor o igual al máximo dado.
  • expires: crea un índice TTL con el valor expresado en segundos.

Mixed Type

El tipo Mixed indica que la propiedad puede ser de cualquier tipo.

Arrays

Para definir una propiedad como un array, se indica el tipo envuelto en corchetes []:

new Schema({
    values: [String]
})

Si nuestro array puede contener diferentes tipos, podemos usar el tipo Mixed:

new Schema({
    values: [Schema.Types.Mixed]
})

Map

El tipo Map es para indicar que la propiedad es un tipo de objeto, por lo que debemos definir un esquema para dicha propiedad:

 new Schema({
  socialMediaHandles: {
    type: Map,
    of: new Schema({
      handle: String,
      oauth: {
        type: ObjectId,
        ref: 'OAuth'
      }
    })
  }
})

Modelos

Al definir un esquema, podemos crear modelos para poder crear objetos con dichos esquemas. Para ello usamos el método model(name, schema) que recibe el nombre del modelo y el esquema a cumplir:

const Blog = mongoose.model('Blog', blogSchema);

Es buena práctica que el nombre de los modelos estén en singular, ya que al crear colecciones automáticas crea dicha colección con el nombre en plural del modelo.

Ids

En el esquema no hace falta referencias el _id ya que se mongoose lo hace de forma automática:

const Model = mongoose.model('Test', schema)

const doc = new Model()
doc._id instanceof mongoose.Types.ObjectId

¡Cuidado!

Aunque si se podría sobre escribir el _id por tu propio _id , no es una buena práctica realizarlo, ya que puede arrojar resultados no deseados:

const schema = new Schema({
  _id: Number
})

const Model = mongoose.model('Test', schema);    
await doc.save(); // (1)!
  1. Lanza una excepción, ya que el documento debe tener un _id antes de ser almacenado.

Nested Schemas

Un esquema puede contar con una propiedad que sigue otro esquema. Por ejemplo, el nuestra colección artista podemos tener una propiedad que sea canciones que representa una lista de canciones, por lo que podemos usar otro esquema:

const nestedSchema = new Schema(
  { name: String },
)

const schema = new Schema({
  subdoc: nestedSchema,
  docArray: [nestedSchema]
})

const Test = mongoose.model('Test', schema);

Queries

El modelo de mongoose dispone de varios métodos para poder realizar operaciones CRUD. Cada una de estas operaciones devuelve un objeto Query. Una query puede ser ejecutada de dos formas, la primera se le puede pasar una callback o se puede usar el método then para ser usadas como promesas. También se puede usar con el await.

Al especificar una query, puedes especificar como un documento JSON o el Shell de MongoDB:

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

const person = await Person.findOne({ 'name.last': 'Ghost' }, 'name occupation')

console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation)

El método exec() me permite ejecutar una query que se ha ido construyendo o modificando sin ser ejecutada todavía. Por ejemplo, hagamos el ejemplo anterior haciendo uso de este método:

const query = Person.findOne({ 'name.last': 'Ghost' }) //(1)!

query.select('name occupation') //(2)!

const person = await query.exec() //(3)!

console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation);
  1. Filtro a las personas, cuyo apellido sea Ghost.
  2. Selecciono el nombre y la ocupación.
  3. Se ejecuta la query.

Además de esta forma se puede concatenar diferentes operaciones en una única query:

await Person
        .find({ 'name.last': 'Ghost' }) // (1)!
        .where('age') // (2)!
        .gt(10) // (3)!
        .exec() // (4)!
  1. Se filtra por apellido.
  2. Se localiza la edad.
  3. Se filtra aquellas edades mayores que 10.
  4. Se ejecuta la query

¡Importante!

Las queries NO son promesas, son thenables. Un thenable son funciones en javascript que pueden usar el método then() para operaciones de async/await según sea conveniente. Sin embargo, a diferencias de las promesas, por lo que no se puede ejecutar then() varias veces. En el caos de las query, si se usa el then después de una ejecución, entonces lanzará un error:

const q = MyModel.updateMany({}, { isDeleted: true });

await q.then(() => console.log('Update 2'));

await q.then(() => console.log('Update 3'));

En la siguiente tabla se recoge los métodos para poder realizar queries:

Nombre Descripción
createCollection([options]) Crea la colección asociada al modelo
deleteMany(condition, [options]) Elimina varios elementos que cumplan la condición
deleteOne(condition, [options]) Elimina el primer elemento que cumpla la condición
distinct(field, [conditions]) Devuelve los diferentes valores que tiene la propiedad en una colección
exists(filter, [options]) Devuelve un documento con _id si existe al menos un elemento que cumpla el filtro
find(filter, [projection], [options]) Localiza todos los documentos que coincidan con el filtro
findById(id, [projection], [option]) Localiza el documento cuya id coincida con el id aportado
findByIdAndDelete(id, [options]) Encuentra el documento que coincida con la id y lo elimina.
findByIdAndUpdate(id, [update], [options]) Encuentra el documento que coincida con la id y lo actualiza
findOne([conditions], [projection], [options]) Encuentra el primer documento que cumpla con las condiciones
findOneAndDelete(conditions, [options]) Encuentra el primer documento que cumpla las condiciones y lo elimina.
findOneAndReplace(filter, [replacement], [options]) Encuentra el primer documento que cumpla las condiciones y lo remplaza.
findOneAndUpdate(filter, [update], [options]) Encuentra el primer documento que cumpla las condiciones y lo actualiza.
insertMany(docs, [options]) Inserta varios documentos
updateMany(filter, update, [options]) Actualiza varios documentos que cumplan el filtro indicado
updateOne(filter, update, [options]) Actualiza el primer documento que cumplan el filtro indicado
where(path, [val]) Retorna una query con los valores indicados

Existen diferentes métodos para poder trabajar con las quieres que se pueden anidar al método where o find:

  • all([path], value): Comprueba si la query coincide con todos los valores aportados:

    MyModel.find().where('pets').all(['dog', 'cat', 'ferret']);
    // Equivalent:
    MyModel.find().all('pets', ['dog', 'cat', 'ferret']);
    
  • and(conditions): Permite concatenar varias condiciones.

  • countDocuments([filter], [options]):

Métodos

Para conocer más métodos puedes visitar la API

Transformaciones

Cuando se realiza la búsqueda de un documento, el servidor devuelve el documento parecido al siguiente:

{
    _id: "ajdkkdfs",
    name: "hola",
    __v: 0
}

Nos gustaría que nos devolvieran el _id como id y que desapareciera _v porque no es importante para nosotros. Podemos hacer uso de la función map para ello. Aunque esto puede suponer problemas porque no el objeto modelo es más complejo ede lo que a simple vista parece. En nuestro modelo, podemos indicar el tipo de transformación a realizar cuando queremos que cambie a JSON:

```javascript schema.set('toJSON', { transform: (document, returnedObject) => { returnedObject.id = returnedObject._id delete document._id delete document.__v } })