lunes, 29 de noviembre de 2010

Conceptos básicos de MongoDB

Filosofía de almacenamiento en MongoDB

La filosofía de las bases de datos NOSQL suele ser chocante para todos los que llevan años trabajando en bases de datos relacionales. El no tener el concepto de una tabla, o de una integridad referencial con su relación de clave maestra a clave foránea, se hace difícil imaginar cómo puede funcionar y cómo pueden relacionarse y entenderse los datos.

MongoDB tiene el concepto de la información almacenada como clave/valor. Estos pares se almacenan en forma de documento u objeto dentro de una colección. Podemos imaginar un símil entre clave/valor como campo/valor, entre un objeto o documento y una fila, y entre colección y tabla. Este concepto puede ayudar a entender un poco mejor este sistema, pero no hay que olvidar que es un símil, no una equiparación.

Un documento u objeto (los dos términos se refieren a lo mismo), aunque se asemeje a una fila, en realidad no tiene nada que ver, pues en una base de datos relacional, cada fila tiene una organización estructurada común entre todas las filas. En MongoDB, cada fila puede tener su propia estructura, tener más o menos campos, e incluso tener campos que en sí mismos son arrays (contener varios valores) o incluso contener otro documento como valor, o incluso un array de documentos. Esto, entendido bien, nos puede dar una idea de la potencia que ello implica, pues es posible tener en un mismo objeto, de manera incrustada, otros objetos, sin necesidad de implicar a la base de datos en varias tablas, definir claves y relacionar dichas claves. Por ejemplo, imaginemos la clásica relación “Categoría” y “Producto”. En un sistema de base de datos tradicional, se definirían dos tablas:

Tabla categoría:
- idcategoria: integer: PRIMARY KEY
- nombrecategoria: char(30)

Tabla producto:
- idproducto: integer: PRIMARY KEY
- nombreproducto: char(30)
- idcategoria: integer

Internamente, el gestor de base de datos debe estar constantemente velando para que los datos clave sean únicos, no nulos y que no violan las reglas de integridad referencial, realizando complejas operaciones de índices y actualización de éstos. Estas operaciones son transparentes para los usuarios y desarrolladores. Es cómodo, pero sobrecargan los tiempos de CPU y penalizan otras operaciones que pueden ser más importantes.

En una base de datos MongoDB se requeriría únicamente una colección de objetos, cada uno de los cuales puede tener una estructura propia (no tiene por qué ser la misma).

{producto: “Perdiz escabechada”, categoria:[“carne”,”conserva”]}
{producto:”Naranja”, categoria:”fruta”]}
{producto:”Sal”}


De este simple ejemplo se pueden extraer algunas reflexiones:
- El almacenamiento físico gana mucho, al no estar supeditada a una estructura fija y definida. Se pueden omitir claves (campos) si se desea, y los campos de texto ocupan sólo el número de caracteres que contiene, no un tamaño fijo.
- Se prescinde de campos id, que dificultan el entendimiento de los datos.
- Se centraliza todo en una única colección, y no añade la dificultad de las relaciones.
- Un producto puede no estar asociado a una categoría, o bien estar asociado a varias categorías. En este último caso, en una base de datos relacional, requeriría de una tercera tabla intermedia con la colección de relaciones, y el trabajo extra en el código para reconstruir las mismas.
- Si bien el control de la redundancia en las categorías es un esfuerzo por parte del código, en el caso de una base de datos relacional, también habría que hacer un esfuerzo en código cuando se determina si hay redundancia por los errores que emite la base de datos (clave duplicada, infracción de integridad…).
- El modo de almacenamiento es más natural para la máquina y para el humano, pues toda la información está en el mismo documento, en lugar de repartido. Esto evita al código repartir la información (al guardar) y de reunirla (al acceder).
- En un modelo relacional, utilizar id’s reduce el espacio de almacenamiento en tablas extensas (ocupa mucho menos un número que un texto), pero complica el desarrollo y el acceso. MongoDB reduce y optimiza espacio de almacenamiento en los campos de texto, supliendo este espacio e incluso mejorándolo. Asimismo, el código para guardar o acceder a la información es mucho más simple (no hay que realizar relaciones (los típicos join o el uso de varias consultas a varias tablas) ni realizar varias actualizaciones por cada una de las tablas involucradas).

Otro ejemplo de almacenamiento en un documento u objeto sería el siguiente:
{ nombre: ‘Rafael’,
apellidos: ‘Hernamperez Martin’,
fechaingreso: Date(’03-22-2010’),
seleccion: [‘Aprenda MongoDB’,’Flex 4 en una semana’,’AJAX para Dummies’],
comentarios: [{autor: ‘adan3000’, comentario: ‘Buena eleccion’},
{autor: ‘majopero’, comentario: ‘Te has pasado’, puntuacion:5}
]
}


La clave “comentarios” es un array de documentos. Un documento puede contener, asimismo, documentos asociados a una clave. Es lógico suponer que el nivel de anidamiento puede ser tan profundo como uno desee.

El formato de datos utilizado por MongoDB es JSON, una especificación estándar para representar la información. Es similar a la de XML, pero reduce el contenido a expresar y haciendo más legible y natural la interpretación de la información. El último documento en formato XML sería el siguiente:
<documento>
  <nombre>Rafael</nombre>
  <apellidos>Hernamperez Martin</apellidos>
  <fechaingreso>03-22-2010</fechaingreso>
  <seleccion>
    <titulo>Aprenda MongoDB</titulo>
    <titulo>Flex 4 en una semana</titulo>
    <titulo>AJAX para Dummies</titulo>
  </seleccion>
  <comentarios>
    <comment autor=”adan3000” comentario=”Buena elección”/>
    <comment autor=”majopero” comentario=”Te has pasado” puntuación=”5”/>
  </comentarios>
</documento>


Alguno se estará preguntando cómo mantener la consistencia de los datos en las aplicaciones. Por ejemplo, en una aplicación es conveniente evitar al usuario teclear la categoría, pudiendo seleccionar una ya predeterminada en una lista para asegurar que sea unívoca y que no haya multitud de referencias a un mismo valor que esté redundante porque se diferencia en una letra o está mal escrito. Se puede crear una colección de categorías, en un formato muy similar al de una tabla (usando documentos con una única clave), y usar ésta como se haría normalmente, o incluso añadir un documento en nuestra colección cuya clave sea un array de valores posibles. Aunque su estructura no tenga nada que ver con el resto de documentos almacenados en la misma. Se accede a dicho documento dentro de la colección y se extraen los posibles valores. La primera opción es más sencilla y legible. La última es más óptima en cuanto almacenamiento y gestión por parte del motor de base de datos (trabaja en la misma colección y comparte los mismos ficheros). Otra solución intermedia, y a la vez elegante, sería definir una colección exclusiva para almacenar documentos que contengan series de datos maestros (categorías, tipos, etc.)

Otra de las ventajas de utilizar este sistema de información, es que no tienes tantas limitaciones a la hora de escalar la información si los requisitos cambian (cosa que ocurre, pues nadie conoce el futuro). En bases de datos relacionales, añadir nuevos campos a una tabla, o una nueva tabla relacionada o maestra y normalizar una base de datos que lleva ya tiempo en producción, es cuanto menos un engorro y un agujero de problemas.

Una vez se entienden estos sencillos conceptos, el adaptar nuestros desarrollos a este tipo de bases de datos es sencillo, e incluso nos beneficiaremos de una mayor legilibilidad y rapidez.


Paso a paso
El movimiento se demuestra andando, y para aprender a caminar en MongoDB procederemos a realizar, paso a paso, un ejemplo práctico. El propósito del mismo es tener una colección de datos personales llamada “agenda”, dentro de una base de datos llamada “ejemplo”. En el ejemplo se verá cómo crear la base de datos, la colección y los datos, y a continuación se verá como realizar consultas a dichos datos.

Arranque del servidor y de la consola MongoDB

Primeramente, arrancar el servidor de MongoDB desde una consola DOS (para Linux, los pasos son muy similares):

cd c:\mongodb-win32-i386-1.2.4\bin
mongod


A continuación, arrancar la consola de MongoDB (dbShell) en otra consola DOS:

cd c:\mongodb-win32-i386-1.2.4\bin
mongo



Creación de la base de datos y de la colección

Aunque no se haya creado aún la base de datos, utilizar ésta (asumirá que se va a utilizar para ser creada):

> use ejemplo
switched to db ejemplo


A continuación crear un objeto que contendrá un documento, el cual se insertará en la colección “agenda” (aún no creada):

> doc = {nombre: "Rafael", apellido1: "Gonzalez", apellido2: "Martin", telefono: "912406790"}

{
"nombre" : "Rafael",
"apellido1" : "Gonzalez",
"apellido2" : "Martin",
"telefono" : "912406790"
}


El siguiente paso es añadir este objeto (documento) a la colección “agenda”:

> db.agenda.save(doc)

Se puede abreviar el proceso en un solo paso, creando directamente el objeto:

> db.agenda.save({nombre: "Rafael", apellido1: "Gonzalez", apellido2: "Martin", telefono: "912406790"})

Esto equivaldría a la sentencia SQL:

INSERT INTO agenda (nombre, apellido1, apellido2, telefono) VALUES (‘Rafael’, ‘Gonzalez’, ‘Martin’, ‘912406790’)

Automáticamente, MongoDB crea los objetos por asunción. De esta manera, crea la base de datos “ejemplo”, y dentro de ésta crea la colección “agenda”, y dentro de ésta crea y agrega el documento “doc”.


Verificaciones

Para verificar todo lo anterior primero comprobaremos qué base de datos está en uso:

> db
ejemplo


A continuación, listaremos las bases de datos creadas con MongoDB:

> show dbs

admin
ejemplo
local
test


Las bases de datos “admin”, “local” y “test” son las bases de datos que por defecto tiene MongoDB.

La siguiente verificación será comprobar qué colecciones disponemos en la base de datos en uso (“ejemplo”):

> show collections
agenda
system.indexes


La colección “system.indexes” es creada y mantenida de forma automática por MongoDB para el control y gestión de los índices de las colecciones.

Para visualizar solamente las colecciones no internas de MongoDB, se puede usar el siguiente comando:

> db.getCollectionNames()
[ "agenda", "system.indexes" ]


Para conocer a qué base de datos pertenece una determinada colección:

> db.agenda.getDB()
ejemplo


Para conocer qué comandos se puede usar para tratar la colección:

> db.agenda.help()


Consultas a los datos

Para visualizar todos los objetos (documentos) de la colección:

> db.agenda.find()
{ "_id" : ObjectId("4ba8a3be5b3d00000000710f"), "nombre" : "Rafael", "apellido1" : "Gonzalez", "apellido2" : "Martin", "telefono" : "912406790" }


Automáticamente, MongoDB asigna un ID único a cada objeto de la colección (clave “_id”).

Vamos a añadir más documentos a la colección:

db.agenda.save({nombre:"Carlos",apellido1:"Sanchez",apellido2:"Sanchez",telefono:"91240890012",email:"carlosss@hotmail.com"}) db.agenda.save({nombre:"Javier",apellido1:"Cristobal",apellido2:"Nombela",telefono:"925407561",email:"javichuc@yahoo.es"})
db.agenda.save({nombre:"Yolanda",apellido1:"Ballesteros",apellido2:"Lopez",telefono:"925406902"})
db.agenda.save({nombre:"Antonio",apellido1:"Blazquez",apellido2:"Fernandez",telefono:"937607812"})
db.agenda.save({nombre:"Jose Miguel", apellido1:"Carvajal", apellido2:"Gomez", telefono:"983679103", email:"picachu234@gmx.com"})
db.agenda.save({nombre:"Juan Carlos", apellido1:"Blazquez", apellido2:"Gil", telefono:"925403789"})


Lista de todos los objetos de la colección:

> db.agenda.find()
{ "_id" : ObjectId("4ba8a3be5b3d00000000710f"), "nombre" : "Rafael", "apellido1" : "Gonzalez", "apellido2" : "Martin", "telefono" : "912406790" }
{ "_id" : ObjectId("4ba8aac55b3d000000007110"), "nombre" : "Carlos", "apellido1" : "Sanchez", "apellido2" : "Sanchez", "telefono" : "91240890012", "email" : "carlosss@hotmail.com" }
{ "_id" : ObjectId("4ba8ab435b3d000000007111"), "nombre" : "Javier", "apellido1" : "Cristobal", "apellido2" : "Nombela", "telefono" : "925407561", "email" : "javichuc@yahoo.es" }
{ "_id" : ObjectId("4ba8ac3f5b3d000000007112"), "nombre" : "Yolanda", "apellido1" : "Ballesteros", "apellido2" : "Lopez", "telefono" : "925406902" }
{ "_id" : ObjectId("4ba8ac865b3d000000007113"), "nombre" : "Antonio", "apellido1" : "Blazquez", "apellido2" : "Fernandez", "telefono" : "937607812" }
{ "_id" : ObjectId("4ba8acde5b3d000000007114"), "nombre" : "Jose Miguel", "apellido1" : "Carvajal", "apellido2" : "Gomez", "telefono" : "983679103", "email" : "picachu234@gmx.com" }
{ "_id" : ObjectId("4ba8af123433000000003118"), "nombre" : "Juan Carlos", "apellido1" : "Blazquez", "apellido2" : "Gil", "telefono" : "925403789" }


Esto equivaldría a la sentencia SQL:

SELECT * FROM agenda

Cuando la colección contiene múltiples objetos será necesario introducir criterios en la búsqueda. El comando “find” permite pasar como parámetros dichos criterios (en formato JSON), los cuales recogen el par (clave/valor) a encontrar. El siguiente comando localiza todos los objetos en cuyo primer apellido sea “Blazquez”:

> db.agenda.find({"apellido1":"Blazquez"})
{ "_id" : ObjectId("4ba8ac865b3d000000007113"), "nombre" : "Antonio", "apellido1" : "Blazquez", "apellido2" : "Fernandez", "telefono" : "937607812" }
{ "_id" : ObjectId("4ba8af123433000000003118"), "nombre" : "Juan Carlos", "apellido1" : "Blazquez", "apellido2" : "Gil", "telefono" : "925403789" }


Lo anterior equivaldría a la sentencia SQL:

SELECT * FROM agenda WHERE apellido1=”Blazquez”

Mediante el siguiente comando sabremos cuántos objetos tiene la colección:

> db.agenda.count()

En un conjunto de datos, para limitar el número de objetos retornados, se especificaría añadiendo el comando limit():

> db.agenda.find().limit(3)
{ "_id" : ObjectId("4ba8a3be5b3d00000000710f"), "nombre" : "Rafael", "apellido1" : "Gonzalez", "apellido2" : "Martin", "telefono" : "912406790" }
{ "_id" : ObjectId("4ba8aac55b3d000000007110"), "nombre" : "Carlos", "apellido1" : "Sanchez", "apellido2" : "Sanchez", "telefono" : "91240890012", "email" : "carlosss@hotmail.com" }
{ "_id" : ObjectId("4ba8ab435b3d000000007111"), "nombre" : "Javier", "apellido1" : "Cristobal", "apellido2" : "Nombela", "telefono" : "925407561", "email" : "javichuc@yahoo.es" }


El comando count() también se puede añadir a find() para saber cuántos objetos ha retornado:

> db.agenda.find({apellido1:"Blazquez"}).count()
2


En el caso de querer visualizar solamente el primero de un conjunto de datos retornados, se usaría el comando findOne():

> db.agenda.findOne({apellido1:"Blazquez"})
{
"_id" : ObjectId("4ba8ac865b3d000000007113"),
"nombre" : "Antonio",
"apellido1" : "Blazquez",
"apellido2" : "Fernandez",
"telefono" : "937607812"
}


Para especificar más criterios, éstos se separan por comas:

> db.agenda.find({"apellido1":"Blazquez","nombre":"Antonio"})
{ "_id" : ObjectId("4ba8ac865b3d000000007113"), "nombre" : "Antonio", "apellido1" : "Blazquez", "apellido2" : "Fernandez", "telefono" : "937607812" }


Su equivalente en SQL sería el siguiente:

SELECT * FROM agenda WHERE apellido1=”Blazquez” AND nombre=”Antonio”

El comando find() retorna realmente un cursor, el cual puede ser utilizado para un acceso más controlado. El siguiente ejemplo se declara una variable que recoge el cursor del comando find(), situándose antes del primer registro. A continuación se declara un bucle while que se repetirá mientras el cursor tenga elementos o no alcance el final (hasNext()). En este bucle se imprimirá, en formato JSON, el objeto actual sobre el cual está situado el cursor. Mediante el comando next(), el cursor avanzará al siguiente objeto.

> var cursor = db.agenda.find()
> while (cursor.hasNext()) { print(tojson(cursor.next())); }
{
"_id" : ObjectId("4ba8a3be5b3d00000000710f"),
"nombre" : "Rafael",
"apellido1" : "Gonzalez",
"apellido2" : "Martin",
"telefono" : "912406790"
}


El siguiente ejemplo, recoge el cursor y accede directamente al tercer objeto del mismo:

> var cursor=db.agenda.find()
> print(tojson(cursor[3]))
{
"_id" : ObjectId("4ba8ac3f5b3d000000007112"),
"nombre" : "Yolanda",
"apellido1" : "Ballesteros",
"apellido2" : "Lopez",
"telefono" : "925406902"
}


La variable “cursor” se podría ver como un array de objetos, por lo que si se quiere acceder a cualquier clave del mismo, se especifica mediante un punto, como si fuera una propiedad:

> print(cursor[3].nombre, cursor[3].apellido1, cursor[3].telefono)
Yolanda Ballesteros 925406902


Safe Creative #1003235820060