# Bases de dades documentals: MongoDB ## Introducció MongoDb, nom que prové de les paraules angleses **humongous**(Enorme) i **Database** (Base de dades), és una base de dades de codi obert orientada a documents. Una forma pragmàtica de definir què és un motor de base de dades orientat a documents és veure'l com un motor de base de dades que treballa amb JSONs. Cada JSON és un document i els podem agrupar en col·leccions (anàlogues a les taules) de documents afins. Segons els creadors de MongoDB, els avantatges són: * Un document pot ser la serialització directa d'un objecte de la nostra aplicació. * Els documents incrustats (embedded) i els arrays, redueixen la necessitat de joins costosos. * L'esquema dinàmic permet el polimorfisme fluid, en forma de documents amb diferents atributs entre ells. A més, aquest dinamisme no ens força a haver de dissenyar un esquema o model abans de registrar la informació, ja que cada document podrà tenir més o menys atríbuts que altres inserits amb anterioritat. > Tots els documents emmagatzemats a una col·lecció de MongoDB contenen el camp '_id'. El camp _id ha de ser únic i funciona de manera anàloga a les claus primàries de les taules de les bases de dades relacionals. Si un document no declara el camp _id, el motor de Mongo l'afegeix de forma automàtica. Per últim, hi ha diferències amb les BBDD clau-valor com és el cas de Redis o DynamoDB; per citar una, a les bases de dades documentals el valor és el propi document XML/JSON, no una seqüència binària. ## Instruccions a la shell de mongo Per començar a treballar amb mongodb podem fer-ho de diferents maneres: instal·lant la versió community a la nostra màquina, utilitzant la versió atlas al cloud o fer servir un contenidor amb docker (com s'ha suggerit al llarg d'aquesta UF). Un cop arrenquem la shell veurem el prompt test>. A partir d’aquí ja podem començar a escriure algunes comandes: * show dbs; ⇒ Llistem les bases de dades. use nom_base_de_dades; ⇒ Triem una de les bases de dades llistada. * show collections; ⇒ Llistem les col·leccions de la base de dades. * db.nom_collection.countDocument(); ⇒ Fem un count del nombre de documents que conté la col·lecció. * db.nom_collection.findOne(); ⇒ troba el primer document de la col·lecció. * db.nom_collection.find(); ⇒ llista tots els documents. Una variant és utilitzar find() per localitzar un document concret per la seva clau. Per trobar un document amb una id concreta fem: find({_id: ObjectId(“5ce45d7606444f199acfba1e”)}). ### Aspectes a tenir en compte * Respecte les claus: * No poden ser nul·les. * Poden contenir qualsevol caràcter UTF8, excepte `“$”` o `“.”`. * Són case-sensitive. * Deuen ser úniques dins un mateix document. * Respecte els seus valors: * Poden ser de qualsevol tipus permès. * Respecte el document: * Deuen posseir un camp _id amb un valor únic que serà l’identificador del document. Si no ho fem, MongoDB generarà un camp del tipus Object_id automàticament. * Respecte les col·leccions: * Els noms no poden ser una cadena buida (“ ”), caràcter nul o contenir el símbol “$”. * Podem utilitzar el punt “.” dins el nom de col·leccions. ### Tipus de dades | **Tipus** | **Descripció** | |-----------------------|--------------------------------------------------------------------------------| | `null` | Representa el valor nul com un camp que no existeix | | `boolean` | Valors `true` i `false` | | `number` | Valors numèrics amb coma flotant. Per enters tenim `NumberInt` (32 bits) i `NumberLong` (64 bits). | | `String` | Qualsevol cadena de caràcters UTF8 vàlida | | `Date` | Dates expressades en milisegons | | `array` | Llista de valors que actuen com a vectors | | `Documents incrustats`| Són documents anidats dins un altre document | | `ObjectId` | Tipus per defecte pels camps `_id` | Compte amb les dates. No és el mateix fer: ``` test> let b=new Date() ``` Que fer ``` test> let a= Date() ``` En el primer cas estarem instanciant una variable amb el tipus Date que esmentàvem a la taula de dalt: ``` test>b ISODate(“2023-03-05T04:46:09.371Z”). ``` I en el segon cas el que estem és creant un String (especificant una data i hora): ``` test>a Sun March 05 2023 04:46:09 GMT+0200 (hora de verano de Europa Central). ``` També són interessants els documents incrustats (encastats) o embedded. Així un document pot contenir un altre document en el seu interior: ``` { _id: ObjectId('68252463c541c81b96e6c8c2'), titol: “Rogue One. A Star Wars Story.”, any: 2016, director: { nom: “Gareth”, cognoms: “Edwards”, any_naixement: 1975, nacionalitat: “anglesa” } } ``` **Avantatges:** * Millor rendiment, ja que no necessitem fer diferents consultes (o joins com a l'SQL) per obtenir tota la informació relacionada amb un o més documents. * Model més senzill i intuïtiu. **Inconvenients:** * Redundància de dades, ja que hi haurà dades que es repetiran vàries vegades en diferents documents. * Creixement "incontrolat" de la mida del document, afectant al rendiment de la base de dades quan el document conté diferents documents incrustats. * Dificultats en actualitzacions: actualitzar informació en documents encastats pot ser més complex, especialment quan es tracta de dades que s'actualitzen amb freqüència o que tenen dependències en altres documents. ### Operacions CRUD #### Create - Inserció Es poden insertar documents de forma individual o en grups. * db.nom_collection.insertOne({ < contingut del document > }) * db.nom_collection.insertMany([ { < un document > }, { < altre document > } ]) Per exemple, si prenem d'exemple la col·lecció students que proposem treballar l'insertOne seria com segueix: ``` db.students.insertOne({ name: 'Xavier', lastname1: 'Sambala', lastname2: 'Agromenor', gender: 'H', email: 'diwasbhattaraitt@yahoo.com', phone: '629.962.656', birth_year: 1960 }); ``` I per insertMany seria com segueix: ``` db.students.insertMany([ { name: 'Xavier', lastname1: 'Viudes', lastname2: 'Agromayor', gender: 'H', email: 'xavi.viudes@example.com', phone: '629.962.656', birth_year: 1960 }, { name: 'Anna', lastname1: 'Martí', lastname2: 'Pérez', gender: 'M', email: 'anna.marti@example.com', phone: '612.345.678', birth_year: 1985 }, { name: 'Carlos', lastname1: 'García', lastname2: 'López', gender: 'H', email: 'carlos.garcia@example.com', phone: '611.000.999', birth_year: 1992 } ]); ``` Tant si inserim 1 o més documents, veurem que la shell ens donarà aquest missatge si tot va bé, especificant la id o les id dels documents inserits: ``` { acknowledged: true insertedId: ObjectId(“6277510ab54867b80b742ddf”) } ``` Consideracions: * Si la col·lecció a la que estem afegint el document no existeix, aquesta es crea de forma automàtica. * El camp _id s’ha generat automàticament, però podem forçar a mà un valor especificant-lo com un camp més. Sols cal tenir en compte la unicitat d’aquest valor. * Les operacions d'insert són atòmiques a nivell de document, si necessitem atomicitat a nivell d'instrucció, hem de recòrrer a les transaccions (com ja solíem fer amb les bases de dades relacionals). #### Read (Find) * Fem una cerca sobre tots els documents de la col·lecció (equival al select * from de SQL): ``` db.students.find(); ``` * Fem una cerca d'aquells estudiants nascuts l'any 1960: ``` db.students.find({birth_year: 1960}) ``` * Especificar més d'un camp comporta una 'AND' implícita: ``` db.students.find({birth_year: 1960, name: "Alberto"}) ``` ##### Operadors de comparació | **Operador** | **Descripció** | **Exemple** | |--------------|------------------------------------------------------------------|-------------------------------------------------| | `$eq` | els valors iguals a un valor especificat | `{ name: { $eq: "John" } }` | | `$gt` | els valors més grans a un valor especificat | `{ quantity: { $gt: 30 } }` | | `$gte` | els valors més grans o iguals a un valor especificat | `{ quantity: { $gte: 30 } }` | | `$in` | els valors compresos dins un vector (array) especificat | `{ names: { $in: ["Peter", "Karl"] } }` | | `$lt` | els valors més petits a un valor especificat | `{ quantity: { $lt: 30 } }` | | `$lte` | els valors més petits o iguals a un valor especificat | `{ quantity: { $lte: 30 } }` | | `$ne` | els valors que no són iguals a un valor especificat | `{ name: { $ne: "Joe" } }` | | `$nin` | els valors NO compresos dins un vector (array) especificat | `{ names: { $nin: ["Peter", "Karl"] } }` | ##### Especificació dels atributs a mostrar De forma semblant a SQL podem fer que la consulta mostri uns determinats camps. Per exemple, imaginem que sols vull el nom i el primer cognom de cada estudiant: ``` db.students.find({birth_year: 1960},{name:1,lastname1:1}) ``` Si no volem que es mostri la id, cal dir-ho explícitament així: ``` db.students.find({birth_year: 1960},{name:1,lastname1:1, _id:0}) ``` ##### AND i OR L’operador $or ens permet realitzar consultes amb camps diferents del document, passant vectors de possibles condicions d’aquesta forma: db.nom_collection.find( { $or: [condició 1, condició 2, … ] } ) On les diferents condicions s'expressen dins un array com {camp:valor} Per exemple, aquells estudiants amb nom Jordi o Xavier: ``` db.students.find({$or:[{name:{$eq:'Xavier'}},{name:{$eq:'Jordi'}}]}) ``` Que en aquest cas concret es pot escriure així també: ``` db.students.find({$or:[{name:'Xavier'},{name:'Jordi'}]} ``` En el cas del $and, la idea és la mateixa: db.nom_collection.find( { $and: [condició 1, condició 2, … ] } ) Per exemple, estudiants amb nom Xavier i any de naixement anterior al 1969: ``` db.students.find({$and:[{name:'Xavier'},{birth_year:{$lt:1969}}]}) ``` Ja anteriorment hem esmentat que el $and podrà ser prescindible i expressar les condicions separades per comes, no obstant pareu atenció quan el camp que apareix a les condicions és el mateix. Fixeu-vos en el següent: ``` students> db.students.find({$and:[{birth_year:{$gt:1960}},{birth_year:{$lt:1969}}]}).count() 808 students> db.students.find({birth_year:{$gt:1960}},{birth_year:{$lt:1969}}).count() 3123 ``` El count() revela diferències significatives en el nombre de documents, degut a la forma d'interpretar les condicions i que en absència del $and sols es té en compte la darrera condició. Per què creieu que passa això? ##### EXISTS Recordem que amb MongoDB els documents no tenen una estructura o esquema comunes, raó per la qual és possible que existeixin atributs presents solament en alguns dels documents. Per comprovar l’existència d’aquestes claus fem ús de l’operador $exists. Així la sintaxi és com segueix: db.nom_collection.find( {camp: { $exists: true / false } } ). Si indiquem true és aquells on el camp hi és present i si indiquem false és on no hi és present. També es pot emprar 1 o 0 en comptes de true i false. Per exemple volem veure aquells estudiants que tenen telèfon auxiliar: ``` db.students.find({phone_aux:{$exists:true}}) ``` O també: ``` db.students.find({phone_aux:{$exists:1}}) ``` ##### Consultes amb expressions regulars Si bé poden utilitzar-se les expressions de regulars de JavaScript, disposem de l’operador $regex, el qual té la següent sintaxi, amb l’ús de $options i sense: {clau: { $regex: /patró/, $options: ‘<opcions>’ } } {clau: { $regex: /patró/<opcions> } } És a dir, l'expressió regular serà igual de vàlida fent servir $regex o no. Es poden utilitzar diferents opcions, però una de les més útils quan treballem amb patrons de caràcters alfabètics es fer servir l'opció "i". Aquesta opció serveix per dir-li a l'interpret que no faci diferència entre caràcters en majúscula i en minúscula. Aquí teniu alguns exemples d´ús: | **patró** | **exemple** | |-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| | conté caràcters: `//` | Telèfons que contenen 111:<br>`db.students.find({phone: {$regex: /111/}})` | | comença per: `/^/` | Noms que comencen per C:<br>`db.students.find({name: {$regex: /^C/}})` | | acaba en: `/$/` | Cognoms que acaben en ‘les’:<br>`db.students.find({lastname2: {$regex: /les$/}})` | | Case insensitive | Noms que comencen per c:<br>`db.students.find({lastname2: {$regex: /^s/, $options: 'i'}})` | | conté un determinat nombre de caràcters en un format específic | Telèfons amb el patró nnn.nnn.nnn:<br>`db.students.find({phone: /[0-9]{3}\\.[0-9]{3}\\.[0-9]{3}/}).count()` | En aquells casos en els quals volem cercar fent la negació d'ún patró, és a dir, quan es volen aquells documents que no segueixin el patró, podem fer servir l'operador $not combinat amb $regex. Per exemple volem aquells números de telèfon que no segueixin el patró nnn.nnn.nnn: ``` db.students.find({phone:{$not:{$regex:/[0-9]{3}.[0-9]{3}.[0-9]{3}/}}}) ``` ##### Cursors Quan realitzem una consulta, MongoDB ens retorna els resultats mitjançant cursors, és a dir, punters als resultats de la consulta. De fet quan realitzem una consulta amb molts resultats, el client Mongo retorna un màxim de 20 resultats i el missatge **Type “it” for more**, de tal forma que si li diem “it” el client seguirà iterant amb el cursor i ens oferirà més resultats. Altres limitacions aplicables sobre els resultats són: * **limit** Per limitar el nombre de resultats a recuperar. * **skip** Salta cap a un nombre de resultat en concret i itera des d’allà. * **sort** Ordena els resultats. Necessita un objecte JSON amb la clau d’ordenació i els valors 1 (ascendent) o -1 (descendent). Per exemple, ordenar els estudiants pel seu nom de forma ascendent i mostrar sols els 3 primers: ``` db.students.find().sort({name:1}).limit(3) ``` #### Modificacions - Update * Modificar un document: db.nom_collection.updateOne(condicio,{$set: {camp_a_modificar}}) * Modificar més d'un document: db.nom_collection.updateMany(condicio,{$set: {camp_a_modificar}}} Update és molt útil quan per exemple volem afegir un nou atribut a un document que ja existeix. Per exemple, imaginem que volem afegir un atribut nou anomenat birth_city al document amb una id en concret: ``` db.students.updateOne( { _id: ObjectId('6277510ab54867b80b742ddf') }, { $set: { birth_city: "Madrid" } } ) ``` O també modificar el valor del camp phone d'aquell document: ``` db.students.updateOne({_id:ObjectId('6277510ab54867b80b742ddf')},{ $set:{phone:'567.789.098'}}) ``` #### Esborrament - Delete Si volem eliminar documents d’una col·lecció podem fer ús de: * deleteOne: esborrem un sol element, usualment el primer element que coincideixi amb el criteri que posem ``` db.nom_collection.deleteOne(condicio) ``` Per exemple, esborrar un estudiant a partir de la seva id: ``` db.students.deleteOne({_id:ObjectId(“6277510ab54867b80b742ddf”)}) ``` * deleteMany: esborrem més elements, els que coincideixin amb un criteri. ``` db.nom_collection.deleteMany(condicio) ``` Per exemple, esborrar els estudiants nascuts l'any 1962: ``` db.students.deleteMany({birth_year:1962}) ``` #### L'aggregation framework Per acabar, MongoDB té un framework anomenat “Aggregation Framework” que ens permet fer consultes agregades tipus GROUP BY, SUM o COUNT, semblant al SQL. La sintaxi és com segueix: ``` db.nom_colleccio.aggregate ( [ < pipeline > ] ) ``` On a pipeline detallarem filtres i camps pels quals fer l'agrupació, a més de detallar les funcions típiques d'agregació. Algunes de les operacions que veurem van al voltant de: * $group agrupar documents d’acord a una determinada condició. * $match filtra l’entrada per reduir el número de documents, deixant sols aquells que coincideixen amb un determinat criteri. Exemple 1: Suposem que alguns documents d'estudiants tenen un camp anomenat `course_mark` que enregistra la nota mitjana del curs vigent (ara per ara la col·lecció amb la que esteu treballant no el té). Vull obtenir la mitjana de notes de tots els estudiants nascuts el 1960 (birth_year) agrupat pel camp lastname1: ``` db.students.aggregate([ { $match: { birth_year: 1960 } }, { $group: { _id: "$lastname1", avgMark: { $avg: "$course_mark" } } } ]); ``` En aquest cas: 1. A _id especifiquem el camp d'agrupació. 2. Emprem la funció d'agregació $avg per obtenir la mitjana. 3. Amb $match hem especificat la condició de filtre (que exclou els documents que no l'acompleixen). En cas que es vulgués ordenar el resultat per lastname1 ascendent, modificaríem l'anterior consulta afegint un sort: ``` db.students.aggregate([ { $match: { birth_year: 1960 } }, { $group: { _id: "$lastname1", avgMark: { $avg: "$course_mark" } } }, { $sort: { _id: 1 } } ]); ``` Algunes de les funcions d'agregació compreses en aquest framework són: | Operador | Descripció | |------------------|-----------------------------------------------------------------------------| | `$sum` | Calcula la suma total dels valors d'un camp dins d'un grup. | | `$min` | Retorna el valor mínim d’un camp dins d’un grup. | | `$max` | Retorna el valor màxim d’un camp dins d’un grup. | | `$first` | Retorna el primer valor trobat d’un camp dins del grup. | | `$last` | Retorna l’últim valor trobat d’un camp dins del grup. | | `$dateToString` | Converteix un valor de data en una cadena de text amb un format definit. |