En publicaciones anteriores hemos aprendido a utilizar multitud de comandos y operadores para consultar y manipular nuestros datos en MongoDB… pero aún no hemos visto una de las cosas que más diferencias tienen en una base de datos noSQL frente a un entorno tradicional SQL o relacional: el hecho de que el esquema de datos está determinado por la aplicación (es decir, por el uso que se va a hacer de los mismos) y no por su propio esquema o modelo de dominio. Este será el tema que abordemos en este post.
Anteriormente
Guía 4: Comandos y operaciones avanzadas
Esquema dirigido por la aplicación (Application-driven Schema)
Tercera forma normal (SQL) vs Application-driven Schema
En MongoDB, como en otras bases de datos noSQL es más importante cómo se utilizan los datos, qué datos son consultados como un conjunto, y cómo son más utilizados en operaciones de lectura o de escritura. Esto supone una gran diferencia con una base de datos relacional, donde la información trata de conservarse de una forma agnóstica a la aplicación, eliminando redundancias y obviando cómo se utilizará la información para favorecer un correcto diseño del esquema.
En las aplicaciones modernas, donde se requiere una gran escalabilidad y velocidad para recuperar datos, ese requisito nos obliga (cuando trabajamos con SQL) a realizar verdaderas virguerias para que las consultas e inserciones funcionen con fluidez, y a veces entramos en esa vaga zona donde no está claro si la “desnormalización” del modelo es adecuada o si supone un peligro para la futura consistencia de los datos.
En las bases de datos noSQL, al emplearse los datos conforme a cómo serán utilizados en la aplicación y distanciarse de la Tercera forma normal de SQL, se asume que será responsabilidad de la aplicación (y del desarrollador) mantener la consistencia e integridad de los datos, pero a cambio se rompe finalmente con las barreras establecidas por esa estricta normalización y permite trabajar con modelo dinámicos, carentes de esquema, fácilmente integrables con los distintos elementos de la arquitectura de un software concreto, y sobre todo drásticamente más escalable y veloz.
Características un Esquema dirigido por la aplicación en MongoDB
En la tercera forma normal, el mundo relacional de SQL nos marca 3 objetivos:
1 - Liberar a la base de datos de anomalías de modificación de los datos: por ejemplo, datos duplicados.
2 - Minimizar el rediseño al extender la base de datos: si la base de datos debe crecer añadiendo nuevas entidades, no deberían verse afectadas las tablas anteriores… sin embargo, en el mundo real esto rara vez se consigue y las nuevas necesidades de la aplicación obligan a construir consultas muy complejas y poco eficientes o bien a rediseñar el modelo.
3 - Evitar cualquier tendencia de patrón de uso de la aplicación: la estructura de los datos es independiente del uso que se haga de ellos. Pero ojo, que hacer esto tiene ventajas e inconvenientes… tantos como hacer exactamente lo contrario.
En MongoDB, vamos a respetar los puntos 1 y 2, intentando evitar en la medida de lo posible el empleo de documentos embebidos (que dificultan la consistencia de la información), pero en el caso del punto 3, vamos a hacer exactamente lo contrario, donde aunque se viola el principio no se pone en peligro la consistencia ni la integridad de los datos.
Veamos un ejemplo con un Post, que puede recibir comentarios y que esta categorizado con una serie de tags.
"_id": 1,
"author": "findemor",
"title": "este es el titulo",
"body": "esto es el contenido de un post",
"comments": [{
"body": "esto es un comentario",
"email": "john@doe.com",
"author": "john"
}, {
"body": "otro comentario",
"email": "jane@doe.com",
"author": "jane"
}],
"date": ISODate("2015-12-05T03:23:33.841Z"),
"permalink": "este_es_el_titulo",
"tags": ["tecnologia", "programacion"]
}
Vivir en un mundo sin constraints.
Una regla que nos facilita iniciarnos en el mundo noSQL después de mucho bagaje en SQL es: “Si algo tiene la forma que tendría en una base de datos relacional, probablemente no lo estás enfocando bien”.
> [ “En MongoDB, si algo tiene la forma que tendría en SQL, probablemente no lo has enfocado bien.”;]
En noSQL, la estructura de los datos forma parte del diseño de la aplicación, por lo que si el diseño no es correcto, obligarán a manipular grandes volumenes de datos en la memoria durante la ejecución de la aplicación.
Veamos un esquema de datos alternativo al anterior para almacenar posts y comentarios, en este caso más parecido a SQL (relacional)
Posts |
Comments |
Tags |
{ _id: 1, title: “- - ”, body: “- - ”, author: “- -”, date: “- - ” } |
{ _id: 3, post_id: 1, author: “- - ”, author_email: “- - ”, order: 0 } |
{ _id: 1, tag: “- - ”, posts: [1] } |
Como podréis imaginar, listar un post en un esquema de este tipo, nos obliga a obtener el post, despues recorrer los comentarios y consultar cada uno de ellos de la base de datos… y lo mismo con los tags. Eso son muchas operaciones de entrada-salida, se rompe la atomicidad de la operación y es muy ineficiente en procesamiento… Podría ser deseable si vamos a realizar un gran número de actualizaciones de los datos (por ejemplo si los usuarios van a editar con mucha frecuencia sus comentarios… cosa que evidentemente no va a ocurrir en este caso). Si estamos dispuestos a pagar este coste a cambio de asegurar la consistencia de los datos, sería la alternativa válida.
Viviendo sin transacciones
Como hemos dicho antes, mongo no soporta transacciones (que se emplean en SQL para ofrecer atomicidad, consistencia, aislamiento y durabilidad). Sin embargo, tenemos operaciones atómicas, es decir: al trabajar en un único documento, la operación se completará antes de que alguien pueda ver el cambio.
Hay tres aproximaciones distintas.
1 - Reestructurar el código para utilizar un único documento en las operaciones que deban ser atómicas, beneficiandote de su atomicidad para simular la transacción.
2 - Implementar por sofware una especie de bloqueo (semáforo, find and modify…).
3 - Tolerancia: hay aplicaciones que se pueden permitir cierta cantidad de inconsistencia, como actualizar el muro de facebook, etc. donde la inconsistencia de los datos no es crítica si no es grave.
En realidad, la segunda aproximación es la más habitual en el mundo real, ¡incluso al trabajar con entornos SQL! Imaginaos una transferencia de dinero entre dos cuentas bancarias de distintas entidades. Evidentemente la operación no se completa en su totalidad en un único sistema relacional, ya que ambos bancos tendrás bases de datos distintas, sino que coordinan la operación con mecanismos de este tipo.
Relaciones en MongoDB
No hay relaciones en MongoDB, pero hay distintas aproximaciones para los distintos tipos de cardinalidad que podamos encontrar. Vamos a verlos uno por uno.
Relaciones 1 a 1
Este tipo de relación se da, por ejemplo, entre un empleado y su curriculum (o entre un paciente y su historia médica, un edificio y su plano…).
Podemos vincular el identificador del curriculum en el empleado, o al revés. O podemos embeber un documento dentro del otro y tener toda la información en un único documento.
Hay que tener en consideración como va a ser leido (por ejemplo ¿siempre va a desearse leer el curriculum?) o cómo va a crecer (si va a crecer todo el tiempo, podría superar el límite de tamaño máximo para un documento, que son 16mb… en cuyo caso habría que fragmentarlo), y finalmente hay que considerar la atomicidad… si no podemos permitir ningún tipo de inconsistencia, hay que unificar el documento.
Relaciones 1 a N (entendiendo que N son Muchos)
Este tipo de relación se daría, por ejemplo, entre una Ciudad y sus Ciudadanos.
Si metiesemos a las personas en el documento inicial, evidentemente el documento podría superar el límite de tamaño permitido (16mb).
Si metemos la ciudad en cada uno de los documentos de persona, vamos a tener una gran posibilidad de que los datos sean inconsistentes si tratamos de actualizar la información de la ciudad.
Una forma válida sería enlazar los N ciudadanos hacia el 1 (ciudad), es decir, dos colecciones con el id de ciudad en un atributo del documento persona.
Relaciones 1 a N (entendiendo que N son Pocos)
Por el contrario al caso anterior, en ocasiones N son previsiblemente pocos. Por ejemplo, no hay mcuhos comentarios en un mismo Post (podría ser del orden de 1 a 10…), y como cada comentario está unicamente vinculado a un único Post y rara vez va a ser editado, parece una solución perfecta embeber los N en el 1.
Relaciones N a N (Pocos a Pocos o Muchos a Muchos)
Este tipo de relación, que se daría entre Libros y Autores, o entre Estudiantes y Profesores… es importante analizarla en cada caso concreto, porque en realidad (en estos ejemplos por ejemplo) podrían entenderse como relaciones Pocos a Pocos en gran parte de los casos del mundo real.
Una opción sería, por ejemplo, embeber los identificadores de cada uno de ellos en un array de identificadores… pero no es lo ideal, ya que suele crear una vulnerabilidad de inconsistencia a la hora de crear los datos en ambas direcciones, ya que no se mantienen juntos (aunque desde el punto de vista del rendimiento, podría ser interesante).
Libros |
Autores |
{ _id: 12, titulo: “El señor de los anillos”, authors: [27] } |
{ _id: 27, author_name: “J.R.R.Tolkien”, books: [12, 7, 13] } |
Otra opción es, en lugar de un array de identificadores, embeber el Libro dentro del Autor, pero esto nos hará duplicar libros y será vulnerable en caso de actualización.
Finalmente, la alternativa en este caso concreto, sería tener ambos objetos como objetos de primera clase de un único documento general.
Multikeys
MongoDB es extremadamente eficiente cuando realiza búsquedas de documentos por su identificador… pero cuando el número de documentos en una colección crece mucho, buscar por el valor de otros atributos suele ser penalizador. Sin embargo, MongoDB nos permite crear multiples índices en un mismo documento (ensureIndex) para realizar búsquedas eficientes por esos otros atributos, como veremos en futuros posts.
Árboles en MongoDB
Por ejemplo, entre productos y categorias.
Producto |
Categoría |
{ _id: 2, category: 7, product_name: “Nexus 5” } |
{ _id: 7, category_name: “smartphone”, parent_id: 6 } |
En el ejemplo, ponemos el identificador de la categoría padre, lo cual sería suficiente para diseñar un árbol y es conceptualmente sencillo. Sin embargo, para obtener la jerarquía tendríamos que recorrer todos los elementos.
Si queremos facilitar la obtención de todos los hijos o ancestros, otra aproximación es poner un array como todos los ancestros.
Producto |
Categoría |
{ _id: 2, category: 7, product_name: “Nexus 5” } |
{ _id: 7, category_name: “smartphone”, parent_id: 6, ancestors: [3, 6, 8] } |
Lo que nos permitiría obtener todos los descendientes de la categoría smartphone con una sola operación:
> db.categorias.find({ ancestors : 7 })
Beneficios de embeber datos
La principal ventaja es el rendimiento… los datos se acceden en el disco de una forma mucho más eficiente, se reducen el lecturas, etc. En el caso de la escritura ocurre lo mismo, que es muchísimo más eficiente, y tan solo en el caso de que el documento crezca habrá que reubicarlo.
Cuándo desnormalizar los datos
Es normal pensar que si utilizamos documentos embebidos de mongo estaremos desnormalizando los datos y generando problemas por no respetar la tercera forma normal. Pero en realidad, mientras no dupliquemos los datos, no crearíamos vulnerabilidades.
- En 1 a 1 es perfectamente viable embeber los datos de forma segura porque no se duplican los datos.
- En relaciones 1 a muchos, funciona bien si embebes los muchos en los unos.
- En relaciones muchos a muchos, hay que vincularlos a través de un array de identificadores en los documentos. Por algunas razones podrías querer embeber documentos, pero duplicarías mucho contenido.
Conclusión
Al principio puede haber una cantidad respetable de miedo a los problemas de consistencia que puede generarse con este tipo de base de datos, pero una vez superemos esa barrera, y aprendamos a diseñar nuestros datos para que encajen perfectamente en nuestra aplicación, el miedo desaparecerá para dejar paso al tremendo rendimiento y flexibilidad de MongoDB.
En futuras publicaciones veremos cómo definir y trabajar con índices, analizar consultas, manejar el framework de agregación para analizar los datos y construir réplicas de MongoDB en un entorno distribuido.
Enlaces a la guía completa
- Guía 1: Introducción a MongoDB, características clave
- Guía 2: Arrancar MongoDB e importar datos
- Guía 3: Comandos y operaciones esenciales
- Guía 4: Comandos y operaciones avanzadas
- Guía 5: Esquema de datos dirigido por la aplicación
- Guía 6: Crear, manejar y entender los índices
- Guía 7: Análisis de datos con el framework de agregación
- Guía 8: Replicación y Sharding