MongoDB

MongoDB es una base de datos no relacional o NoSQL que proporciona un alto rendimiento, disponibilidad y escalado.

En MongoDB todo gira en torno a documentos que serían los equivalentes a las filas de una tabla en un sistema relacional. El equivalente a las tablas serían las colecciones salvo que estas son flexibles y pueden almacenar diferentes estructuras de datos.

Operaciones

Crear base de datos

use pruebas1;

Añadir colección a la base de datos

db.usuarios.save({nombre 'DevSeo', web: 'www.devseo.xyz'});

Ver las bases de datos creadas

show dbs;

Ver las colecciones

show collections;

Buscar documentos en la base de datos

db.usuarios.find();

Exportar base de datos

C:\Program Files\MongoDB\Server\4.2\bin>mongodump.exe --db nombreBaseDatos

Importar datos

Desde la shell

Mongo admite scripts JavaScript por lo que podemos insertar datos mediante código

for (i = 0; i & lt; 10; ++i) {  
	var doc = {  
		name: "user-" + i;  
	}

	db.posts.insert(doc);  
}  

Cargar script externo

load("ruta/al/scripts/myScript.js";)

Importar base de datos mongo

Ejecutar desde la terminal:

C:\Program Files\MongoDB\Server\4.2\bin>mongorestore.exe --drop --db nombreBaseDatos archivoAImportar\dump\off

Importar archivo que con datos JSON

Ejecutar desde la terminal:

mongoimport.exe -h host:port -d database -c collection -u username -p password --ssl --jsonArray --file "C:\Ruta\Al\Fichero\json.json"

Desde código

var MongoClient = require('mongodb').MongoClient;

MongoClient.connect('mongodb://localhost:27017/testdb, function(err, db) {  
	if (err) throw err;  
	var query = {  
		'grade': 100  
	};

	function callback(err, doc) {  
		if (err) throw err;  
		console.dir(doc);  
		db.close();
	}  
});  

Insertar documentos

Podemos omitir el atributo _id y mongo generará automáticamente uno único para el documento.

Además podemos variar la estructura de datos.

> db.users.insert({ _id: 1, username : "Pedro", edad: 50, casado: true })
> db.users.insert({ _id: 2, username : "Juan", edad: 80, casado: false })
> db.users.insert({ _id: 3, username : "María", edad: 15 })

Crear colección de forma explícita

Las colecciones se crean de forma automática al insertar datos pero podemos crearlas de forma explícita para configurar ciertos datos.

Por ejemplo, podemos crear colecciones en las que se eliminarán automáticamente los registros antiguos al insertar los nuevos cuando se alcanza un límite.

> db.createCollection("users", { capped: true, max: 1000 })

Listar todos los documentos

> db.users.find()

Listar un documento al azar

> db.users.findOne()

Listar elementos por el valor de sus atributos

> db.users.find({ edad: 50 })
> db.comments.find({date: ISODate("2020-09-24T00:00:00Z")});

Listar por rango de valores

> db.users.find({ edad: { $gt : 20, $lte: 50 }})

Listar por múltiples condiciones

> db.users.find({ 
edad: { $gt : 20 }, 
casado: true
})

Listar elementos que no incluyen un atributo

> db.users.find({ casado: { $exists : false } })

Listar filtrando por expresiones regulares

> db.users.find({ username : { $regex : "Mar" }})

Filtrar elementos por tipo de dato

Ver aquí todos los tipos de datos.

> db.users.find({ casado: { $type : 8 }})

Agrupar dos consultar con un OR

> db.users.find({ $or : [ { _id : 1 }, { _id : 2 } ] })

Consultar el número de documentos de una colección

> db.users.count()
> db.users.count({ casado: true })

Filtrar coincidencias en un array u objetos embebidos

> db.users.find({ hobbies : "futbol" }).pretty()
> db.users.find({ hobbies : { $all: ["futbol", "padel"] }}).pretty()
> db.users.find({ hobbies : { $in: ["futbol"] }}).pretty()
> db.users.find({ "address.city": "Asturias" }).pretty()

Paginar los resultados

> db.users.find().skip(2).limit(2)

Ordenar resultados

> db.users.find().sort({ _id : 1 }).limit(2)

Iterar con cursores

> var c = db.users.find().sort({ username : -1 }).limit(2)
> c.hasNext()
> c.next()

Operación de agregación

Ver etapas admitidas en las operaciones de agregación.

Limitaciones de las funciones de agregación

  • Existe un límite de 100mb de tamaño de pipeline para cada etapa durante el procesado. Se puede saltar esta restricción con allowDiskUse pero ralentizará la consulta.
  • Existe un límite de 16mb de tamaño para la salida de cada pipeline. Se puede evitar esto obteniendo un cursor y recorriendo el cursor elemento a elemento.
  • Sharding: cuando se usa group o sort todos los datos se reúnen en un solo shard (el principal) antes de continuar procesando pipes. Esto se puede mejorar importando el mongo en Hadoop y usar mapreduce ya que tiene mejor escalabilidad.
👉  Cómo apagar la Raspberry Pi con un botón: guía paso a paso

Avg

El siguiente ejemplo realiza una función de agregación, un sumatorio, para todos los registros del documento orders que cumplen la condición (tienen status = A). La suma se realiza a partir del campo amount dando como resultado un nuevo objeto con dos propiedades: id y total.

db.orders.aggregate([
   { $match: { status: "A" } },
   { $group: { _id: "$cust_id", total: { $sum: "$amount" } } }
])

Actulizar datos

Se actualiza el elemento que coincide con la búsqueda. Si no existe, se añade.

> db.users.update({ _id : 1 }, { $set : { casado: false }})

Eliminar un atributo

> db.users.update({ _id : 1 }, { $unset : { cassado: 1 }})

Actualización masiva de datos. Hay que indicar multi si no solo actualizará la primera coincidencia.

> db.users.update({}, { $set : { casado: true }}, { multi : true })

Actualizaciones de atributos de tipo array

> db.users.update({ _id:4 }, { $set: { hobbies : "baloncesto" }})
> db.users.update({ _id:4 }, { $push: { hobbies : "caza" }})
> db.users.update({ _id : 4 }, { $addToSet : { hobbies: "cazar"; }}) // añade solo si no existe
> db.users.update({ _id : 4 }, { $pushAll : { hobbies: [ "baloncesto", "caza" ]}})
> db.users.update({ _id:4 }, { $pop : { hobbies: 1 }}) // elimina el primero
> db.users.update({ _id:4 }, { $pop : { hobbies: -1 }}) // elimina el último
> db.users.update({ _id : 4 }, { $pullAll: { hobbies: [ "baloncesto", "caza" ]}}) // elimina por valor

Eliminar

Eliminar documento

> db.users.remove({ _id : 4 })

Eliminar colección

> db.users.drop()

Índices

Índices simples y compuestos

> db.users.createIndex({ username: 1 });
> db.users.createIndex({ username: 1, edad: -1 })

Crear índices sobre un campo de tipo array. Mongo crea entradas separadas automáticamente para cada elemento del array en un tabla de índices haciendo muy eficiente la búsqueda por valores del array.

No se puede crear un índices compuesto por más de un campo de tipo array.

> db.users.createIndex({ hobbies: 1})

Índices únicos

> db.users.createIndex({ username: 1, edad: -1 }, { unique: 1 }

Operaciones cubiertas por el índice: son muy eficientes ya que no tiene que acceder al documento para obtener los datos si no que los saca del índice

> db.users.createIndex({ edad: 1 })
> db.users.find({ edad: { "$lt": 30 } }, { edad: 1, _id : 0 })

Listar los índices de una colección

> db.users.getIndexes()

Eliminar un índice existente

> db.users.dropIndex({ username: 1, edad: -1 })

Análisis y estadísticas

Ver resultados del plan de ejecución

Ver documentación oficial.

> db.users.find({ _id : 1 }, { _id : 1 }).explain()

Método genérico para obtener el tiempo que tarda en ejecutarse una consulta

> d = new Date; db.consulta.... ; print(new Date - d + 'ms');

Información de una colección

> db.users.stats()

Códigos ejemplo (conexión desde NodeJS)

Procesar las fotografías de caratula de album, cuyo album no existe en la base de datos.

var client = require('mongodb')
    .MongoClient;
client.connect('mongodb://localhost:27017/h7', function(err, db) {
    if (err) throw err;
    var albums = db.collection('albums');
    db.collection('images', function(err, images) {
        if (err) throw err;
        images.find({}, {
            '_id': true
        }, function(err, cursor) {
            if (err) throw err;
            var count = cursor.count(function(err, count) {
                console.dir('num images: ' + count);
                // iterate over each image  
                cursor.each(function(err, item) {
                    if (item !== null) {
                        // attempt to find an album containing the photo, if not prune  
                        albums.findOne({
                            images: item._id
                        }, function(err, doc) {
                            if (err) throw err;
                            if (doc == null) {
                                images.remove({
                                    '_id': item._id
                                }, function(err, numRemoved) {
                                    if (err) throw err;
                                    count - ;
                                    console.dir('count: ' + count);
                                    if (count == 0) db.close();
                                });
                            } else {
                                count - ;
                                console.dir('count: ' + count);
                                if (count == 0) db.close();
                            }
                        });
                    }
                });
            });
        })
    });
});

Registro de usuario y validación usando node y mongo

var bcrypt = require('bcrypt-nodejs');
/\* The UsersDAO must be constructed with a connected database object \*/

function UsersDAO(db) {
    "use strict";
    /* If this constructor is called without the "new" operator, "this" points

    \* to the global object. Log a warning and call it correctly. \*/
    if (false === (this instanceof UsersDAO)) {
        console.log('Warning: UsersDAO constructor called without "new" operator');
        return new UsersDAO(db);
    }
    var users = db.collection("users");
    this.addUser = function(username, password, email, callback) {
        "use strict";
        // Generate password hash  
        var salt = bcrypt.genSaltSync();
        var password_hash = bcrypt.hashSync(password, salt);
        // Create user document  
        var user = {
            '_id': username,
            'password': password_hash
        };
        // Add email if set  
        if (email != "") {
            user['email'] = email;
        }
        users.insert(user, function(err, result) {
            "use strict";
            if (!err) {
                console.log("Inserted new user");
                return callback(null, result[0]);
            }
            return callback(err, null);
        });
    }
    this.validateLogin = function(username, password, callback) {
        "use strict";
        // Callback to pass to MongoDB that validates a user document  
        function validateUserDoc(err, user) {
            "use strict";
            if (err) return callback(err, null);
            if (user) {
                if (bcrypt.compareSync(password, user.password)) {
                    callback(null, user);
                } else {
                    var invalid_password_error = new Error("Invalid password");
                    // Set an extra field so we can distinguish this from a db error  
                    invalid_password_error.invalid_password = true;
                    callback(invalid_password_error, null);
                }
            } else {
                var no_such_user_error = new Error("User: " + user + " does not exist");
                // Set an extra field so we can distinguish this from a db error  
                no_such_user_error.no_such_user = true;
                callback(no_such_user_error, null);
            }
        }
        users.findOne({
            '_id': username
        }, validateUserDoc);
    }
}
module.exports.UsersDAO = UsersDAO;

Operaciones de un blog

* The PostsDAO must be constructed with a connected database object */

function PostsDAO(db) {
    "use strict";
    /* If this constructor is called without the "new" operator, "this" points

    \* to the global object. Log a warning and call it correctly. \*/
    if (false === (this instanceof PostsDAO)) {
        console.log('Warning: PostsDAO constructor called without "new" operator');
        return new PostsDAO(db);
    }
    var posts = db.collection("posts");
    this.insertEntry = function(title, body, tags, author, callback) {
        "use strict";
        console.log("inserting blog entry" + title + body);
        // fix up the permalink to not include whitespace  
        var permalink = title.replace(/\s/g, '_');
        permalink = permalink.replace(/\W/g, ”);
        // Build a new post  
        var post = {
            "title": title,
            "author": author,
            "body": body,
            "permalink": permalink,
            "tags": tags,
            "comments": [],
            "date": new Date()
        }
        // now insert the post  
        posts.insert(post, function(err, result) {
            "use strict";
            if (err) return callback(err, null);
            console.log("Inserted new post");
            callback(err, permalink);
        });
    };
    this.getPosts = function(num, callback) {
        "use strict";
        posts.find()
            .sort('date', -1)
            .limit(num)
            .toArray(function(err, items) {
                "use strict";
                if (err) return callback(err, null);
                console.log("Found " + items.length + " posts");
                callback(err, items);
            });
    };
    this.getPostsByTag = function(tag, num, callback) {
        "use strict";
        posts.find({
                tags: tag
            })
            .sort('date', -1)
            .limit(num)
            .toArray(function(err, items) {
                "use strict";
                if (err) return callback(err, null);
                console.log("Found " + items.length + " posts");
                callback(err, items);
            });
    };
    this.getPostByPermalink = function(permalink, callback) {
        "use strict";
        posts.findOne({
            'permalink': permalink
        }, function(err, post) {
            "use strict";
            if (err) return callback(err, null);
            // XXX: Look here for final exam to see where we store "num_likes"  
            // fix up likes values. set to zero if data is not present  
            if (typeof post.comments === 'undefined') {
                post.comments = [];
            }
            // Each comment document in a post should have a "num_likes" entry, so we have to  
            // iterate all the comments in the post to make sure that is the case  
            for (var i = 0; i < post.comments.length; i++) {
                if (typeof post.comments[i].num_likes === 'undefined') {
                    post.comments[i].num_likes = 0;
                }
                post.comments[i].comment_ordinal = i;
            }
            callback(err, post);
        });
    };
    this.addComment = function(permalink, name, email, body, callback) {
        "use strict";
        var comment = {
            'author': name,
            'body': body
        }
        if (email != "") {
            comment['email'] = email
        }
        posts.update({
            'permalink': permalink
        }, {
            '$push': {
                'comments': comment
            }
        }, function(err, numModified) {
            "use strict";
            if (err) return callback(err, null);
            callback(err, numModified);
        });
    };
    this.incrementLikes = function(permalink, comment_ordinal, callback) {
        "use strict";
        posts.findOne({
            'permalink': permalink
        }, function(err, post) {
            "use strict";
            console.dir(post.comments[comment_ordinal]["num_likes"]);
            if (err) return callback(err, null);
            if (!post.comments[comment_ordinal]["num_likes"]) {
                post.comments[comment_ordinal]["num_likes"] = 0;
            }
            post.comments[comment_ordinal]["num_likes"] += 1;
            posts.update({
                'permalink': permalink
            }, post, function(err, numModified) {
                console.log("numModified= " + numModified);
                if (err) return callback(err, null);
                callback(err, numModified);
            });
        });
    };
}
module.exports.PostsDAO = PostsDAO;

Teoría

Al crear un documento, MongoDB automáticamente crea el campo _id para el mismo. Este es único para cada documento y sobre él se establece un índice para acelerar las búsquedas.

👉  Reglas htaccess esenciales: redirecciones, seguridad y más

En las bases de datos tradicionales (relacionales), el almacenamiento de datos suele ser agnóstico a la aplicación. En cambio en MongoDB es más importante cómo se usan los datos, qué consultas se realizan y si son más usados en operaciones de lectura o escritura.

Este esquema se llama dirigido por la aplicación y tiene las siguientes características:

  • Documentos enriquecidos: un documento contiene toda la información relevante necesaria. No se encuentra distribuida en otros documentos.
  • Pre-join o datos embebidos: un documento puede contener otros documentos embebidos (el equivalente a los joins). Estos se almacenan con atributos de arrays u objetos.
  • No hay joins: el objetivo de mongo es dar un alto rendimiento. Los JOINS de SQL escalan muy mal y son el motivo de bajos rendimientos. Por tanto en mongo se tiene todos los datos en un documento y no se utilizan JOINS. Se pueden implementar pero se deberían usar de forma excepcional.
  • No hay constraints: esto se suple con los datos embebidos.
  • Operaciones atómicas: las operaciones son atómicas a nivel de documento.
  • No hay que definir esquemas: no necesitamos definir esquemas para empezar a guardar datos en un documento. Además un documento admite múltiples estructuras de datos, no queda limitada a la estructura de la primera insercción.

Fuentes recomendadas

https://blog.findemor.es/2015/06/aprender-a-usar-mongodb-guia-5/

👇Tu comentario