You are on page 1of 154

MongoDB:

gestión, administración
y desarrollo de aplicaciones

Modelos de almacenamiento de objetos


Agregaciones
MapReduce
Seguridad
Réplicas
Desarrollo de aplicaciones J2EE para
MongoDB

Material adicional online en: http://goo.gl/IsZPst




Macario Polo Usaola


www.fullengineeringbook.net
Profesor Titular de Universidad
Universidad de Castilla-La Mancha

www.fullengineeringbook.net

Capítulo 1. Introducción a JSON
1 Objetos o documentos en Mongodb con Java
1.1 Alternativa 1: org.bson.BSONObject
1.2 Alternativa 2: org.bson.Document
1.3 Alternativa 3: org.bson.BsonDocument
1.3.1 Un ejemplo: campos de tipo fecha
1.3.2 El ObjectId
Capítulo 2. Esquemas de almacenamiento de datos
1 Incrustación de objetos
2 Objetos no incrustados
3 Manipulación de una base de datos sintética con objetos incrustados
3.1 Creación de la base de datos desde Java
3.2 Consultas desde la consola de MongoDB
3.2.1 Proyecciones: consulta de unos pocos campos
3.2.2 Consultas con OR
3.2.3 Consultas con AND
3.2.4 Consultas mezclando AND y OR
3.2.5 Consultas con !=
3.2.6 Otros operadores lógicos
3.2.6.1 Ejemplos
3.2.7 Cursores
3.2.7.1 Ejemplos
Capítulo 3. Agregaciones
1 Operaciones de propósito simple
1.1 count()
1.2 distinct()
1.3 group()
1.4 Pipelines
1.4.1 $unwind
1.4.2 $group

www.fullengineeringbook.net
1.4.2.1 Agrupación por fechas
1.4.3 Cuenta del número total de llamadas
1.4.3.1 Número de llamadas recibidas por el 600000079
1.4.4 Operadores para $group
1.4.5 Operadores para los filtros
1.4.5.1 Operadores booleanos
1.4.5.2 Operadores para conjuntos
1.4.5.3 Operadores de comparación
1.4.5.4 Operadores aritméticos
1.4.5.5 Operadores de cadena
1.4.5.6 Operadores de búsqueda de texto
1.4.5.7 Operador para arrays
1.4.5.8 Operadores para variables
1.4.5.9 Operador para literales
1.4.5.10 Operadores para fechas
1.4.5.11 Operadores condicionales
1.4.5.12 Operadores de acumulación y totalización
Capítulo 4. Reglas MapReduce
1 Ejemplo básico: map, reduce y out
2 Prefiltrado con query
3 Adición de una función de finalización: finalize
4 Cálculo de la facturación mensual con MapReduce
5 Almacenamiento de scripts en el servidor
6 Más sobre el comando mapReduce
Capítulo 5. Índices
1 Uso de ensureIndex para crear índices
1.1 Índices únicos
1.2 Indexado de objetos y arrays incrustados
2 Consulta de los índices
3 Eliminación de índices
Capítulo 6. Arranque del servidor y control de acceso
1 Roles por defecto

www.fullengineeringbook.net
1.1 Roles a nivel de base de datos
1.2 Roles a nivel de servidor
1.3 Roles a nivel de clúster
1.4 Rol de súperusuario: root
2 Creación de usuarios
3 Modificación de permisos
4 Creación de roles
5 Otros comandos de administración
Capítulo 7. Gestión de réplicas
1 Conjuntos de réplica
1.1 Nodo primario
1.2 Nodos secundarios
1.3 Árbitros
1.4 Votaciones
1.5 Regreso al conjunto de réplicas de un nodo primario
2 Operaciones de lectura y escritura en los conjuntos de réplica
2.1 Configuración de operaciones de lectura: read preferences
2.2 Configuración de operaciones de escritura: el write concern
3 Creación de un conjunto de réplica
3.1 Creación del conjunto inicial
3.2 Caída de un nodo
3.3 Regreso del antiguo nodo primario
3.4 Adición de nodos
3.5 Consulta y modificación de la configuración del conjunto
3.5.1 Modificación de la configuración de la réplica
3.6 Cambio de la configuración de lectura
3.7 Cambio de la configuración de escritura
4 Arranque de los servidores con ficheros de configuración
Capítulo 8. Gestión de la base de datos desde una aplicación web Java
1 Creación de la base de datos sintética
1.1 Creación de la colección de Tarifas
1.2 Creación de la colección de Clientes

www.fullengineeringbook.net
1.3 Creación de la colección de Llamadas
2 Diseño de la aplicación
3 Un vistazo en detalle a una clase DAO
3.1 Delete
3.2 Update
3.3 Materialización de instancias (SELECT)
4 Ejecución de operaciones MapReduce
5 El MongoBroker
6 Utilización de réplicas
Capítulo 9. Índice alfabético

www.fullengineeringbook.net
www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 1. Introducción a JSON
MongoDB es un sistema de gestión de bases de datos no relacional. Lo que Mongo
almacena son objetos (a los que se llama documentos) en lugar de filas. El formato de
almacenamiento de objetos es del estilo de JSON.
JSON (JavaScript Object Notation) es un formato estandarizado para
representación de objetos, muy utilizado en aplicaciones web, en las que deben
intercambiarse objetos entre el cliente (normalmente un navegador) y el servidor
(desarrollado en cualquiera de las muchas tecnologías disponibles: PHP, J2EE, ASPX,
etcétera).
JSON es un mecanismo de representación de objetos estandarizado por ECMA
(European Computer Manufacturers Association) y ampliamente utilizado.
Un objeto JSON es una colección no ordenada de pares (nombre, valor). Un objeto
empieza y termina, respectivamente, por {}; entre medias, se colocan los pares separados
por comas: {“nombre” : “Pepe”} es un objeto con un campo nombre cuyo valor es la
cadena Pepe.
En la figura siguiente se muestra una clase Cliente y la represetnación en JSON de
un objeto de esta clase. El campo id es de tipo numérico (el valor no va entrecomillado),
mientras que el resto son de tipo cadena.

{
“id” : 1,
“nombre” : “Pepe”,
“apellido : “Pérez”,
“dni” : “37373737X”,
“telefono” : “985123456”
}

Figura 1

Los tipos de datos permitidos se resumen en la siguiente tabla:

Tipo de dato Ejemplo

String (en Unicode) “nombre” : “Pepe”

int “edad” : 30

real “salario” : 1200.50

potencia de 10 “atomos” : 1.65e6

Array “telefonos” : [“985123456”, “612123456”]

Objeto “amigo” : {

www.fullengineeringbook.net
“id” : 1,
“nombre” : “Pepe”,
“apellido : “Pérez”,
“dni” : “37373737X”,
“telefono”: “985123456”
}

Boolean “permitido” : true

null “proyecto” : null

Tabla 1

Para incluir caracteres especiales en los tipos de datos de tipo String disponemos
de los siguientes caracteres de escape:
\”
\
\/
\b
\f
\n
\r
\t
\u four-hex-digits: se usa para enviar cadenas con caracteres dependientes
del idioma. Por ejemplo: {“apellido” : “Žužemberk”} se codifica como
{“apellido” : “\u017du\u017eemberk”}. Véase Figura 2.

Figura 2

Como se ha visto en la tabla anterior, en JSON se permiten los arrays (que se


delimitan con corchetes) y los objetos anidados. En el lado derecho de la figura siguiente
se muestra en JSON un objeto de tipo Cliente que se corresponde con la estructura de
clases mostrada en el lado izquierdo: además de sus datos personales, tiene un array de
llamadas (cada una de las cuales es un objeto de tipo Llamada) y un objeto de tipo Tarifa
instanciado al subtipo TarifaFinDeSemana.

{
“id” : 1,
“nombre” : “Pepe”,
“apellido : “Pérez”,
“dni” : “37373737X”,

www.fullengineeringbook.net
“telefono” : “985123456”,
“llamadas” :
[ {
“origen” : “985123456”,
“destino” : “612123456”,
“duracion” : 12
},
{
“origen” : “985123456”,
“destino” : “985654321”,
“duracion” : 35
} ],
“tarifa” : {
“tipo” :
“TarifaFinDeSemana”,
“cuotaFija”: 25,
“establecimiento” :
0.35,

“importePorSegundo” : 0.01
}
}

Figura 3

Existen multitud de librerías, para todos los lenguajes de programación, para


manipular objetos JSON. Para Java, una de las más utilizadas es json-simple. El código de
la siguiente función construye un JSONObject como el mostrado en la Figura 3 a partir de
una instancia de tipo Cliente:
public JSONObject toJSONObject() {

JSONObject jso=new JSONObject();


jso.put(“id”, id);

jso.put(“nombre”, nombre);

jso.put(“apellido”, apellido);
jso.put(“dni”, dni);

jso.put(“telefono”, telefono);

JSONArray jsa=new JSONArray();


for (Llamada llamada : jsa) {

jsa.add(llamada.toJSONString());

}
jso.put(“llamadas”, jsa);

jso.put(“tarifa”, tarifa.toJSONObject());

return jso;

www.fullengineeringbook.net
}

Figura 4

Igual que a partir de una instancia de cualquier clase se construye una instancia de
JSONObject siguiendo el método indicado en la Figura 4 (el cual se representa como una
cadena de caracteres del estilo de la mostrada en la Figura 3), se puede ejecutar el proceso
inverso: es decir, construir un JSONObject a partir de una cadena de caracteres. Para ello,
en la librería json-simple se utiliza un JSONParser. La siguiente figura construye una
instancia de tipo Llamada a partir de una cadena recibida en formato JSON que representa,
en efecto, una llamada como las de la Figura 3. Si se produce algún error (por que la
cadena no tenga el formato esperado), se lanza una ParseException, que habrá que tratar
adecuadamente en donde corresponda.
public Llamada(String llamadaEnJSON) throws ParseException {
JSONParser parser=new JSONParser();

JSONObject jso=(JSONObject) parser.parse(llamadaEnJSON);


this.origen=jso.get(“origen”).toString();
this.destino=jso.get(“destino”).toString();

this.duracion=Integer.parseInt(jso.get(“duracion”).toString());
}

Figura 5

www.fullengineeringbook.net
1 OBJETOS O DOCUMENTOS EN MONGODB CON JAVA
Una base de datos MongoDB puede guardar los objetos en formatos diversos,
aunque muy parecidos todos:
Como instancias de alguno de los subtipos de la interfaz
org.bson.BSONObject.
Como instancias de org.bson.Document.
Como instancias de org.bson.BsonDocument.
1.1 Alternativa 1: org.bson.BSONObject
Las especializaciones de BSONObject (sobre todo BasicDBObject y BasicDBList,
Figura 6) son el tipo de objeto utilizado en las versiones de MongoDB anteriores a la 3.0.
Las versiones posteriores aún soportan estos tipos, pero se prefiere cualquiera de los otros
dos mecanismos. Por tanto, no los utilizaremos en estos apuntes.

Figura 6

1.2 Alternativa 2: org.bson.Document


Esta clase (Figura 7) es probablemente la más sencilla de utilizar. Sin embargo, no
se hace comprobación de tipos en los campos, con lo que es relativamente fácil que se
produzcan errores en tiempo de ejecución, sobre todo al leer los valores de los campos de
los objetos.

www.fullengineeringbook.net
Figura 7

En la siguiente figura se muestra el código de tres operaciones


toDocument():Document definidas en las tres clases Cliente, Llamada y Tarifa: la primera
construye el org.bson.Document correspondiente al objeto Cliente sobre el que se ejecuta
la operación: en este Document se colocan los campos id, nombre, apellido, dni y telefono,
también un array de Document de Llamadas (que se consigue llamando al toDocument de
Llamada, que aparece en la segunda fila de la figura) y un Document con la tarifa de este
cliente (que se consigue llamando al tercer toDocument que aparece).

public Document toDocument() {

Document r=new Document();

r.append(“id”, id);
r.append(“nombre”, nombre);

r.append(“apellido”, apellido);

r.append(“dni”, dni);
r.append(“telefono”, telefono);

Vector<Document> dLlamadas=new Vector<>();

for (Llamada llamada : llamadas)


dLlamadas.add(llamada.toDocument());

r.append(“llamadas”, dLlamadas);

r.append(“tarifa”, tarifa.toDocument());
return r;

www.fullengineeringbook.net
public Document toDocument() {

Document r=new Document();


r.put(“origen”, origen);

r.put(“destino”, destino);
r.put(“duracion”, duracion);
r.put(“fecha”, fecha.getTime());

return r;

public Document toDocument() {


Document r=new Document();

r.append(“tipo”, this.getClass().getSimpleName());

r.append(“cuotaFija”, this.getCuotaFija());
r.append(“establecimiento”, this.getEstablecimiento());

r.append(“importePorSegundo”, this.getImportePorSegundo());
return r;
}

Figura 8

1.3 Alternativa 3: org.bson.BsonDocument


El BsonDocument es type-safe. Es decir, los campos se almacenan en el documento
cada uno con su valor instanciado a alguno de los tipos Bson. Para entender esto mejor,
fijémonos en la siguiente figura, que muestra la vista del depurador de Eclipse. En ambos
lados se está representando el mismo objeto de tipo Cliente: en el lado izquierdo, como un
Document; en el derecho, como un BsonDocument. Vayamos bajando hasta la fila en la
que en ambos casos pone table y, dentro de ésta, examinemos el elemento con índice [7]:
este elemento, en ambos casos, guarda el campo apellido de este cliente. En el lado
izquierdo, su campo value es una String con el valor “MORO”, que es el apellido de este
cliente (véase que, en efecto, el campo key de este elemento de índice [7] es “apellido”);
en el lado derecho, su key sigue siendo la misma (“apellido”), pero su value es ahora un
BsonString.

El mismo objeto Cliente, pero representado como un…

Document BsonDocument

www.fullengineeringbook.net
Figura 9. El mismo objeto, visto de dos formas distintas

Si nos fijamos ahora en el elemento de índice [1] (cuya key es “tarifa”), vemos en
el lado izquierdo que su value es de tipo Document, y de tipo BsonDocument en el lado
derecho. Si expandimos este campo value en ambos lados (Figura 10), vemos a la
izquierda que el campo tipo del objeto es directamente la cadena “Tarifa50Minutos” y que
el campo establecimiento es un Double; a la derecha, sin embargo, el tipo es un
BsonString y el establecimiento un BsonDouble.

El mismo objeto Tarifa, pero representado como un…

Document BsonDocument

Figura 10. El mismo objeto visto de dos formas diferentes

www.fullengineeringbook.net
Es decir que, aunque con ambos tipos de documentos (Document y
BsonDocument) podemos representar las mismas realidades, BsonDocument nos da un
grado adicional de “blindaje”, pues nos obliga a especificar el tipo Bson de cada uno de
los campos del objeto. Así pues, en la medida de lo posible, procuraremos utilizar
BsonDocument en nuestros desarrollos.
Los tipos BsonString, BsonDouble y el mismísimo BsonDocument son
especializaciones de BsonValue: el tipo de cualquier campo de un objeto representado en
forma de BsonDocument debe ser una de las especializaciones de BsonValue. Esta
jerarquía de tipos se muestra en la Figura 11: los campos de un objeto representado como
un BsonDocument pueden ser de cualquiera de esos tipos que se muestran: el id es un
BsonInt32; el nombre, un BsonString; la lista de llamada, un BsonArray; la tarifa, un
BsonDocument. Estos nuevos tipos de datos hacen que, probablemente, el BsonDocument
sea el formato más apropiado para manipular los objetos.

Figura 11. Especializaciones de BsonValue

En la Tabla 2 se muestran algunos ejemplos de valores de estos tipos:

Tipo de dato Observaciones

String (en Unicode) “nombre” : “Pepe”

Array “telefonos” : [“985123456”, “612123456”]

Document “amigo” : {
“id” : 1,
“nombre” : “Pepe”,
“apellido : “Pérez”,
“dni” : “37373737X”,
“telefono”: “985123456”
}

www.fullengineeringbook.net
Boolean “permitido” : true

null “proyecto” : null

double número real de 64 bits

int32 número entero de 32 bits

int64 número entero de 64 bits

binary array de bytes (una imagen o un audio, por ejemplo)

ObjectId 12 bytes

Date número entero int64 que representa la fecha UTC

Tabla 2

La forma de transformar un objeto Cliente a un BsonDocument es muy parecida,


pero se fuerza ahora a instanciar campo a uno de los subtipos de BsonValue: véase en la
figura siguiente cómo ahora, en todos los append, creamos el subtipo de BsonValue
adecuado al campo.
public BsonDocument toBsonDocument() {
BsonDocument r=new BsonDocument();
r.append(“id”, new BsonInt32(id));

r.append(“nombre”, new BsonString(nombre));


r.append(“apellido”, new BsonString(apellido));
r.append(“dni”, new BsonString(dni));

r.append(“telefono”, new BsonString(telefono));


BsonArray llamadas=new BsonArray();

for (Llamada llamada : this.llamadas)


llamadas.add(llamada.toBsonDocument());

r.append(“llamadas”, llamadas);

r.append(“tarifa”, tarifa.toBsonDocument());
return r;

public BsonDocument toBsonDocument() {

BsonDocument r=new BsonDocument();

r.append(“origen”, new BsonString(origen));

r.append(“destino”, new BsonString(destino));


r.append(“duracion”, new BsonInt32(duracion));

r.append(“fecha”, new BsonDateTime(fecha.getTime().getTime()));

return r;
}

public BsonDocument toBsonDocument() {


BsonDocument r=new BsonDocument();

www.fullengineeringbook.net
r.append(“tipo”, new BsonString(this.getClass().getSimpleName()));

r.append(“cuotaFija”, new BsonDouble(this.getCuotaFija()));


r.append(“establecimiento”, new BsonDouble(this.getEstablecimiento()));

r.append(“importePorSegundo”, new BsonDouble(this.getImportePorSegundo()));


return r;
}

Figura 12

1.3.1 Un ejemplo: campos de tipo fecha


En la clase Llamada que mostrábamos en la Figura 3 no había campo para
representar la fecha y hora de inicio de la llamada. Al incluirse el tipo BsonDateTime,
podemos añadir dicho campo (que, en JSON, podríamos haber representado como un int)
y manipularlo con operaciones propias de este tipo de dato. En el lado izquierdo de la
Figura 13 se ha añadido un campo fecha de tipo java.util.Calendar a la clase Llamada; en
el lado derecho se muestra el método toBsonDocument(), que devuelve un
org.bson.BsonDocument con el campo fecha instanciado a un tipo BsonDateTime. Como
ya hemos dicho, el resto de campos se colocan en el objeto resultado instanciados al tipo
que corresponde.
public BsonDocument toBsonDocument() {

BsonDocument r=new BsonDocument();


r.append(“origen”, new BsonString(origen));
r.append(“destino”, new BsonString(destino));

r.append(“duracion”, new BsonInt32(duracion));


r.append(“fecha”,
new BsonDateTime(fecha.getTime().getTime()));

return r;
}

Figura 13

1.3.2 El ObjectId
Los campos de tipo ObjectId identifican unívocamente a cada objeto, lo que en
bases de datos es fundamental. Este tipo consta de 12 bytes que se interpretan de la
siguiente forma:

0 1 2 3 4 5 6 7 8 9 10 11

Timestamp Máquina PID Incremento

Los cuatro bytes del timestamp representan el segundo en el que el objeto se ha


creado (desde el 1 de enero de 1970). Los tres bytes de la máquina proceden de calcular
un hash sobre el nombre de host de la máquina. Los dos bytes del PID corresponden al
identificador del proceso, dentro de la máquina, que creó el objeto. Los tres últimos bytes
se van autocincrementando a medida que el mismo proceso de la misma máquina en el
mismo segundo inserta un objeto. Al dedicarse 3 bytes (24 bits) al incremento, se podrían
insertar hasta 224=16.777.216 objetos en un segundo.
www.fullengineeringbook.net
Los dos siguientes valores corresponden al identificador de dos objetos insertados
en una colección de Mongo, con 2 segundos de diferencia, en la misma máquina y por el
mismo proceso:

Timestamp Máquina PID Incremento

56 34 cd 30 4e 89 7e 2d c4 03 1e 5c

56 34 cd 32 4e 89 7e 2d d4 03 1e 5d

Figura 14

Cada vez que insertamos un objeto nuevo en la base de datos, el propio MongoDB
le asigna un campo “_id” de tipo ObjectId.

www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 2. Esquemas de almacenamiento de
datos
Como hemos dicho, Mongo almacena los objetos como documentos en la base de
datos, bien como instancias de BSONObject, de Document o de BsonDocument. Igual que
sucede con otros modelos de datos (el relacional, por ejemplo), para un mismo diagrama
de clases existen diferentes formas de almacenamiento de sus objetos en Mongo.
Utilizaremos como ejemplo el ya conocido diagrama de clases de los clientes, las
llamadas y las tarifas:

Figura 15

www.fullengineeringbook.net
1 INCRUSTACIÓN DE OBJETOS
En este modelo, los objetos de tipo Cliente se almacenan en Mongo con su tarifa y
su colección llamadas en un solo documento (como el ejemplo del cliente de la Figura 3).
La figura siguiente muestra un pantallazo de la consola de Mongo en la que se ven
los datos del cliente CRISTIAN SANTANA, que realizó dos llamadas el 1 y 2 de junio de
2015: además de ilustrarse el campo _id (que, por cierto, no tiene nada que ver con el id
que le sigue), se muestra el BsonArray con estas llamadas.

Figura 16

Podemos imaginar que, en la base de datos, a continuación de Silvia vendrán los


datos de otro cliente con sus llamadas, luego de otro con las suyas, luego de otro, etcétera.
Si las llamadas de cada instancia de Cliente las almacenamos como un array de objetos
incrustados en el propio BsonDocument que representa a cada cliente, cada vez que el
cliente realice una llamada nueva, habrá que actualizar este array incrustado, desplazando
la ubicación física en disco de gran cantidad de información. Considerando el número de
clientes y el número de llamadas simultáneas, esta solución de almacenamiento no es
eficiente si los arrays incrustados van a cambiar con frecuencia, pues exige el movimiento
físico de tremendas cantidades de información.
Algo parecido sucede con el almacenamiento de las tarifas como objetos
incrustados: si la compañía telefónica ofrece, por ejemplo, cuatro tipos de tarifas (por
ejemplo: Tarifa 50 minutos, Tarifa tardes, Tarifa fin de semana y Tarifa plana), no es muy
eficiente almacenar el objeto completo de tipo Tarifa dentro del documento
correspondiente a cada cliente: si la Tarifa fin de semana cambia sus precios y este cambio
afecta a 100.000 clientes, tendremos que realizar 100.000 actualizaciones en los 100.000
documentos correspondientes a estos 100.000 clientes.

www.fullengineeringbook.net
2 OBJETOS NO INCRUSTADOS
Al usar objetos no incrustados, el esquema de datos es más parecido a una base de
datos relacional clásica: en efecto, guardaremos los clientes en una colección, las tarifas en
otra y las llamadas en otra. Como cada documento de cada colección va a tener su _id,
cada Cliente conocerá el _id de su Tarifa, y cada Llamada almacenará el _id del Cliente
que la ha realizado.

www.fullengineeringbook.net
3 MANIPULACIÓN DE UNA BASE DE DATOS SINTÉTICA CON
OBJETOS INCRUSTADOS
Antes de continuar, debemos poner en marcha el servidor de base de datos, cuyo
modo más básico se explica en el Capítulo 6 (página 67).
3.1 Creación de la base de datos desde Java
Con el fin de ir aprendiendo a manipular bases de datos Mongo desde Java, en esta
sección crearemos una con datos sintéticos (es decir, artificiales). Como origen de datos,
utilizaremos el listado de los 100 nombres (de hombres y mujeres) y los 100 apellidos más
frecuentes publicados en la página web del Instituto Nacional de Estadística[1]. En la web
original, estos datos están todos en mayúsculas y sin tildes, y así los hemos mantenido.
En esta sección utilizaremos org.bson.Document como formato de almacenamiento
y manipulación de los objetos. Utilizaremos la estructura de clases de la Figura 17. Es
muy parecida a la Figura 15, pero:
Los clientes tienen dos apellidos (no solamente uno) y una fecha de
nacimiento, y habrá cuatro tipos de tarifas.
Se han sustituido las asociaciones UML por composiciones. Las
composiciones (o “agregaciones fuertes”) indican que el objeto agregado
“está dentro” del objeto agregante. Es decir, nace después del objeto
agregante y muere, como muy tarde, a la vez. En nuestro modelo de objetos
incrustados, tanto las llamadas como la tarifa están realmente dentro del
objeto cliente, con lo que es más correcto especificar estas relaciones como
composiciones que como asociaciones.

Figura 17. Modelo con objetos incrustados

El procedimiento de creación de la base de datos será el siguiente:


1) En una constante NUMERO_DE_CLIENTES especificaremos el número
máximo de clientes que queremos crear. En princpio crearemos 10, pero luego
será interesante crear muchos más para ser conscientes de la influencia que el
tipo de esquema de datos tiene en el rendimiento.
2) Crearemos un Vector<String> con los nombres y otro con los apellidos
contenidos en los ficheros.

www.fullengineeringbook.net
3) Iteraremos en dos bucles anidados sobre los nombres y apellidos. En cada
iteración crearemos un objeto Java de tipo Cliente de la siguiente manera:
1. Al azar, obtendremos de los dos vectores un nombre, un apellido1 y un
apellido2.
2. Generaremos un DNI al azar, que será el valor 5.000.000 al que
sumaremos un número al azar entre 0 y 5.000.000.
3. Genraremos un número de teléfono autoincrementable, que empezará
por 600-000-000 (el 600-000-000 será el número del primer cliente, el
600-000-001 el del segundo, etcétera).
4. Generaremos una fecha de nacimiento al azar.
5. Generaremos un número al azar entre 0 y 3: si es 0, el cliente tendrá
Tarifa plana; si 1, Tarifa 50 minutos; si 2, Tarifa fin de semana; si 3,
Tarifa de tardes.
6. A continuación, generaremos para el cliente llamadas al azar, desde
mayo hasta octubre de 2015. Conseguiremos que cada cliente realice un
tercio de sus llamadas a otro cliente, simulando así un número favorito
de cada cliente (un familiar, por ejemplo).
7. Ya que en este punto tendremos toda la información necesaria de un
cliente, lo guardaremos en la base de datos e iteraremos de nuevo.
El código de carga de datos lo escribiremos en una clase CargadoraSintetica, que
se muestra en las siguientes figuras:
public static void main(String[] args) throws IOException {
final int NUMERO_DE_CLIENTES = 10;
MongoClient mongoClient=new MongoClient(“localhost”, 27017);

MongoDatabase db = mongoClient.getDatabase(“telefonosIncrustados”);
if (db.getCollection(“clientes”)==null)
db.createCollection(“clientes”);


MongoCollection<Document> clientes=db.getCollection(“clientes”);

String nombresFileName=” nombres.txt”;

String apellidosFileName=“/apellidos.txt”;
Vector<String> nombres=read(nombresFileName);

Vector<String> apellidos=read(apellidosFileName);

int cont=0;
Random dado=new Random();

for (int i=0; i<nombres.size()*apellidos.size(); i++) {

int posNombre=dado.nextInt(nombres.size());

int posApellido1=dado.nextInt(apellidos.size());
int posApellido2=dado.nextInt(apellidos.size());

int iDNI=5000000+dado.nextInt(5000000);

int iTelefono=600000000+i;
int tarifa=dado.nextInt(4);

String nombre=nombres.get(posNombre);

www.fullengineeringbook.net
String apellido1=apellidos.get(posApellido1);

String apellido2=apellidos.get(posApellido2);
String dni=””+iDNI;

String telefono=””+iTelefono;
Cliente c=new Cliente((cont+1), nombre, apellido1, apellido2, dni, telefono, tarifa);
int favorito; String sFavorito;

do {

favorito=(600000000 + dado.nextInt(NUMERO_DE_CLIENTES));
sFavorito=”” + favorito;

} while (sFavorito.equals(telefono));

addLlamadas(c, sFavorito, NUMERO_DE_CLIENTES);


Document d=c.toDocument();

clientes.insertOne(d);

cont++;
if (cont==NUMERO_DE_CLIENTES)

break;
}
mongoClient.close();

Figura 18. Función principal de carga de datos

private static Vector<String> read(String fileName) throws IOException {

Vector<String> result=new Vector<>();


FileInputStream fis = new FileInputStream(fileName);
DataInputStream dis = new DataInputStream(fis);

BufferedReader br = new BufferedReader(new InputStreamReader(dis));


String linea;
while ((linea = br.readLine()) != null) {

result.add(linea);

}
fis.close();

return result;

Figura 19. Función auxiliar para leer los ficheros nombres.txt y apellidos.txt

private static void addLlamadas(Cliente c, String favorito, int numClientes) {


Random dado=new Random();

for (int mes=5; mes<=10; mes++) {

for (int dia=1; dia<=30; dia++) {


int numLlamadas=dado.nextInt(4);

for (int j=0; j<numLlamadas; j++) {

int n=dado.nextInt(numClientes);
String telefonoDestino=”” + (600000000 + n);

if (c.getTelefono().equals(telefonoDestino))

www.fullengineeringbook.net
continue;

int duracion=dado.nextInt(600);
int year=2015;

int hora=dado.nextInt(24), minuto=dado.nextInt(60), segundo=dado.nextInt(60);


if (dado.nextFloat()<0.3)
telefonoDestino=favorito;

Llamada llamada=new Llamada(c.getTelefono(), telefonoDestino, duracion, year,


mes,

dia, hora, minuto, segundo);


c.addLlamada(llamada);

}
}

Figura 20. Función auxiliar para crear llamadas

private static GregorianCalendar fechaAlAzar() {


GregorianCalendar result = new GregorianCalendar();

int year = randBetween(1930, 2010);


result.set(GregorianCalendar.YEAR, year);
int dayOfYear = randBetween(1, result.getActualMaximum(GregorianCalendar.DAY_OF_YEAR));

result.set(GregorianCalendar.DAY_OF_YEAR, dayOfYear);
return result;
}


private static int randBetween(int start, int end) {
return start + (int)Math.round(Math.random() * (end - start));

Figura 21. Función auxiliar para generar una fecha de nacimiento (tomada de
http://stackoverflow.com/questions/3985392/generate-random-date-of-birth)

3.2 Consultas desde la consola de MongoDB


Una vez creados y almacenados los datos, podemos comenzar a hacer consultas
sobre ella.
En las primeras líneas del código de la Figura 18aparecía la sentencia MongoDatabase
db = mongoClient.getDatabase(“telefonosIncrustados”); . Esta sentencia devuelve en db una referencia a
la base de datos telefonosIncrustados; si tal base de datos no existiera, la crea y se
devuelve en db.
En el servidor podemos tener multitud de bases de datos. Cada base de datos puede
tener una o más colecciones, que más o menos equivalen a las tablas de una base de datos
relacional. Dentro de cada colección tenemos documentos, que más o menos son las tuplas
o filas de una tabla del modelo relacional.
Para conocer qué base de datos tenemos activa, en la consola escribimos el
comando db. Nada más entrar a la consola, la base de datos por defecto es test; como
www.fullengineeringbook.net
queremos usar telefonosIncrustados, ejecutamos use telefonosIncrustados .

Figura 22

En telefonosIncrustados tenemos solamente una colección llamada clientes.


Saquemos un listado de sus datos. El formato básico es db.nombreDeColección.find() :

Figura 23

Aunque hemos metido datos sólo de diez clientes, el listado de la Figura 23 es


bastante grande, pues cada cliente incluye sus llamadas y su tarifa. Si nos fijamos en el
método addLlamadas (Figura 20), a cada cliente le creamos entre 0 y 3 llamadas cada uno
de los 30 días de cada mes entre mayo y octubre de 2015, con lo que la cantidad de datos
es bastante grande.
3.2.1 Proyecciones: consulta de unos pocos campos
En general, todos los comandos que lancemos sobre Mongo o no tomarán
parámetros o, si los toman, irán en forma de objetos JSON. El comando find puede tomar
parámetros, construyéndose de la siguiente manera:
find(criterios, proyección)

Saquemos un listado de los nombres los clientes: db.clientes.find({}, {nombre : 1}) :


El criterio ( {} ) es un filtro de consulta y, de momento, es un JSON vacío.
El segundo ( {nombre : 1} ) es un objeto JSON en el que decimos que nos
interesa listar solamente el campo nombre.
El resultado se muestra en la Figura 24:

www.fullengineeringbook.net
Figura 24

Además del nombre aparece el _id. Este campo aparece por defecto; si queremos
excluirlo:

Figura 25

A continuación listamos sólo nombre y dos apellidos:

Figura 26

3.2.2 Consultas con OR


A continuación especificamos algunos criterios: en el primer caso, buscamos a la
clienta cuyo nombre es “MIRIAM”; en el segundo, a todas las personas que tengan
“BENITEZ” como primer o segundo apellido:

Figura 27

El OR, por tanto, es un objeto JSON cuyo nombre de campo es $or y cuyo valor es
un array JSON que contiene los campos y valores por los que queremos restringir el
filtrado de datos:
{

www.fullengineeringbook.net
$or : [

{ nombre : “MANUEL”},
{ nombre : “MIRIAM”}

]
}

Figura 28

Podemos buscar con OR de otra manera: si nos interesan todas las personas cuyo
primer apellido sea “BENITEZ” o “SANZ”, podemos usar el operador $in para que
busque el apellido1 en un array de valores:

Figura 29

El criterio, en este último caso, tiene la siguiente representación:


{
apellido1 : {
$in : [“SANZ”,
“BENITEZ”]
}

Figura 30

Hasta que nos familiaricemos con la notación, podemos dibujarnos arbolitos


sintácticos. Por ejemplo, si queremos recuperar todos los clientes cuyo primer o segundo
apellido sea “SANZ” o “BENITEZ”, el arbolito para el criterio podría ser como el de la
Figura 31, en el que tenemos un campo de nombre $or cuyo valor es el array de nodos que
hay a la derecha y que son de su mismo nivel. Estos dos nodos son dos $in, cuyos
nombres de campo son apellido1 y apellido2 y cuyos respectivos valores son los dos
arrays que se muestran:

Figura 31. Árbol sintáctico para representar un criterio

Traducimos con cuidado el árbol a un criterio en formato de texto JSON:

www.fullengineeringbook.net
Figura 32

Y ya, por fin, escribimos la consulta con cuidado de no equivocarnos: como


criterio tenemos el $or que hemos construido, y como proyección (segundo parámetro) los
campos que queremos mostrar:

Figura 33

3.2.3 Consultas con AND


Para hacer consultas con AND, basta con poner como criterio un solo objeto JSON
en el que asignamos los valores buscados a aquellos campos que nos interesan. Por
ejemplo, db.clientes.find({nombre : “MIRIAM”, apellido1 : “RAMIREZ”}) devuelve todos aquellos
objetos correspondientes a MIRIAM RAMIREZ, sin importar el resto de campos.
3.2.4 Consultas mezclando AND y OR
Obviamente podemos mezclar AND y OR. En el siguiente árbol buscamos a
“MIRIAM RAMIREZ” o a “GUILLERMO BENITEZ”:

Figura 34

La traducción a texto es sencilla:

www.fullengineeringbook.net
Figura 35

Y, por fin, la consulta y su resultado:

Figura 36

3.2.5 Consultas con !=


Si queremos un listado de todos los clientes que no se llamen “MIRIAM” usamos
el operador distinto (not equal): $ne. Como en todos los ejemplos anteriores, lo que le
vamos a decir a Mongo es más o menos lo siguiente: “Dame todos aquellos objetos que
tengan la estructura {nombre : todo-lo-que-no-sea-MIRIAM}”. El fragmento todo-lo-que-
no-sea-MIRIAM lo traducimos a la siguiente expresión: {$ne : “MIRIAM”}.
A continuación mostramos el arbolito, el texto procedente del arbolito y la consola:

Figura 37

3.2.6 Otros operadores lógicos


Los operadores lógicos que ofrece Mongo son los de la Tabla 3:

Operador Significado

www.fullengineeringbook.net
$gt >

$gte >=

$lt <

$lte <=

$ne !=

$in Incluido en un array

$nin No incluido en un array

Tabla 3

3.2.6.1 Ejemplos
Personas cuyo nombre empieza por letras anteriores a la “O”:
a) db.clientes.find({nombre : {$lt : “O”}}, {nombre : 1, _id : 0})
b) db.clientes.find({nombre : {$lte : “N”}}, {nombre : 1, _id : 0})
Personas que no se llaman ni “MIRIAM” ni “GUILLERMO”:
a) db.clientes.find({nombre : {$ne : “MIRIAM”, $ne : “GUILLERMO”}}, {nombre : 1, _id : 0})
b) db.clientes.find({nombre : {$nin : [“MIRIAM”, “GUILLERMO”]}}, {nombre : 1, _id : 0})
3.2.7 Cursores
Los datos devueltos por la operación find se llaman cursores. Un cursor es una
colección de documentos. O sea que, algebraicamente:
find : Collection Cursor
Los cursores ofrecen multitud de operaciones que pueden devolver también
cursores. De cualquiera de las operaciones find de las secciones anteriores obtenemos un
cursor. Podemos usar el cursor para, por ejemplo, sacar un listado ordenado: lo primero
que hacemos es obtener el cursor con el find y, luego, aplicarle a éste un sort. A
continuación ordenamos los clientes por apellidos y nombre. Primero, de manera
ascendente (Figura 38); luego, de manera descendente (Figura 39). Obsérvese de paso, en
estas figuras, que la consola permite introducir comandos en varias líneas.

Figura 38

www.fullengineeringbook.net
Figura 39

Podríamos hacer algo un tanto absurdo: sacar el listado ordenado por apellidos y
nombre y, el resultado, ordenarlo por nombre:

Figura 40

Los tipos de datos que tenemos en cada llamada son los siguientes:

db. clientes. find({…}, {…}). sort({…}). sort({…})

Database

Collection

Cursor

Cursor

Cursor

Figura 41

Los cursores se recorren de principio a fin. Las operaciones más interesantes que
tenemos para manipular cursores son las siguientes:

www.fullengineeringbook.net
count(), length() y size(): devuelven el número de objetos en el cursor.
limit(n : natural): devuelve los n primeros objetos del cursor.
skip(n : natural): salta al objeto enésimo.
sort(criterio : JSON): ordena por el criterio especificado.
hasNext() : boolean: devuelve true si hay más elementos después del
elemento al que punta el puntero del cursor, y false en caso contrario.
next() : Document: devuelve el documento actual (aquel al cual apunta el
puntero del cursor).
3.2.7.1 Ejemplos
Número de elementos en una colección:
a) db.clientes.find().size()
b) db.clientes.find().length()
c) db.clientes.find().count()
Todos los datos del primer cliente de la colección:
db.clientes.find().next()


Nombre del primer cliente de la colección:
db.clientes.find().next().nombre


En este último ejemplo, obsérvese que:

db. clientes. find(). next(). nombre

Database

Collection

Cursor

Document

String

Figura 42

www.fullengineeringbook.net

Nombre y apellidos del primer cliente de la colección. En este caso, asignaremos
el cliente a una variable utilizando notación JavaScript. Luego, accedemos a los campos
que nos interesen:

Figura 43

www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 3. Agregaciones
Las agregaciones son conjuntos de operaciones que procesan conjuntos de objetos
y realizan algún tipo de cálculo con ellos. Hay tres mecanismos básicos para realizar
agregaciones:
Operaciones de propósito simple
Pipelines
Reglas MapReduce

www.fullengineeringbook.net
1 OPERACIONES DE PROPÓSITO SIMPLE
En esta categoría encontramos operaciones muy sencillas y limitadas, pero que
ofrecen resultados potentes de forma muy cómoda.
1.1 count()
Devuelve el número de objetos (o documentos) en la colección.
db.clientes.count()

La operación count() también puede ejecutarse sobre cursores que, como se


recordará, es el resultado que devuelve find.
1.2 distinct()
Devuelve un array con los valores del campo que se pasa como parámetro. Por
ejemplo, el resultado que devuelve db.clientes.distinct(“nombre”) es de este estilo:
[“DANIELA”, “VICTOR MANUEL”, “XAVIER”, …, ]

Puesto que es un array, podemos acceder a cualquiera de sus elementos por su


posición:
db.clientes.distinct(“nombre”)[0] muestra “DANIELA” .

1.3 group()
Esta operación equivale al Select… group by de SQL. Es decir, agrupa los
elementos de la colección de acuerdo a algún campo y realiza con ellos un cálculo que
normalmente es sencillo pero que puede ser muy complicado.
Saquemos, a partir del esquema de la figura Figura 17, un listado de los clientes
con el número de llamadas que ha hecho cada uno:

Figura 44

En el código de la Figura 44, group toma como argumento un JSON que puede
tener hasta cuatro campos. En el ejemplo estamos utilizando estos tres:
key: objeto JSON en el que listamos los campos que queremos mostrar. En
el ejemplo, son los campos nombre, apellido1 y apellido2. Los ponemos a
1, pero realmente da igual el valor que asignemos.
reduce: función en JavaScript con la operación que se desea hacer. Esta
función toma siempre dos argumentos:
El primero es el objeto actual, sobre el que se está iterando. Lo
hemos llamado cliente porque, en este ejemplo, la función de

www.fullengineeringbook.net
agrupación se ejecuta sobre la colección de clientes, con lo que en
cada iteración se procesa un cliente.
El segundo es un objeto que representa el resultado, y por eso lo
llamamos así. En este objeto creamos uno o más campos (en este
caso sólo uno, llamadas) al que asignamos el cómputo deseado (la
longitud del array de llamadas del cliente actual).
initial: aquí asignamos el valor inicial al campo o campos del objeto
resultado que utilizamos en la función reduce.
La función del ejemplo devuelve un array JavaScript de objetos (lado izquierdo de
la Figura 45). Si el comando de la Figura 44 lo hubiéramos asignado a una variable x (es
decir: x=db.clientes.group(…) ), podríamos acceder a cualquier elemento del array mediante su
posición (lado derecho de la Figura 45).

Figura 45

Arrays en JavaScript
Es importante tener en cuenta que el resultado devuelto por group() es, como
hemos dicho, un array, no una collection ni un cursor, por lo que no
podemos utilizar operaciones como findOne, find, next, etcétera. Para
manipularlo este resultado desde la consola de Mongo, podemos usar las
funciones de JavaScript para arrays: entre otras, las de la Tabla 4:

Propiedad u operación Significado

length Devuelve el número de elementos

pop() Elimina el último elemento y lo devuelve

push(objeto1, … objeton) Añade al final del array el objeto u objetos que


se pasan como parámetros

reverse() Le da la vuelta al array

shift() Elimina el primer elemento y lo devuelve

sort() Ordena los elementos convirtiéndolos a strings

www.fullengineeringbook.net
sort(funciónDeOrdenaciónEnJS) Ordena los elementos utilizando para ello la
función en JavaScript que se pasa como
parámetro

unshift(objeto1, … objeton) Añade al principio del array el objeto u objetos


que se pasan como parámetros

Tabla 4

Los arrays en JavaScript admiten cualquier tipo de objeto: es decir, podemos


guardar en un array una cadena, luego un entero, luego una fecha, luego un
cliente… Así pues, sería legal guardar en una variable x el resultado de un
db.clientes.group(…) y, luego, añadirle lo que queramos. Por ejemplo, en la
Figura 46 asignamos a x el array producido por el último group. En el lado
izquierdo de la Figura 47 se muestra el final del array x y, en el lado derecho,
añadimos a x dos objetos y los mostramos: son objetos que no tienen nada
que ver con el resto de elementos del array; pero como legal, la construcción
es legal.

Figura 46

Figura 47


Otro de los posibles campos que podemos usar en el group es cond, con el que
especificamos una condición sobre los objetos que deseamos agrupar. Por ejemplo:
número de llamadas de los clientes nacidos a partir del 1 de enero de 1997 (o sea, aquellos
cuya fecha de nacimiento sea posterior al 31/12/1996: ojo al formato de la fecha, que ha
de ir como mes/día/año: en otro caso, Mongo no nos advierte de ningún error, pero los
resultados pueden no ser los deseados):

www.fullengineeringbook.net
Figura 48

La función reduce puede ser tan compleja como queramos. Compliquemos un


poco el ejemplo de la Figura 48 para que muestre también la duración de la llamada más
larga:

Figura 49

Obsérvese que, además del código que hemos añadido a la propia función
(declaración de la variable duracionMaxima, el bucle y la asignación al campo
resultado.duracionMaxima), en el campo initial establecemos el valor inicial del campo
resultado.duracionMaxima.
Igualmente, podemos agrupar o poner condiciones sobre campos de objetos
incrustados. Si queremos restringir la consulta de la Figura 49 a aquellos clientes que
tengan la tarifa Tarifa tardes, restringimos por tarifa.tipo=“TarifaTardes” mediante el
campo cond:

www.fullengineeringbook.net
Figura 50

Un detalle importante en el ejemplo anterior es que debemos entrecomillar


tarifa.tipo, para que Mongo entienda esta cadena como un campo dentro de un objeto
incrustado.
Si en el listado nos interesase incluir los datos de los clientes con TarifaTardes o
Tarifa50Minutos, podemos sustituir el valor del campo tarifa.tipo para que, en lugar de
valer “TarifaTardes”, valga la expresión $in que se ilustra:

Figura 51

De la figura anterior deducimos que, cuando con el OR queremos restringir los


valores de un solo campo (tarifa.tipo en el ejemplo) a un conjunto de elementos, usamos
$in, acordándonos de que el valor del $in es un array de objetos. Si el OR se aplica a
varios campos o a un solo campo pero con rangos de valores, usaremos $or.
Por ejemplo, si queremos los datos de las consultas anteriores pero restringidos a
los clientes nacidos después de 1996 o antes de 1950, escribimos un $or:

www.fullengineeringbook.net
Figura 52

Para finalizar con el group, en el siguiente ejemplo restringimos un poco más: los
mismos datos pero para los clientes que tengan la TarifaTardes o la Tarifa50Minutos:

Figura 53

1.4 Pipelines
Un pipeline es una secuencia de comandos. En la consola de Unix, por ejemplo, se
utilizan mucho, separando los distintos comandos con la barra vertical (|).
A los pipelines se los llama también modelo de tuberías y filtros: un filtro recibe
datos de entrada de una tubería, los procesa y los saca por otra tubería. Los Diagramas de
Flujo de Datos se corresponden también con modelos de pipeline. En Mongo, a las
ocurrencias de cada filtro se las llama “etapas” (stages).
Un pipeline se inicia con la operación aggregate aplicada a una colección.
aggregate toma como parámetros una lista de los filtros que desean aplicarse. Así pues, la
colección es la primera “tubería”, y a estos datos se les van aplicando los diversos filtros

www.fullengineeringbook.net
hasta llegar al último, que produce la salida que se haya programado.

$aggregate en versiones anteriores


Antes de la versión 3, el resultado devuelto por aggregate era un objeto
con dos campos:
result, que es un array con los objetos que componen el

resultado.
ok, que vale 1 si el aggregate ha funcionado correctamente.

Por ejemplo, en el result de la siguiente instrucción guardamos en x el


array de todos los clientes con Tarifa50Minutos:
x=db.clientes.aggregate(
{$match : {
“tarifa.tipo” : “Tarifa50Minutos”
}
})

Para conocer el número de clientes con esa tarifa, escribiríamos:


x.result.length .


El filtro utilizado en el ejemplo del cuadro anterior anterior es el $match, que
devuelve sin cambios cada documento (u objeto) que casa o “matchea” con la condición.
Pero hay varios más:

Filtro Descripción Ejemplo

$project Indica los campos que se van a db.clientes.aggregate(


seleccionar de cada documento
{
$project : {
“tarifa.tipo” : 1
}
}
)

$match Filtra los objetos que coinciden db.clientes.aggregate(


con el criterio
{
$match : {
“tarifa.tipo” :
“Tarifa50Minutos”
}
})

www.fullengineeringbook.net
$limit Limita el nº de objetos en el db.clientes.aggregate(
resultado
{ $limit : 20}
)

$skip Salta al elemento que se db.clientes.aggregate(


indique
{ $skip:10 }
)

$unwind Desagrupa o “aplana” los Ver más adelante (sección 1.4.1)


elementos de un campo array
contenido en los objetos de una
colección

$group Agrupa documentos de acuerdo Ver más adelante


a algún criterio

$sort Ordena según uno o más db.clientes.aggregate(


campos de ordenación
{
$project : {
nombre : 1,
telefono : 1,
_id : 0
}
},
{$sort :
{
telefono:1
}
}
)

$geoNear Devuelve los objetos ordenados


según su proximidad a un punto
geoespacial

Tabla 5. Filtros

1.4.1 $unwind
Supongamos que tenemos la colección de la figura siguiente, que muestra
acortadamente una posible lista de clientes. El primero tiene un array de 10 llamadas, el
segundo de 8 llamadas y el tercero, 12. Al aplicar $unwind, se obtiene un array formado
por 10+8+12=30 objetos. En cada uno de estos objetos se almacenan los campos del
cliente y los campos de la llamada:

Colección $unwind : llamadas

www.fullengineeringbook.net
{ nombre : “Pepe”, [
llamadas : [ {
{ destino : “600000000”, duracion : 20}, nombre : “Pepe”,
{ destino : “607000000”, duracion : 15}, llamadas : { destino : “600000000”,
… duracion : 20}
]}, },
{ nombre : “Ana”, {
llamadas : [ nombre : “Pepe”,
{ destino : “600245000”, duracion : 18}, llamadas : { destino : “607000000”,
{ destino : “604500000”, duracion : 11}, duracion : 15}
… },
]}, …
… {
{ nombre : “Juan”, nombre : “Ana”,
llamadas : [ llamadas : { destino : “600245000”,
{ destino : “601234000”, duracion : 20}, duracion : 18}
{ destino : “607028000”, duracion : 45}, },
… …
]} {
nombre : “Juan”,
llamadas : { destino : “607028000”,
duracion : 45}
}
]

Figura 54

En nuestra base de datos de ejemplo aplicamos el comando de la siguiente manera.


Nótese que el campo de tipo array (llamadas) debe ir precedido del símbolo $ y encerrado
entre comillas:
db.clientes.aggregate({$unwind:”$llamadas”})

Versiones anteriores a la 3.0


En versiones anteriores a la 3.0, el resultado se devuelve en un objeto
que tiene dos campos: result y ok, como en la la Figura 55. Obsérvese
el efecto de $unwind: el array de llamadas (contenido en el campo
result) se ha “aplanado”, en el sentido de que se devuelve un objeto
completo por cada llamada de cada cliente. Véase también que los
contenidos del array result son objetos JavaScript, no objetos de
Mongo: en Mongo, el _id de cada objeto es único, y aquí no lo es: las
dos últimas llamadas de MARIA NIEVES FERNANDEZ NIETO
tienen exactamente el mismo valor en el id.

www.fullengineeringbook.net
Figura 55

Si queremos saber cuántas llamadas han realizado nuestros


clientes, una forma posible es:

Figura 56

En versiones posteriores a la 3.0, el resultado de la agregación ya no se devuelve


en un objeto result, como sucedía antes y así se ha mostrado en la Figura 56.
Si queremos saber desde qué teléfonos se ha llamado al número “600000079”,
podemos concatenar tres filtros:
En el primero ($unwind) aplanamos la colección.
En el segundo ($project) seleccionamos los campos telefono (el número
llamante) y llamadas.destino (el número llamado)
Con el tercero ($match) especificamos el filtro deseado: que el destino sea
el buscado.

www.fullengineeringbook.net
Figura 57

¿Y cuántas llamadas ha recibido ese número?


En versiones de MongoDB anteriores a la 3.0, añadiríamos a la consulta de
la Figura 57 el sufijo: .result.length .
En versiones posteriores, añadimos un filtro $group adicional. Lo
comentamos en la sección 1.4.2.
1.4.2 $group
Este operador agrupa elementos por los valores de uno o más campos. Equivale
aproximadamente a una instrucción Select… group by de SQL. A continuación se
presentan algunos ejemplos de su utilización.
1.4.2.1 Agrupación por fechas
Supongamos que queremos conocer el número de llamadas realizadas cada día.
Aproximémonos poco a poco a la solución mediante un $aggregate:
Primero, obtengamos la lista de las fechas en las que se han realizado todas las
llamadas: lo podemos conseguir “aplanando” las llamadas y seleccionando de cada una el
campo fecha:

Figura 58. Proyección de la fecha de las llamadas

En la figura anterior observamos que las llamadas 3ª y 4ª se hicieron el 5 de junio


de 2015. No obstante, si agrupamos directamente por el campo fecha, esos dos objetos no
se agruparán puesto que no tienen la misma hora. Por tanto, como sólo nos interesa
agrupar por fecha y no por hora, extraigamos el valor de la fecha (y no de la hora) del
campo fecha. Mongo, para esto, nos ofrece diversas operaciones ($dayOfYear,
$dayOfMonth, $dayOfWeek, $year, $month, $week, $hour, $minute, $second, $millisecond
y $dateToString). En la figura siguiente, añadimos a la instrucción de la figura anterior un
nuevo filtro para proyectar, del campo preproyectado llamadas.fecha, el año:

www.fullengineeringbook.net
Figura 59. Proyección del año en que se realizan las llamadas

Del mismo modo que utilizamos la función $year podemos utilizar cualquiera de
las otras. Pero, para nuestro particular, nos viene muy bien la función $dateToString, que
transforma una fecha a una cadena con el formato que le especifiquemos. Esta función
toma dos parámetros (format y date), de esta manera:
{ $dateToString: { format: <formatString>, date: <dateExpression> } }

Como primer parámetro pasamos una cadena con una o más máscaras de
extracción de campos de la fecha (Tabla 6); como segundo parámetro, el campo de tipo
fecha que deseamos manipular.

Máscara Resultado Intervalo de valores

%Y Año 0000-9999

%m Mes 01-12

%d Día 01-31

%H Hora 00-23

%M Minuto 00-59

%S Segundo 00-60

%L Milisegundo 000-999

%j Día del año 001-366

%w Día de la semana 1-7

%U Semana del año 00-53

%% Introducción del símbolo % %

Tabla 6. Máscaras para $dateToString

A nosotros nos interesa agrupar por día, mes y año. Por tanto, modificaremos la
www.fullengineeringbook.net
agregación de la Figura 59, sustituyendo la última proyección (la que extraía el año) de
esta manera:

Figura 60

El resultado es una lista con las fechas (en formato año-mes-día, porque así lo
hemos impuesto con la máscara %Y-%m-%d) de cada llamada. Ahora sí que estamos en
disposición de agrupar por este valor. Para ello, añadimos por fin un filtro $group:

Figura 61

$group toma al menos un parámetro, el _id, que representa el campo por el cual
vamos a agrupar. El resto de parámetros son opcionales y se utilizan para realizar cálculos
de totalización o acumulación (sumas, cuentas, medias, etcétera):
{ $group: {
_id: <expresión>,
<campo1>: { <acumulador1> : <expresión1> }, …
}

El pipeline de la Figura 61
En el $group que hemos añadido a la Figura 61 agrupamos por el
campo fechaDeLlamada. Este campo procede del $project previo: el
campo fechaDeLlamada es el resultado de aplicar la función
$dateToString a la fecha de cada llamada.

www.fullengineeringbook.net
Es claro entonces el encadenamiento, en el más estricto sentido del
pipes and filters, que produce la expresión de la figura:
{$unwind : “$llamadas”} produce una lista de campos _id, nombre, …,
llamadas
{$project : { “llamadas.fecha” : 1 …}} produce una lista de
llamadas.fecha, eliminando el resto de campos que produjo el
$unwind
{$project : { fechaDeLlamada : …}} crea un nuevo campo
fechaDeLlamada, que es el que se procesa en el $group.
{$group: {_id : “$fechaDeLlamada”}} realiza la agrupación mediante el
campo especificado en el _id. Los campos que estaban accesibles
en los filtros previos (el nombre del cliente, por ejemplo), no
están ya accesibles aquí.


En la Figura 60 veíamos que aparecen valores de fecha duplicados porque, como
allí no agrupábamos, Mongo mostraba la fecha de cada una de las llamadas, incluidas las
que se producían el mismo día. En la Figura 61 ya sí estamos agrupando, por lo que no
hay valores repetidos. Para comprobarlo, le podemos añadir un filtro $sort para que
ordene por la fechaDeLlamada. Primero, hagámoslo como en la Figura 62, a la que
agregamos un filtro {$sort : {fechaDeLlamada : 1}}. Los resultados son exactamente
iguales a los de la Figura 61:

Figura 62. Un pipeline $sort que no hace lo que deseamos

¿Qué es lo que falla en el aggregate de la Figura 62? Si el lector no cae, tal vez le
venga bien releer el comentario sobre El pipeline de la Figura 61.
La solución aparece en la Figura 63: es una sútil pero importante diferencia.

www.fullengineeringbook.net
Figura 63. El pipeline de la Figura 62, corregido para que haga lo que queremos

En la figura siguiente pondremos derecha la Figura 63, pero añadiendo ahora uno
de los parámetros opcionales al filtro $group: hemos dicho que $group toma un primer
parámetro _id que indica el campo por el que agrupamos, y que puede tomar uno o más
parámetros opcionales para hacer cálculos de acumulación o totalización, y esto es
precisamente lo que queremos, puesto que deseábamos contar el número de llamadas
realizadas en cada día del periodo: lo que haremos es crear un campo totalLlamadas que
sume el valor 1 por cada llamada:

Figura 64

Otra forma de agrupar por fechas


En este ejemplo hemos agrupado utilizando $dateToString, pero
podríamos haber agrupado directamente por día, mes y año:

www.fullengineeringbook.net
Figura 65

1.4.3 Cuenta del número total de llamadas


Una forma de calcular el número total de llamadas, con un aggregate, es aplanar
las llamadas y, luego, agrupar por null y sumar (Figura 66). Agrupar por null significa
agrupar todos los objetos en un solo grupo.

Figura 66

Existen por supuesto otras maneras de contar, como la del cuadro siguiente, y
como la que veremos en la Figura 80 de la sección 1.4.5, dedicada a los operadores para
los filtros.

Otra forma de contar


Mongo incorpora un motor de ejecución de JavaScript, así que
podemos programar funciones en este lenguaje para ejecutar cualquier
operación. Escribamos, paso a paso, una función JavaScript que cuente
el número total de llamadas.
Como sabemos, la ejecución de db.clientes.find() devuelve un cursor con
todos los clientes. Si queremos mostrar los datos del cliente nº 57,
podemos escribir lo siguiente:
db.clientes.find().skip(57)

Como sabemos (sección 3.2.7 del Capítulo 2, página 24), skip(n) salta
hasta el objeto n-simo, devolviendo un cursor formado por todos los
objetos desde el n-simo hasta el último. En el
ejemplo, db.clientes.find().skip(57) devuelve un cursor formado por todos los
clientes desde el 57º hasta el 87º (hay 87 clientes en este momento, en

www.fullengineeringbook.net
el estado actual de la base de datos). En efecto:

Figura 67

Si queremos recuperar solamente los clientes del 57 al 60, podemos


limitar el tamaño del cursor con limit(n):

Figura 68

Lo que devuelven tanto skip como limit es un cursor, que es un


conjunto de objetos. Si nos interesa conocer, por ejemplo, el nombre del
cliente nº 57, la siguiente instrucción no nos devuelve nada,
precisamente porque el resultado del limit, aunque le pasemos el valor
1 como parámetro, sigue siendo un cursor, y un cursor no tiene campo
nombre:

Figura 69

Podemos, sin embargo, recuperar el primer elemento del cursor


mediante su posición:

Figura 70

Escribiremos una función JavaScript que cuente el número total de


llamadas basándonos en las instrucciones anteriores.
Obsérvese, en el primer prompt la definición de la función y, en el
segundo, la llamada:

Figura 71

www.fullengineeringbook.net
1.4.3.1 Número de llamadas recibidas por el 600000079
Al final de la sección 1.4.1 hemos visto cómo recuperar el número de llamadas
realizadas al 600000079 mediante el acceso al campo length del objeto result devuelto por
el aggregate, lo cual funciona para versiones antiguas de Mongo. En el ejemplo anterior,
hemos contado el número de llamadas realizado en cada día del período, para lo que
hemos utilizado la función {$sum : 1} como segundo campo de un filtro $group.
Pues ahora haremos algo parecido a lo que hacíamos allá: aplanaremos la lista de
llamadas, extraeremos con un $project el campo llamadas.destino, filtraremos con un
$match aquellas llamadas realizadas al 600000079 y, por último, las agruparemos con un
$group y las contaremos con $sum:

Figura 72

Cálculo en JavaScript del número de llamadas recibidas por el


600000079
De la misma forma que hacíamos en el cuadro anterior, también
podemos calcular el número de llamadas recibido por un número
mediante una función JavaScript, como por ejemplo:

Figura 73

1.4.4 Operadores para $group


$group dispone de un conjunto específico de operadores para realizar
acumulaciones y totalizaciones (Tabla 7). Todos estos operadores toman como valor una
expresión.

Operador Descripción

{$sum : expr} Suma

{$avg: expr} Media

www.fullengineeringbook.net
{$first: expr} Primer elemento

{$last: expr} Último elemento

{$max: expr} Máximo

{$min: expr} Mínimo

{$push: expr} Devuelve un array con los valores resultantes de aplicar la expresión a
los documentos de cada grupo devueltos por el $group

{$addToSet: expr} Devuelve un array de valores sin repetir, que es el resultado de aplicar
la expresión a los elementos de cada grupo devueltos por el $group

Tabla 7. Operadores de totalización

Veamos algunos ejemplos:


Tiempo total en que ha estado hablando cada uno de nuestros clientes
($sum):

Figura 74

Duración media de las conversaciones de cada cliente ($avg):

Figura 75

Duración media de las conversaciones de todos cliente (agrupamiento por


null y aplicación de $avg):

www.fullengineeringbook.net
Figura 76

Tiempo total de charla y duración media de las conversaciones de cada


cliente ($sum y $avg):

Figura 77

Duración de las llamadas más larga y más corta de cada cliente ($max y
$min):

Figura 78

Listado de los teléfonos, cada uno con un array de las duraciones de sus
llamadas ($push):

Figura 79

1.4.5 Operadores para los filtros


Existen muchos operadores para aplicar en los filtros de las consultas pipeline.

www.fullengineeringbook.net
Para calcular el tamaño de un campo de tipo array, por ejemplo, se utiliza el operador
$size. Si queremos calcular el número de llamadas realizado por cada cliente de una
manera distinta a como lo hemos hecho en ejemplos anteriores, podemos escribir el
aggregate de la Figura 80, en la que hacemos una proyección de dos campos: el nombre y
n, siendo n un campo “artificial” al que asignamos como valor el tamaño del array de
llamadas.

Figura 80. Todavía una forma adicional de contar las llamadas de cada cliente

1.4.5.1 Operadores booleanos


Disponemos de los operadores $and, $or y $not. Veamos unos ejemplos.
Supongamos que queremos extraer un listado de todos los clientes menores de
edad y que hacen más de 200 llamadas al mes. Una posible solución, sin $and, es extraer
primero la fechaDeNacimiento y el numeroDeLlamadas y aplicar dos $match seguidos,
uno que filtre por uno de los campos y otro por otro:

Figura 81. Clientes menores de edad (a fecha de 26/11/2015) que llaman más de 200 veces, resuelto con dos $match

Y ahora, usando un $and para obtener en un solo $match el mismo resultado:

Figura 82. Como en la Figura 81, pero con un solo $match y un $and

Queremos enviar un SMS a los clientes que cumplan años cierto día o que sea su
santo. Supondremos que hoy se celebra San Antonio y que es, aunque no coincida la
fecha, es 28 de octubre.
Buscaremos con un $or a todos los clientes que cumplan uno de estos dos criterios.
Pero vayamos poco a poco: primero, saquemos los teléfonos de aquellos clientes que
cumplan años: es decir, que el día y el mes de su fechaDeNacimiento coincidan con los de
www.fullengineeringbook.net
la fecha buscada, el 28 de octubre: proyectamos los tres campos que nos interesan
(telefono, dia y mes: estos dos últimos los extraemos con dos operaciones de fecha) y
luego filtramos con un $match para extraer los objetos cuyos mes y día casen con los
buscados (28 y 10):

Figura 83

Ahora, por otro lado, saquemos aquellosclientes que se llaman ANTONIO: una
búsqueda rápida con db.clientes.find({nombre : “ANTONIO”}, {nombre : 1, _id : 0}).size() devuelve 0,
indicando aparentemente que no hay Antonios; pero tal vez haya algún cliente que lo
tenga de segundo nombre. Busquemos utilizando una expresión regular:

Figura 84

En la figura anterior, estamos buscando todos los objetos cuyo campo nombre case
con la expresión regular que empieza por cualquier cosa, termina por cualquier cosa, pero
tiene ANTONIO entre medias. Obsérvese que tenemos a un tal JOSE ANTONIO.
Utilizando un aggregate, con la siguiente consulta obtenemos el mismo resultado
que con el find de la Figura 84:

Figura 85

Para conseguir el resultado deseado (cumpleaños el 28 de octubre o que se llamen


ANTONIO), debemos combinar de alguna manera las consultas de la Figura 83 y la
Figura 85. Por ejemplo, como mostramos en la Figura 86:

www.fullengineeringbook.net
Figura 86

¿Hubo errores con el $project sobre mes y día?


Es posible que hayamos obtenido errores al tratar de proyectar el día y
mes de la fechaDeNacimiento con $dayOfMonth y $month:

Figura 87

El campo fechaDeNacimiento es de tipo Date, exactamente igual que el


campo fecha de las llamadas que realiza cada cliente: si en la Figura 65
procesábamos este campo con estos dos operadores ($dayOfMonth y
$month), ¿por qué rayos falla ahora al procesar los datos?
Se debe a que se nos ha colado algún cliente o bien con algún error del
tipo de dato en fechaDeNacimiento, o bien algún cliente directamente
sin valor en este campo:
a) Para sacar la lista de los clientes cuya fechaDeNacimiento no
es de tipo Date, podemos hacer la siguiente consulta, en la que le
pedimos a la colección de clientes que nos devuelva aquellos
objetos cuyo campo fechaDeNacimiento no es del tipo 9. En
https://docs.mongodb.org/manual/reference/bson-types/ aparecen
los tipos de Mongo y sus códigos correspondientes.
En la respuesta a la consulta, vemos que hay tres objetos
(correspondientes a MARIA JOSE LEON SOLER, MANUEL
VELASCO VELASCO y LIDIA REYES SOLER) que,
realmente, carecen del campo.

www.fullengineeringbook.net
Figura 88

b) El mismo resultado que con la consulta anterior lo podemos


obtener pidiendo un listado de aquellos objetos cuya
fechaDeNacimiento es null (ojo: obviamente, si alguno de los
objetos tuviera fechaDeNacimiento, pero de un tipo distinto de
Date, el resultado de ambas consultas sería diferente).

Figura 89

Para poder resolver sin problemas el ejercicio anterior (enviar SMS a


los clientes que cumplen hoy años o que es el día de su santo),
eliminamos previamente los tres clientes sin fecha de nacimiento:
usamos el comando remove pasando el criterio adecuado:

Figura 90


1.4.5.2 Operadores para conjuntos
Los operadores para conjuntos operan sobre los campos de tipo array de cada
objeto, tratándolos como conjuntos (es decir, ignorando los elementos duplicados).
Disponemos de los operadores que aparecen en la Tabla 8:

Operador Descripción

$setEquals Devuelve true si los conjuntos tienen los


mismos elementos.

$setIntersection Devuelve la intersección de los conjuntos


pasados como parámetros.

www.fullengineeringbook.net
$setUnion Devuelve la unión de los conjuntos pasados
como parámetros.

$setDifference Devuelve la diferencia de los dos conjuntos


pasados como parámetros (es decir: elementos
que están en el primero y no en el segundo).

$setIsSubset Devuelve true si el primer conjunto es


subconjunto del segundo.

$anyElementTrue Devuelve true si algún elemento del conjunto


es evaluable a true.

$allElementsTrue Devuelve true si todos los elementos del


conjunto son evaluables a true.

Tabla 8

Utilizando estos operadores, queremos sacar la lista de números que han llamado
al 600000079. Para ello, vamos a pedirle a Mongo que nos dé los números de teléfono de
aquellos clientes cuyos arrays de llamadas contengan al array [“600000079”]. En otras
palabras, pediremos aquellos valores del campo telefono de los clientes en los que se
verifique que [“600000079”] es un subconjunto del campo llamadas.
Primer paso: sacar el valor del campo existente telefono y del campo artificial
haLlamado: éste es un valor booleano (véase Tabla 8) que devuelve, para cada objeto, si
se ha llamado o no al número buscado. Obsérvese que se devuelve un objeto por cada
cliente: el campo haLlamado vale true a veces y false otras.

Figura 91

Segundo paso: añadamos un filtro $match al pipeline anterior para que sólo
muestre los números que sí han llamado al número buscado:

www.fullengineeringbook.net
Figura 92

En las dos figuras anteriores, el array [“600000079”] lo estamos creando dentro


del propio pipeline. Lo podemos crear fuera también, asignándolo a una variable. Por
ejemplo, si queremos saber todos los números que han llamado al 600000079 o al
60000080, podemos asignar a una variable del shell un array con estos dos números, y
utilizar esta variable en la consulta. Es decir:

Figura 93

1.4.5.3 Operadores de comparación


Estos operadores permiten comparar dos valores. Excepto $cmp, que devuelve -1,
0 o +1 según el primer valor sea menor, igual o mayor que el segundo, todos los demás
devuelven un booleano. Los operadores son los siguientes:
$cmp, $eq, $gt, $gte, $lt, $lte, $ne

1.4.5.4 Operadores aritméticos


Sirven para realizar operaciones aritméticas sencillas con los campos numéricos de
cada objeto ($add también permite sumar números en milisegundos a fechas).
Disponemos de los siguientes:
$add, $subtract, $multiply, $divide, $mod

En el siguiente pipeline calculamos, para cada teléfono, parte del importe total de
la factura: de momento, sumamos a la cuotaFija de la tarifa el producto de multiplicar el
numeroDeLlamadas (un campo artificial que recuperamos en el primer $project) por el
establecimiento de llamada correspondiente a la tarifa del cliente. Obsérvese que add,
multiply, etcétera toman un solo parámetro de tipo array, que contiene los valores que se
quieren sumar, multiplicar, etcétera:

www.fullengineeringbook.net
Figura 94

1.4.5.5 Operadores de cadena


Para manipular cadenas disponemos de las operaciones siguientes:

Operación Descripción

$concat Concatena las cadenas que se pasan en


el parámetro de tipo array

$substr Devuelve la subcadena que empieza en


donde se indique y termina en el
número de caracteres deseados

$toLower Pasa a minúsculas

$toUpper Pasa a mayúsculas

$strcasecmp Compara (sin fijarse en mayúsculas o


minúsculas) dos cadenas. Devuelve -1,
0 o +1 en función de que la primera
cadena sea menor, igual o mayor a la
segunda

Tabla 9

La siguiente consulta devuelve, por orden alfabético, el listado de los clientes en


formato apellido1 apellido2, nombre:

www.fullengineeringbook.net
Figura 95

La siguiente consulta extrae la inicial del nombre y apellidos y las devuelve


concatenadas:

Figura 96

1.4.5.6 Operadores de búsqueda de texto


En esta categoría tenemos solamente el operador $meta , que devuelve el score de la
búsqueda de un patrón en un campo de tipo texto. El campo de búsqueda debe estar
indexado, por lo que volveremos a este operador al hablar de los índices.
1.4.5.7 Operador para arrays
Disponemos solamente de $size , que ya utilizamos al comienzo de esta sección, en
la Figura 80 (página 45).
1.4.5.8 Operadores para variables
Tenemos dos operadores:
$map , que sirve para aplicar una expresión a los elementos de un array y
devolver un array con esa expresión aplicada.
$let , que permite definir variables para ser utilizadas en una expresión,
devolviendo el resultado de la aplicaciónd de dicha expresión.
$map toma tres parámetros: input , que indica el array sobre el que vamos a
operar; as , que con el que asignamos un nombre a cada elemento del array (para poder
referirnos a él a continuación); e in , en donde se escribe la operación que se desee hacer.
La siguiente consulta nos devuelve dos arrays por cada cliente: el primero contiene las
duraciones de las llamadas en segundos, y el segundo en minutos:
www.fullengineeringbook.net
Figura 97

1.4.5.9 Operador para literales


El operador $literal devuelve directamente una cadena. En el siguiente ejemplo se
devuelve el texto “Un gran cliente” para cada cliente.

Figura 98

1.4.5.10 Operadores para fechas


Son los operadores que ya vimos en la sección 1.4.2.1, página 36.
1.4.5.11 Operadores condicionales
Tenemos dos: $cond y $ifNull .
El primero toma tres parámetros: if , que es la expresión booleana que queremos
evaluar; then , que contiene la expresión que debe devolverse si la condición es cierta; else ,
con la expresión que se debe devolver en otro caso.
En el siguiente ejemplo devolvemos un texto en función de si el cliente ha hecho
280 llamadas o más:

www.fullengineeringbook.net
Figura 99

1.4.5.12 Operadores de acumulación y totalización


En esta categoría se encuentran los operadores ya vistos en la sección 1.4.4y que
son solamente aplicables al filtro $group .

www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 4. Reglas MapReduce
Las reglas MapReduce permiten manipular grandes cantidades de datos,
transformándolos en pequeñas agregaciones con las que resulta más sencillo operar.
Las colecciones disponen de la operación mapReduce, que puede tomar los
siguientes parámetros:
mapReduce(
map : función JavaScript,
reduce : función JavaScript,
out : colección,
query : documento,
sort : documento,
scope : documento,
limit : número,
finalize : función JavaScript,
jsMode : booleano,
verbose : booleano)
Fijémonos de momento en los tres primeros:
La función map extrae de la colección los campos que estemos interesados
en tratar, devolviéndolos en forma de pares clave, valor, en donde el valor
es un array de todos los objetos asociados a la clave.
reduce aplica el cálculo en el que estemos interesados a los datos
producidos por la función map.
Finalmente, out representa la colección en la que Mongo dejará los
resultados obtenidos.

www.fullengineeringbook.net
1 EJEMPLO BÁSICO: MAP, REDUCE Y OUT
Hagamos un primer ejemplo para calcular cuánto tiempo ha hablado cada cliente.
Como sabemos, nuestra colección de clientes tiene la estructura que se muestra en la
Figura 100. Nuestra regla MapReduce ha de producir una colección nueva, que
llamaremos por ejemplo “duraciones”, en la que figure el número de teléfono de cada
cliente y la suma total de las duraciones de sus llamadas.

{ nombre : “Pepe”, telefono : “600123456”, …


llamadas : [
{ destino : “600000000”, duracion : 20, …},
{ destino : “607000000”, duracion : 15, …},

]},
{ nombre : “Ana”, telefono : “600123457”, …
llamadas : [
{ destino : “600245000”, duracion : 18, …},
{ destino : “604500000”, duracion : 11, …},

]},

{ nombre : “Juan”, telefono : “600123458”, …
llamadas : [
{ destino : “601234000”, duracion : 20, …},
{ destino : “607028000”, duracion : 45, …},

]}

Figura 100. Objetos en la colección clientes

Para nuestro ejemplo, llamaremos a la función desde la consola de esta manera:


db.clientes.mapReduce(map, reduce, {out : “duraciones”})

La función map es una función JavaScript sin parámetros que se aplica a cada
elemento de la colección: realiza el procesamiento que se indique con cada objeto y
produce un par (clave, valor) que se coloca en un mapa:
La primera vez que al mapa le llega un valor con cierta clave, se le crea una
entrada en el mapa para esa clave y se coloca el valor en su lista de valores.
La segunda o sucesiva vez que le llega un valor al mapa para una clave ya
existente en el mapa, se añade el valor a su lista de valores.
Para los datos de la colección de clientes de la Figura 100, si se utiliza el valor del
campo telefono como clave y la duración de las llamadas como valor, se obtendría el
siguiente mapa tras ejecutar la función map:
www.fullengineeringbook.net
Clave Valor

“600123456” [20, 15]

“600123457” [18, 11]

“600123458” [20, 45]

Figura 101

La función reduce, por otro lado, procesa cada entrada del mapa de la manera que
se especifique en su código. reduce sí toma parámetros: la clave y el valor. Según el
enunciado de nuestro ejemplo, reduce debería transformar el mapa de la Figura 101 y
dejarlo de la siguiente manera:

Clave Valor

“600123456” 35

“600123457” 29

“600123458” 65

Figura 102

Para realizar la primera tarea (construcción del mapa), escribimos el siguiente


código:

Figura 103

Como se ha dicho, la función se aplicará a cada uno de los elementos de la


colección: si la aplicamos a la colección clientes, el objeto this representará a cada uno de
los clientes de la colección. Dentro de la función definimos una variable clave en la que
almacenaremos el valor que representa la clave del mapa y que, en el ejemplo, es el
número de teléfono de cada cliente (de this). En el bucle, por cada llamada telefónica
recuperada se invoca a la función predefinida emit(clave, valor), que coloca en la entrada
del mapa correspondiente a la clave el valor que se pase (en este caso, la duración de la
llamada). Aplicada esta sencilla función a los datos de la Figura 100, se obtiene el mapa
que se mostraba en la Figura 101.
Cada uno de las entradas del mapa entra a la función reduce que, en nuestro
ejemplo, debe sumar todos los valores contenidos en el campo valor de la entrada
correspondiente a la clave. Si emit “emite” dos parámetros (clave y valor) al mapa, reduce

www.fullengineeringbook.net
los recoge de éste. El código de reduce se muestra en la Figura 104: la función toma los
parámetros clave y valor; el segundo es el array de duraciones correspondiente a cada
entrada del mapa: si, en el mapa de la Figura 101, la primera entrada (clave, valor) es
(“600123456”, [20, 15]), la función debe reducir el array a un solo valor que se asociará a
su clave. En la Figura 104 se muetra el código de la posible función de reducción, que
acumula en la variable tiempoCharlando la suma de las duraciones de todas las llamadas,
que vienen en el segundo parámetro de la función.

Figura 104

Una vez definidas las dos funciones map y reduce, que han sido asignadas a dos
variables JavaScript en la consola de Mongo, podemos llamar a la función mapReduce
sobre la colección a la que deba aplicarse: en el ejemplo de la Figura 105 se aplican las
dos funciones a la colección clientes para producir una nueva colección llamada
duraciones. Los resultados se resumen en las líneas siguientes: se han procesado 1000
objetos (correspondientes a 1000 clientes), se han ejecutado 270.275 emits
(correspondientes a 270.0275 llamadas), han llegado 1000 pares (clave, valor) a la función
reduce y, ésta, ha producido 1000 objetos como salida.

Figura 105

La colección duraciones se puede manipular como cualquier otra colección: en la


Figura 106 se recupera el primer objeto: el _id es el valor de la clave; el valor, la suma
total.

Figura 106

www.fullengineeringbook.net
2 PREFILTRADO CON QUERY
Supongamos que deseamos hacer el mismo cálculo que en la sección anterior, pero
restringiéndonos a las llamadas de los clientes que tengan un cierta tarifa. Mediante el
parámetro query se realiza una selección previa de los objetos a los que se aplicará la
función map.
Copiemos y pequemos de nuevo el ejemplo de la Figura 100, que extendemos
ahora con el campo tarifa (que, por comodidad, la escribimos en una sola cadena) de cada
cliente:

{ nombre : “Pepe”, telefono : “600123456”, tarifa : TarifaTardes, …


llamadas : [
{ destino : “600000000”, duracion : 20, …},
{ destino : “607000000”, duracion : 15, …},

]},
{ nombre : “Ana”, telefono : “600123457”, tarifa : TarifaPlana …
llamadas : [
{ destino : “600245000”, duracion : 18, …},
{ destino : “604500000”, duracion : 11, …},

]},

{ nombre : “Juan”, telefono : “600123458””, tarifa : TarifaTardes …
llamadas : [
{ destino : “601234000”, duracion : 20, …},
{ destino : “607028000”, duracion : 45, …},

]}

Figura 107. Objetos en la colección clientes

Como sabemos, la función map se aplica a cada elemento de la colección que, en


el ejemplo, serán instancias de tipo Cliente. Como nos interesa filtrar las llamadas por tipo
de tarifa, restringiremos esto en el parámetro query de la función mapReduce. Si sólo nos
interesan los clientes con TarifaTardes, definimos una variable query como se muestra en
la primera línea resaltada de la Figura 108, y pasamos esta variable como valor del
parámetro query. Como se observa, el resultado incluye ahora solamente 255 clientes, que
son los que tienen la tarifa por la que se ha filtrado.

www.fullengineeringbook.net
Figura 108

www.fullengineeringbook.net
3 ADICIÓN DE UNA FUNCIÓN DE FINALIZACIÓN: FINALIZE
Se puede añadir una función de finalización para hacer un postproceso de los
objetos devueltos por la función reduce, de modo que los resultados que se viertan en la
colección especificada en el parámetro out habrán sido modificados.
Con la siguiente función, la duración total de las llamadas de cada cliente se
expresa en formato de horas, minutos y segundos:

Figura 109

La función finalize se pasa como valor del parámetro finalize en la llamada a


mapReduce:

Figura 110

El resultado, efectivamente, se muestra en el formato especificado:

Figura 111

Obsérvese, en el código de la Figura 109, que el resultado que devuelve la función


es un objeto complejo con tres campos horas, minutos, segundos. Evidentemente, también
en las funciones map y reduce podríamos haber hecho exactamente lo mismo.

www.fullengineeringbook.net
4 CÁLCULO DE LA FACTURACIÓN MENSUAL CON
MAPREDUCE
En la Figura 112 se muestra abreviadamente el campo fecha de cada llamada de
cada cliente. Supongamos que se desean facturar las llamadas del mes 6.

{ nombre : “Pepe”, telefono : “600123456”, tarifa : TarifaTardes, …


llamadas : [
{ destino : “600000000”, duracion : 20, fecha : 1/5/2015…},
{ destino : “607000000”, duracion : 15, fecha : 1/6/2015 …},

]},
{ nombre : “Ana”, telefono : “600123457”, tarifa : TarifaPlana …
llamadas : [
{ destino : “600245000”, duracion : 18, fecha : 1/6/2015…},
{ destino : “604500000”, duracion : 11, fecha : 3/6/2015…},

]},

{ nombre : “Juan”, telefono : “600123458””, tarifa : TarifaTardes …
llamadas : [
{ destino : “601234000”, duracion : 20, fecha : 31/3/2015…},
{ destino : “607028000”, duracion : 45, fecha : 8/6/2015…},

]}

Figura 112

El resultado que deseamos obtener al final será algo de este estilo:

Clave Valor

“600123456” {cuotaFija : 25, importesEstablecimiento : 0.3, importesConsumo : 1.2}

“600123457” {cuotaFija : 250, importesEstablecimiento : 0.3, importesConsumo : 0.0}

“600123458” {cuotaFija : 25, importesEstablecimiento : 0.3, importesConsumo : 3.6}

Figura 113. Resultado final del proceso de facturación

Nuestras funciones MapReduce pueden ser las siguientes:

www.fullengineeringbook.net
Figura 114

Este ejemplo es interesante porque muestra la clave que se construye en la función


map es un objeto complejo, formado por los campos telefono y tarifa. En la función
reduce se manipulan también los campos de la clave. Es decir, que tanto la clave como el
valor pueden ser tan complejos como sea necesario. En el ejemplo se observa también que
las funciones map y reduce, que hasta este ejemplo hemos asignado a variables de la
consola llamadas también map y reduce, pueden asignarse a cualesquiera variables: en la
Figura 114, a llamadasDeJunio y a facturacionJunio.
Este MapReduce produce una colección que llamaremos facturacionJunio. Al
llamarla le añadimos el parámetro verbose=true, que muestra información sobre los
tiempos de ejecución:

Figura 115

Una consulta al primer elemento de la colección resultante muestra este resultado:

www.fullengineeringbook.net
Figura 116

www.fullengineeringbook.net
5 ALMACENAMIENTO DE SCRIPTS EN EL SERVIDOR
Todas las funciones JavaScript que vamos creando las podemos ir almacenando en
el servidor para dejarlas accesibles a los programas externos que manejen la base de datos.
Las dos funciones llamadasDeJunio y facturacionJunio creadas en la sección anterior las
guardamos, dentro de la base de datos, en una colección separada a la que podemos llamar
scripts:

Figura 117

Tanto desde la consola de Mongo como desde un programa externo (una


aplicación Java, por ejemplo) podemos recuperar el código de cualqueira de estos scripts
con una sencilla llamada a find sobre la colección scripts:
var llamadas=db.scripts.find({_id : “llamadasDeJunio”})

www.fullengineeringbook.net
6 MÁS SOBRE EL COMANDO MAPREDUCE
En la sección 4 del Capítulo 8 (página 102) se muestran más opciones tanto del
comando mapReduce como de la lectura y ejecución de scripts almacenados en el
servidor.

www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 5. Índices
Los índices permiten realizar búsquedas más rápidas, pero ralentizan las
inserciones y actualizaciones. En efecto, un índice se almacena como una estructura de
datos ordenada y separada: cuando se hace un búsqueda por un campo indexado, se
localiza el valor en el índice, el cual posee un puntero hacia la ubicación del objeto en el
fichero de datos. Al hacer una inserción, además de añadir el objeto al fichero de datos, el
valor o valores indexados deben insertarse en el fichero o ficheros de índice, lo que hace
un poco más lenta la operación.

www.fullengineeringbook.net
1 USO DE ENSUREINDEX PARA CREAR ÍNDICES
Para una colección, los índices se crean con ensureIndex, como en el siguiente
ejemplo, que acelerará las búsquedas de clientes:

Figura 118

ensureIndex recibe como parámetro la lista de campos que desean utilizarse para
crear el índice: en el ejemplo, los dos apellidos y el nombre. El valor pasado indica si el
índice se guarda en orden ascendente (+1) o descendente (-1). Todas las colecciones tienen
por defecto un índice (el del campo _id); por ello, en la salida que da Mongo en la Figura
118, nos dice que, tras ejecutar la operación, la colección clientes tiene dos índices.
1.1 Índices únicos
Si no se indica explícitamente, los índices admiten valores duplicados. Para crear
un índice único, se pasa un segundo parámetro {unique : true}. En el siguiente ejemplo
intentamos crear un índice único para el campo nombre; obviamente, Mongo devuelve un
error porque hay clientes que tienen el mismo nombre:

Figura 119

Pero sí que podemos hacerlo para el DNI:

Figura 120

1.2 Indexado de objetos y arrays incrustados


Dentro de la colección de clientes, podemos indexar por objetos y arrays
incrustados. Por ejemplo, si queremos indexar el array de llamadas de cada cliente por el
número de destino, basta con escribir la siguiente instrucción:
db.clientes.ensureIndex({“llamadas.destino” : 1})

Del mismo modo, para indexar por el tipo de tarifa, hacemos:


db.clientes.ensureIndex({“tarifa.tipo” : 1})

www.fullengineeringbook.net
2 CONSULTA DE LOS ÍNDICES
Podemos consultar qué índices tiene una colección mediante getIndexes():

Figura 121

www.fullengineeringbook.net
3 ELIMINACIÓN DE ÍNDICES
Si, de los tres índices de la figura anterior, queremos eliminar el tercero, cyo name
es “dni_1”, escribimos lo siguiente:

Figura 122

También lo podíamos haber hecho de esta manera:

Figura 123

www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 6. Arranque del servidor y control de
acceso
La puesta en marcha del servidor de bases de datos es bien sencilla: vamos al
directorio bin de la instalación de Mongo y escribimos el comando mongod. Sin más
modificadores, esto pondrá a la escucha el servidor en el puerto 27017. En la Figura 124,
desde un Mac:

Figura 124. Arranque del servidor

Una vez lanzado el servidor, en otra terminal vamos al directorio bin y ejecutamos
el comando mongo (Figura 125).

Figura 125. Arranque de la consola

En principio, el arranque del servidor como hemos hecho arriba no habilita ningún
mecanismo de control de acceso: cualquiera puede conectarse a nuestro servidor y hacer
cualquier manipulación sobre las bases de datos y colecciones que tengamos. Para impedir
el acceso anónimo a nuestro servidor, debemos crear usuarios y asignarles roles, y asignar
los permisos necesarios a cada rol.

www.fullengineeringbook.net
1 ROLES POR DEFECTO
Un rol define los privilegios de acceso que los usuarios con este rol tienen a los
diferentes recursos de una base de datos. El nivel máximo de granularidad es la colección:
es decir, puede permitirse que un usuario de un cierto rol tenga acceso a la colección
Clientes pero, una vez en ésta, no se le puede prohibir que lea los datos de un cierto
cliente. Existen roles a nivel de base de datos, a nivel de servidor, de copia y restauración
y a nivel de clúster. Además, existe el rol de súperusuario y el _system, que no debiera ser
asignado a personas ni aplicaciones.
1.1 Roles a nivel de base de datos
Los roles predefinidos a nivel de base de datos se resumen en la Tabla 10.

Rol Permisos Operaciones permitidas

read Acceso de lectura a: collStats


-Todas las colecciones de datos dbHash
-Colecciones del sistema: dbStats
system.indexes, system.js y
find
system.namespaces
killCursors
listIndexes
listCollections

readWrite Todos los permisos de read y, Todas las de read.


además, acceso de escritura a convertToCapped
yodas las colecciones de datos y
a la colección del sistema createCollection
system.js dropCollection
createIndex
dropIndex
emptycapped
insert
remove
renameCollectionSameDB
update

dbAdmin Sobre las colecciones del sistema system.indexes,


system.namespaces y system.profile puede ejecutar: collStats,
dbHash, find, killCursors, listIndexes y listCollections. Sobre
system.profile puede ejecutar además dropCollection y

www.fullengineeringbook.net
createCollection.
Sobre las colecciones de datos, puede ejecutar las siguientes:
collMod, collStats, compact, convertToCapped,
createCollection, createIndex, dbStats, dropCollection,
dropDatabase, dropIndex, enableProfiler, indexStats, reIndex,
renameCollectionSameDB, repairDatabase, storageDetails y
validate

dbOwner Todos los permisos de los roles de arriba

userAdmin Permite crear y modificar usuarios y roles mediante las


siguientes operaciones: changeCustomData, changePassword,
createRole, createUser, dropRole, dropUser, grantRole,
revokeRole, viewRole y viewUser

Tabla 10. Roles predefinidos a nivel de base de datos

1.2 Roles a nivel de servidor


Son muy similares a los roles a nivel de base de datos, pero se aplican a todas las
bases de datos del servidor:
readAnyDatabase: similar al rol read, pero para todas las bases de datos.
Permite además la ejecución de listDatabases.
readWriteAnyDatabase: similar al readWrite, pero para todas las bases de
datos. Permite también la operación listDatabases.
userAdminAnyDatabase: similar al userAdmin, pero para todas las baes de
datos.
dbAdminAnyDatabase: similar a dbAdmin, pero para todas las bases de
datos.
1.3 Roles a nivel de clúster
Los roles a nivel de clúster permiten administrar un clúster de servidores completo.
Son los siguientes:
hostManager: permite monitorizar y gestionar servidores individuales.
clusterMonitor: proporciona acceso de sólo lectura a herramientas de
monitorización.
clusterManager: permite gestionar y monitorizar acciones en el clúster.
clusterAdmin: engloba todos los permisos de los tres roles anteriores, así
como permiso para ejecutar dropDatabase.
1.4 Rol de súperusuario: root
El rol root combina todos los permisos de todos los roles a nivel de servidor, más
el rol clusterAdmin.

www.fullengineeringbook.net
2 CREACIÓN DE USUARIOS
El proceso habitual para crear usuarios es: arrancar el servidor sin control de
acceso (Figura 124), arrancar la consola (Figura 125) y crear a continuación un usuario
administrador (Figura 126). Obsérvese el campo customData, que puede contener
cualquier información. Hecho esto, se reinicia el servidor con control de acceso(se arranca
el servidor con el parámetro —auth ; es decir, en la consola del sistema operativo se
escribe: ./mongod —auth ), se autentica el usario desde la consola (Figura 127) y se crean
los usuarios que sean precisos (Figura 128).

Figura 126. Creación de un usuario

Como se acaba de ver, los usuarios se crean con el comando createUser, que toma
como parámetro un objeto con los siguientes campos: user, pwd y roles, que es un array
formado por objetos con los campos role y db. En el ejemplo de la figura anterior, se
asigna al usuario el rol userAdminAnyDatabase, pasándose admin como base de datos.
admin es una base de datos en la que se guarda toda la información de administración del
servidor: usuarios, otras bases datos, roles, etcétera.

Figura 127. Conexión al servidor con nombre de usuario (-u) y contraseña (​-p)

www.fullengineeringbook.net
Figura 128. El administrador crea un nuevo usuario con acceso readWrite para una base de datos determinada

En la Figura 128, un usuario administrador ha creado a otro usuario pepe con


permisos de lectura y escritura sobre la base de datos telefonosNoIncrustados. En la
Figura 129, pepe accede al sistema con su contraseña, abre la base de datos para la que se
le ha concedido el acceso y lanza una consulta: Mongo le responde que hay 999 objetos en
la colección clientes; a continuación se cambia a otra base de datos, repite la consulta y
Mongo le devuelve un error de acceso no autorizado.

Figura 129. Acceso de un usuario no administrador

www.fullengineeringbook.net
3 MODIFICACIÓN DE PERMISOS
Supongamos que el usuario maco (creado en la Figura 126 con el rol
userAdminAnyDatabase) desea realizar conocer cuántos clientes hay en
telefonosIncrustados. Además de mediante los parámetros –u y –p, los usuarios
administradores pueden autenticarse conectando a la base de datos de administración y
ejecutando el comando db.auth. En la Figura 130, maco se conecta a un servidor que
requiere autenticación, abre la base de datos admin y se autentica sobre la marcha con
db.auth:

Figura 130. Conexión sin autenticación previa

Acto seguido, lanza la consulta db.clientes.find().length() , pero Mongo le devuelve un


error de acceso: en efecto, si repasamos los roles de la Sección 1.2, vemos que el rol de
este usuario no tiene permiso para ejecutar find().

Figura 131

Puesto que el usuario fue creado con el rol userAdminAnyDatabase, que permite
crear y modificar usuarios y roles, este usuario se asignará a sí mismo el rol de
readWriteAnyDatabase mediante el comando grantRolesToUser, que toma el nombre del
usuario y la lista de nuevos roles que se le quieren asignar. En la Figura 132, el usuario se
asigna el rol y, luego, recupera la lista de usuarios de admin. El usuario pepe, que creó en
la Figura 128, reside en la colección telefonosNoIncrustados.

www.fullengineeringbook.net
Figura 132. Concesión de un nuevo rol y consulta de usuarios en admin

Ahora, la ejecución del comando db.clientes.find.length(), que fallaba en la Figura


131, tiene éxito.

www.fullengineeringbook.net
4 CREACIÓN DE ROLES
Además de los roles predefinidos, en Mongo se pueden crear roles nuevos y
asignarles privilegios específicos mediante el comando createRole.
createRole toma tres parámetros: role (el nombre del rol), privileges (un arrya de
privilegios) y roles (una lista de roles de los que heredar permisos, que puede ser la lista
vacía: []).
El campo privileges está formado por pares (resource, actions), en donde redouces
es la lista de los recursos sobre los que se concede permiso y actions la lista de acciones
permitidas.
En la base de datos sintética telefonosNoIncrustados, que se creará en el 0, hay tres
colecciones: clientes, llamadas y tarifas. Supongamos que deseamos crear un role llamado
atencionAlCliente que tenga acceso de lectura a llamadas, y acceso de lectura y
actualización a clientes.
En la Figura 133 se crea este rol: la lista privileges tiene dos elementos, cada uno
formado por un par (resource, actions). El primer elemento concede los permisos de
buscar y actualizar (find y update) sobre clientes; el segundo, permiso de lectura sobre
llamadas.

Figura 133. Creación de un rol

Ahora, ya se pueden crear usuarios con este rol:

www.fullengineeringbook.net
Figura 134. Creación de un usuario con el nuevo rol

Cada elemento del array privileges tiene dos campos: resource y actions. resource
puede hacer referencia a una colección de una base de datos (como en la Figura 133) o a
un cluster: en este caso, el valor de resource sería {cluster : true} .
Respecto de las acciones (campo action), existen varias categorías. Las más
comunes son las siguientes:

Acción Descripción

find

insert
Permisos para ejecutar la operación indicada
remove

update

changeCustomData El usuario puede modificar el campo


customData de cualquier usuario en la base de
datos

changeOwnCustomData El usuario puede modificar su campo


customData

changeOwnPassword El usuario puede modificar su propia password

changePassword El usuario puede modificar la password de


cualquier otro usuario de la base de datos

createCollection

createIndex Permisos para crear colecciones, índices, roles

www.fullengineeringbook.net
createRole y usuarios

createUser

dropCollection
Permisos para borrar colecciones, roles y
dropRole
usuarios
dropUser

grantRole El usuario puede conceder permisos a roles

revokeRole El usuario puede eliminar permisos de roles

viewRole El usuario puede ver información de cualquier


rol

viewUser El usuario puede ver información de cualquier


usuario

Tabla 11. Algunos posibles valores del campo action

www.fullengineeringbook.net
5 OTROS COMANDOS DE ADMINISTRACIÓN
Se puede modificar completamente una cuenta de usuario mediante el comando
updateUser, que reemplaza por completo los datos preexistentes de la cuenta. Los usuarios
se borran mediante dropUser.
El comando changeUserPassword permite la modificación de la contraseña.
Supongamos que el operador01, usuario que se ha creado en la Figura 134, desea cambiar
su password a “1234”. Puesto que el rol atencionAlCliente no tiene concedido el permiso
para modificar su propia password, el comando fallará:

Figura 135. El usuario no tiene permiso para cambiar su contraseña

Para asignar al rol el priviliegio changeOwnPassword usaremos el comando


grantPrivilegesToRole, que añade nuevos privilegios a este rol: el administrador, en
primer lugar, se cambia a la base de datos sobre la que quiere otorgar privilegios; luego,
ejecuta el comando sobre todas las colecciones de la base de datos:

Figura 136. Asignación de un nuevo permiso a un usuario

Ahora, operador01 ya puede modificar su contraseña:

Figura 137.

www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 7. Gestión de réplicas
Una réplica es una copia idéntica de los datos de un servidor primario que se
conserva en otro servidor secundario. El servidor primario recibe las peticiones de los
clientes, mientras que los secundarios conservan solamente copias de los datos a partir de
la información que les va enviando el primario. Si el servidor primario cae, uno de los
secundarios pasa a ser el primario.

www.fullengineeringbook.net
1 CONJUNTOS DE RÉPLICA
Un conjunto de réplica es un grupo de servidores que proporcionan redundancia
respecto de los datos almacenados en otro servidor primario y alta disponibilidad de los
datos, pues si el servidor primario queda inaccesible o fuera de servicio, uno de los
secundarios pasa a ocupar su lugar mediante un sistema de votación entre los nodos
secundarios y los árbitros. Un árbitro es un nodo que no almacena copia de los datos, pero
que conoce la disponibilidad de los nodos y puede participar en la votación.
Un conjunto de réplicas requiere al menos un servidor primario, un secundario y
un árbitro, si bien lo habitual es disponer al menos de dos secundarios. El número máximo
de nodos en un conjunto de réplicas es de 50 miembros, si bien únicamente 7 tendrán
derecho a voto.
1.1 Nodo primario
El nodo primario es el único que recibe peticiones de escritura de los clientes.
Cuando se recibe una petición de escritura, el nodo primario registra la operación en una
colección de sistema llamada oplog, una colección en la que se almacenan las últimas
operaciones realizadas. De vez en cuando, los nodos secundarios leen este log y aplican a
sus datos las operaciones registradas para mantenerlos actualizados.
1.2 Nodos secundarios
Los nodos secundarios almacenan copias de la información del primario mediante
la ya mencionada lectura de la colección oplog del nodo primario. oplog es una capped
collection, es decir, una colección que tiene un tamaño máximo predeterminado[2]:
cuando la colección alcanza su tamaño máximo, comienza a eliminarse la información
más antigua, por lo que los secundarios deben actualizarse antes de que oplog empiece a
“vaciarse”.
Los secundarios no admiten peticiones de escritura desde los clientes y, por
defecto, tampoco de lectura, aunque este comportamiento puede cambiarse. No obstante,
es necesario tener en cuenta que un nodo secundario puede no estar completamente
actualizado con el primario (es decir, quizás las últimas escrituras habidas en el primario
no hayan sido todavía reflejadas en el secundario del que está leyendo). La lectura de
nodos secundarios proporciona a los clientes lo que se llama consistencia eventual; la
lectura del primario, que devuelve siempre información actualizada, se llama consistencia
estricta.
Además, los nodos secundarios se pueden configurar para que desempeñen papeles
específicos dentro del conjunto de réplicas:
De prioridad 0: son nodos que admiten peticiones de lectura y tienen
derecho a voto, pero ni pueden ser primarios ni convocar votaciones.
De réplica oculta: son nodos secundarios de prioridad 0 (no pueden ser
primarios) que, además, están ocultos para los clientes.
De réplica postergada: son nodos de réplica oculta (ni pueden ser
primarios ni estar accesibles para los clientes) que, además, contienen
copias de los datos de cierta antigüedad: es decir, leen el oplog del primario

www.fullengineeringbook.net
cada cierto tiempo. Para que estos nodos no pierdan información respecto
del nodo primario, y ya que oplog es una capped collection de tamaño
limitado, deben leer los datos de esta colección antes de que se supere su
tamaño máximo.
1.3 Árbitros
Un árbitro es un nodo que conoce al nodo primario pero que no mantiene copia de
sus datos. Si el primario cae, el árbitro puede participar en la votación con 1 voto y, puesto
que no tiene copia de los datos, no es elegible para primario. Estos nodos se utilizan para
evitar empates en las votaciones al nuevo nodo primario.
Supongamos un conjunto de réplicas con un nodo primario P, un árbitro A y tres
secundarios S0, S1, S2. Supongamos que P cae, que se produce una votación y que cada Si
vota a S(i+1)%3 (es decir, al siguiente): en este escenario, cada secundario recibe un voto,
pero el empate se resuelve gracias al “voto de calidad” de A.
1.4 Votaciones
Las votaciones se pueden celebrar en tres circunstancias:
La primera vez que se arranca el conjunto de réplicas.
Cuando un nodo secundario pierde el contacto con el primario.
Cuando el servidor primario pasa a un estado de no disponible. Un nodo
primario pasa a estar no disponible cuando recibe un comando
replSetStepDown, cuando hay votaciones y las gana un nodo secundario o
cuando el primario no tiene contacto con la mayoría de los nodos del
conjunto.
Durante el “periodo electoral” (es decir, durante el tiempo en que los nodos están
celebrando la votación) no hay nodo primario y, por tanto, el conjunto de réplicas no
admite operaciones de escritura, aunque sí de lectura.
Los nodos del conjunto de réplicas se envían constantemente (cada 2 segundos)
heartbeats (“latidos”, que son señales semejantes al ping), en las que avisan de que están
vivos. Si un latido enviado no recibe respuesta en 10 segundos, el nodo emisor marca al
receptor como inaccesible.
Cuando se crea el conjunto de réplicas se establece una prioridad para cada nodo,
de modo que, en la votación, los nodos secundarios y los árbitros preferirán votar a los
nodos que tengan más prioridad. Por defecto, la prioridad es 1, pero puede incrementarse
la prioridad de, por ejemplo, el nodo que esté geográficametne más cercano a los clientes
si se desea que éste tome el rol de nodo primario.
Tienen “derecho a voto” los nodos primarios, los secundarios, los árbitros, los
secundarios de recuperación y los secundarios de rollback.
Una vez realizada la votación, el nodo elegido puede ser vetado si no se encuentra
completamente actualizado: es decir, si el primario tiene operaciones más recientes que el
secundario. Esto se sabe mediante el optime, una marca de tiempo que almacena el
instante en que un nodo secundario ha realizado la última lectura de la colección oplog del
nodo primario.
www.fullengineeringbook.net
1.5 Regreso al conjunto de réplicas de un nodo primario
Es posible que un nodo primario reciba operaciones de escritura, no llegue a
enviarlas a ninguno de los secundarios, caiga y, por algún motivo, pasado un tiempo
regrese al conjunto de réplicas, cuando ya hay un nuevo nodo primario en funcionamiento.
En este caso, el nodo que acaba de regresar deshace (es decir, hace rollback) las
operaciones que había realizado pero de las que el resto de nodos no se habían enterado.
Esta información “deshecha” se almacena en forma de ficheros bson en la carpeta
rollbacks. En algún momento, el administrador decidirá qué hacer con estos objetos.

www.fullengineeringbook.net
2 OPERACIONES DE LECTURA Y ESCRITURA EN LOS
CONJUNTOS DE RÉPLICA
Aunque, por defecto, todas las operaciones de lectura y escritura se ejecutan sobre
el nodo primario del conjunto de réplica, estos comportamientos pueden cambiarse. A
continuación vemos algunas opciones para hacerlo.
2.1 Configuración de operaciones de lectura: read preferences
Como se ha dicho, todas las operaciones de lectura se envían, por defecto, al nodo
primario, aunque esta preferencia se puede configurar en la conexión del cliente. En este
caso existe, obviamente, el riesgo de que un cliente lea información de un nodo secundario
que no haya sido actualizada desde el primario.
Mongo permite cinco modos de especificar la lectura de datos desde los clientes:
primary, que es el modo por defecto: todas las lecturas se lanzan sobre el
nodo primario.
primaryPreferred, en la que las lecturas se hacen del nodo primario salvo
que esté fuera de servicio.
secondary, que indica al cliente que debe leer de nodos secundarios.
secondaryPreferred, para que el cliente lea de nodos secundarios salvo que
no haya ninguno disponible.
nearest, para que el cliente lea del nodo que le proporciona menor tiempo
de respuesta.
En la sección 3.6 veremos cómo cambiar esta preferencia.
2.2 Configuración de operaciones de escritura: el write concern
Respecto de las operaciones de escritura, al cliente le da igual que se estén
recibiendo en un servidor aislado o en el servidor primario de un conjunto de réplicas.
El comportamiento por defecto de una operación de escritura (por ejemplo, un
insert) espera que le llegue la confirmación de la operación sólo desde el nodo primario.
Al lanzar la operación se puede, sin embargo, esperar a que se confirme también la
actualización de los datos en más de un nodo del conjunto.
Si se desea modificar ese comportamiento por defecto para todas las operaciones,
se puede alterar el fichero de configuración del cojunto de réplica. Veremos estas
operaciones en la sección 3.7 de este mismo capítulo.

www.fullengineeringbook.net
3 CREACIÓN DE UN CONJUNTO DE RÉPLICA
En esta sección crearemos un conjunto que inicialmente estará formado por un
nodo primario y dos réplicas que correrán en la misma máquina física: ubicaremos los
servidores en los puertos 27010, 37017 y 47017 de localhost. Más adelante (sección 3.4)
añadiremos dos nodos adicionales.
3.1 Creación del conjunto inicial
Cada servidor almacenará los datos en una ubicación distinta de la misma
máquina. En este ejemplo, en tres subcarpetas (a las que, por claridad, llamaremos con el
puerto de cada host) dentro la carpeta /Users/Maco/Desktop/dbsReplica.
La figura siguiente muestras tres consolas, cada una de las cuales arranca un
servidor mongod en los puertos citados: amarilla en 27017, verde en 37017 y azul en
47017. Los parámetros que pasamos al comando mongod son:
—port para especificar el puerto
—dbpath para especificar la ubicación del directorio de datos de este
servidor
—replSet para indicar el nombre del conjunto de réplica
(replica3EnElMismoHost en este ejemplo). Como se ve, el parámetro toma
el mismo valor en las tres consolas, pues todos los servidores se refieren al
mismo conjunto.

Figura 138. Arranque de tres servidores en la misma máquina

A continuación, abrimos una consola cliente, conectamos a cualquiera de los


servidores y ejecutamos el comando rs.initiate() para iniciar el conjunto de réplica: en la
consola cliente de la Figura 139, que es la que aparece en primer plano, conectamos a
localhost sin especificar puerto, con lo que estamos conectando al servidor de consola
amarilla (que escucha en el puerto por defecto, el 27017).
En la consola cliente definimos una variable replica3EnElMismoHost en la que
especificamos las características de la réplica: un _id y una lista de members, cada uno de
los cuales tiene un _id y un host:
El _id es un valor numérico.
El host es el nombre o la dirección IP del host. En la figura, hemos
preferido utilizar la dirección de la máquina en la red local
(192.168.1.130) en lugar de localhost o 127.0.0.1. Los conjuntos de

www.fullengineeringbook.net
réplica tienen que tener o todos los nodos o ninguno en localhost. Como
añadiremos un nodo en una dirección diferente en la sección 3.4, debemos
evitar utilizar cualquiera de esos dos valores.
Una vez creada la variable que usaremos para pasar a rs.initiate, se ejecuta el
comando. Tras unos instantes, las tres consolas de servidor comienzan a mostrar mensajes
de conexión. Como recordamos (sección 1.4, sobre Votaciones, en la página 76), al crearse
un conjunto de réplicas se inicia una votación y, como vemos en el prompt de la consola
cliente, el host amarillo ha pasado a ser el servidor primario.

Figura 139. Arranque del conjunto de réplica

Si abrimos consolas clientes a los otros dos servidores, su prompts muestran que
sus correspondientes hosts son secundarios. En la Figura 141 abrimos una consola cliente
al servidor verde (puerto 37017: véase el parámetro —port pasado al ejecutar el comando
mongo) y otra al azul (puerto 47017). Sus prompts, en efecto, muestran la palabra
SECONDARY. Como sabemos, la opción por defecto impide operaciones de escritura en
estos nodos: en la misma Figura 141, los servidores secundarios verde y azul devuelven un
mensaje de error al intentar guardar un objeto en la colección prueba; en la Figura 140, sin
embargo, conseguimos insertar con éxito en el nodo primario.

Figura 140

www.fullengineeringbook.net
Figura 141. Apertura de clientes a hosts secundarios e intentos de escritura, que son rechazados

3.2 Caída de un nodo


Si pulsamos Control-C o, de otra manera, matamos el proceso del nodo amarillo,
los nodos secundarios celebran elecciones. En este caso, las gana el candidato del “partido
verde”: en la Figura 142 se ha matado el proceso mongod del amarillo (que era el
primario); un instante después, las consolas de los servidores secundarios comienzan a
mostrar mensajes de error al conectar al primario; tras las elecciones, el prompt del 37017
cambia a PRIMARY. La operación de escritura en el cliente verde, que en la Figura 141
fallaba por lanzarse sobre un nodo secundario, ahora tiene éxito (Figura 143).

Figura 142. Situación después de la caída del nodo primario

www.fullengineeringbook.net
Figura 143. Las operaciones de escritura ya se pueden ejecutar sobre un nodo secundario que ha pasado a ser
primario

3.3 Regreso del antiguo nodo primario


Si lanzamos de nuevo el servidor amarillo, se celebran nuevas elecciones. En este
caso, sigue de primario el verde y el amarillo queda de secundario.

Figura 144. Resultado electoral tras el regreso del amarillo al conjunto

3.4 Adición de nodos


A continuación añadiremos sobre la marcha dos nuevos nodos al conjunto de
réplica: uno que se ejecuta en una máquina virtual de Virtual Box que ejecuta Windows 10
y que corre en la misma máquina física que los servidores verde, amarillo y azul, y otro en
un host remoto.
En la Figura 145 se lanza un servidor mongod en una máquina virtual VirtualBox
con Windows 10. La máquina virtual corre en la misma máquina física que alberga los
servidores verde, amarillo y azul. La dirección IP de la máquina anfitriona es
192.168.1.130, mientras que la de la máquina con Windows es 192.168.1.136.

Figura 145. Arranque de un servidor en una máquina virtual

El servidor de Windows se arranca también con el parámetro —replSet establecido


al mismo nombre del conjunto de réplica que ya usamos para arrancar los tres servidores
“de colores”. Ahora, en uno de éstos, debemos añadir este nuevo nodo al conjunto

www.fullengineeringbook.net
mediante el comando rs.add(“host:puerto”), tal y como hacemos en la Figura 146.

Figura 146. Adición “en caliente” de un nuevo nodo al conjunto

En la siguiente figura añadimos un host remoto como árbitro, que escucha en el


puerto 27017 de una ubicación diferente. El comando, en este caso, es rs.addArb.

Figura 147. Adición de un árbitro

3.5 Consulta y modificación de la configuración del conjunto


Todas las operaciones sobre la configuración del replica set se ejecutan sobre el
objeto rs, que ya hemos utilizado con los comandos initiate, add y addArb, pero hay más.
A continuación consultaremos el estado de la réplica mediante rs.status() y notaremos una
curiosidad.
En la Figura 148 se muestra el estado del conjunto de réplica: todos los nodos en
192.168.1.130 se encuentran vivos, mientras que los nodos de la máquina virtual y del
host externo aparecen como no alcanzables:
Respecto del equipo en la máquina virtual, se debe a un problema con la
configuración con Virtual Box, que tampoco debe preocuparnos.
Respecto del equipo remoto, sucede que responde bien al comando ping y,
además, acepta peticiones si ejecutamos ./mongo —host
alarcosj.esi.uclm.es —port 2701
replica3EnElMismoHost:PRIMARY> rs.status()
{

“set” : “replica3EnElMismoHost”,

“date” : ISODate(“2015-12-10T19:35:59.422Z”),
“myState” : 1,

“members” : [

{
“_id” : 1,

“name” : “192.168.1.130:27017”,

“health” : 1,
“state” : 1,

“stateStr” : “PRIMARY”,

“uptime” : 5180,
“optime” : Timestamp(1449775720, 1),

“optimeDate” : ISODate(“2015-12-10T19:28:40Z”),

“electionTime” : Timestamp(1449771126, 1),


“electionDate” : ISODate(“2015-12-10T18:12:06Z”),

“configVersion” : 5,

www.fullengineeringbook.net
“self” : true

},
{

“_id” : 2,
“name” : “192.168.1.130:37017”,
“health” : 1,

“state” : 2,

“stateStr” : “SECONDARY”,
“uptime” : 5039,

“optime” : Timestamp(1449775720, 1),

“optimeDate” : ISODate(“2015-12-10T19:28:40Z”),
“lastHeartbeat” : ISODate(“2015-12-10T19:35:58.637Z”),

“lastHeartbeatRecv” : ISODate(“2015-12-10T19:35:57.578Z”),

“pingMs” : 0,
“configVersion” : 5

},
{
“_id” : 3,

“name” : “192.168.1.130:47017”,
“health” : 1,
“state” : 2,

“stateStr” : “SECONDARY”,
“uptime” : 5039,
“optime” : Timestamp(1449775720, 1),

“optimeDate” : ISODate(“2015-12-10T19:28:40Z”),
“lastHeartbeat” : ISODate(“2015-12-10T19:35:58.637Z”),
“lastHeartbeatRecv” : ISODate(“2015-12-10T19:35:57.578Z”),

“pingMs” : 0,
“configVersion” : 5

},
{

“_id” : 4,

“name” : “192.168.1.136:27017”,
“health” : 0,

“state” : 8,

“stateStr” : “(not reachable/healthy)”,


“uptime” : 0,

“optime” : Timestamp(0, 0),

“optimeDate” : ISODate(“1970-01-01T00:00:00Z”),
“lastHeartbeat” : ISODate(“2015-12-10T19:35:50.489Z”),

“lastHeartbeatRecv” : ISODate(“1970-01-01T00:00:00Z”),

“configVersion” : -1
},

“_id” : 5,

www.fullengineeringbook.net
“name” : “alarcosj.esi.uclm.es:27017”,

“health” : 0,
“state” : 8,

“stateStr” : “(not reachable/healthy)”,


“uptime” : 0,
“lastHeartbeat” : ISODate(“2015-12-10T19:35:58.625Z”),

“lastHeartbeatRecv” : ISODate(“1970-01-01T00:00:00Z”),

“configVersion” : -1
}

],

“ok” : 1
}

Figura 148. Resultado de rs.status()

¿Por qué, entonces, aparece el host remoto como inalcanzable? Cuando se ha


iniciado la configuración en el nodo primario con rs.initiate, las direcciones IP de los hosts
hacen referencia a la red local del domicilio del autor: 192.168.130 es la IP de localhost, y
192.168.1.136 es la IP del equipo que corre en Virtual Box con Windows 10. Al añadir
alarcosj.esi.uclm.es al conjunto replica3EnElMismoHost, el proceso mongod ubicado en
alarcosj intenta conectar a los procesos que escuchan en los puertos 27017, 37017 y
47017 de 192.168.1.130. alarcosj busca estos procesos en su propia red: la búsqueda es
infructuosa ya que se encuentran corriendo en una red local diferente de la red esi.uclm.es
en la que se encuentra la máquina remota. Por esto, alarcosj es accesible para nuestros
procesos, pero nuestros procesos son inaccesibles para alarcosj.
3.5.1 Modificación de la configuración de la réplica
Para conseguir incorporar alarcosj debemos reconfigurar el conjunto. Para ello
usaremos el comando rs.reconfig(nuevaConfiguración, forzar). La idea es definir una
nueva variable con la nueva configuración y aplicarla con este comando: en la Figura 149
se redefinen las IP de los tres hosts locales por la IP pública (realmente, la IP del router)
con la que son accesibles. Previamente, hemos eliminado el host de la máquina virtual con
rs.remove(“192.168.1.36:27017”).

Figura 149. Aplicación de una nueva configuración

Otra forma de modificar la configuración se ilustra a continuación: en la Figura


150 se recupera mediante el comando rs.conf() y se guarda en una variable (por ejemplo,
en cfg).
replica3EnElMismoHost:PRIMARY> var cfg=rs.conf()
replica3EnElMismoHost:PRIMARY> cfg

www.fullengineeringbook.net
{

“_id” : “replica3EnElMismoHost”,
“version” : 6,

“members” : [
{
“_id” : 1, “host” : “192.168.1.130:27017”, “arbiterOnly” : false,

“buildIndexes” : true, “hidden” : false, “priority” : 1, “tags” : { },

“slaveDelay” : 0, “votes” : 1
},

“_id” : 2, “host” : “192.168.1.130:37017”, “arbiterOnly” : false,


“buildIndexes” : true, “hidden” : false, “priority” : 1, “tags” : { },

“slaveDelay” : 0, “votes” : 1

},
{

“_id” : 3, “host” : “192.168.1.130:47017”, “arbiterOnly” : false,


“buildIndexes” : true, “hidden” : false, “priority” : 1, “tags” : { },
“slaveDelay” : 0, “votes” : 1

},
{
“_id” : 5, “host” : “alarcosj.esi.uclm.es:27017”, “arbiterOnly” : true,

“buildIndexes” : true, “hidden” : false, “priority” : 1, “tags” : { },


“slaveDelay” : 0, “votes” : 1
}

],
“settings” : {
“chainingAllowed” : true, “heartbeatTimeoutSecs” : 10, “getLastErrorModes” : { },

“getLastErrorDefaults” : {
“w” : 1,

“wtimeout” : 0
}

Figura 150. Otra forma de modificar la configuración (I)

Como se ve, cfg tiene un campo members con la lista de nodos. Cualquiera de
estos campos puede modificarse y, luego, reaplicar la configuración con reconfig. Este es
el procedimiento que se sigue en la Figura 151.

Figura 151. Otra forma de modificar la configuración (y II)

www.fullengineeringbook.net
Por otro lado, la consulta mediante rs.conf() que hemos hecho en la Figura 150 nos
sirve para conocer los diferentes campos que pueden establecerse para determinar la
configuración del conjunto: host, arbiterOnly, buildIndexes, hidden, tags, etcétera.
3.6 Cambio de la configuración de lectura
Ya sabemos que el comportamiento por defecto impide leer de los nodos
secundarios (Figura 152), pero también sabemos que esta opción puede modificarse en las
conexiones que establecen los clientes.

Figura 152. Error al leer de un host secundario

En la sección 2.1 (página 77) se enunciaban los cinco modos de lectura: primary,
primaryPreferred, secondary, secondaryPreferred y nearest. Para indicar al cliente del
host azul que deseamos leer con el modo secondary, establecemos la propiedad mediante
el comando db.getMongo().setReadPref: en la Figura 153 se cambia el modo y, a
continuación, se ejecuta el find que fallaba en la figura anterior.

Figura 153. Cambio del modo de lectura

El cambio, por tanto, se establece a nivel de cliente.


3.7 Cambio de la configuración de escritura
Las preferencias de escritura se indican también a nivel de cliente mediante el
campo opcional writeConcern de las operaciones de modificación de datos. En la figura
siguiente, se añade, en la consola del primario, un objeto a la colección prueba. Véase que
a la operación insert se le pasa un segundo parámetro, writeConcern, con los valores w : 3
y wtimeout : 1000:
El primero indica que el nodo primario debe dar por buena la inserción
cuando el objeto esté insertado en 3 nodos (el propio primario y dos
secundarios).
El segundo establece el timeout a partir del cual se aboratará la operación:
en el ejemplo, se ha establecido en 1 segundo (1000 milisegundos).

Figura 154. Modificación de las preferencias de escritura

Los valores admitidos del campo w son los siguientes:


Un entero: denota el número de hosts de la réplica que deben confirmar la

www.fullengineeringbook.net
escritura. Por defecto, es 1. También puede ser 0: en este caso, no se espera
confirmación, aunque sí se recibirán mensajes de error en caso de fallo de
la red.
majority: indica que se espera confirmación de escritura por la mayoría de
los nodos con derecho a voto.
Se puede especificar también un conjunto de etiquetas para indicar los
nodos de los que se espera confirmación.

www.fullengineeringbook.net
4 ARRANQUE DE LOS SERVIDORES CON FICHEROS DE
CONFIGURACIÓN
En la Figura 138 arrancábamos los servidores utilizando el parámetro —replSet en
la línea de comando. En su lugar, podemos establecer la configuración de cada servidor en
un fichero de configuración.
En la figura siguiente se muestran los contenidos de los ficheros de configuración
de los tres servidores locales. Los campos que se han incluido (puede haber muchos más)
indican la ubicación de las bases de datos de cada uno (un directorio con el nombre de su
puerto de escucha que cuelga de /Users/Maco/Desktop/dbsReplica), el puerto (27017,
37017, 47017), si admiten conexiones por http (es decir, mediante un navegador web: cada
servidor Mongo lanza un servidor web en su puerto de escucha +1000: es decir, en los
puertos 28017, 38017 y 48017), el tamaño de la colección oplog y el nombre del conjunto
de réplica.

Figura 155. Ficheros de configuración para los tres servidores

El siguiente comando, lanzado desde la consola del sistema operativo, lanza un


servidor utilizando el fichero de configuración mongod27017.conf (en la Figura 155, el
que aparece al fondo):
sudo ./mongod —config /Applications/mongodb-osx-x86_64-3.0.7/bin/mongod27017.conf

www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 8. Gestión de la base de datos desde
una aplicación web Java
En esta sección crearemos una aplicación web que ofrezca un pequeño conjunto de
funcionalidades para manipular una base de datos Mongo. En este caso, utilizaremos una
base de datos similar a la anterior (clientes, llamadas y tarifas) pero, ahora, de objetos no
incrustados: es decir, tendremos las tres colecciones separadas. El esquema conceptual de
la base de datos será el siguiente:

Figura 156. Estructura de las colecciones, que estarán separadas

Como se ve, cada cliente conoce a su tarifa y, cada llamada, al cliente que la ha
realizado. La relación 1:1 desde Llamada a Cliente está modelando una relación 1:1 desde
Cliente hasta Llamada: en la implementación de la base de datos, cada llamada contendrá
un campo que apuntará al identificador del cliente que ha efectuado la llamada. Respecto
de la relación entre las clases Cliente y Tarifa sucede algo muy parecido: son muchos los
clientes que tienen la misma tarifa; así pues, esta relación 1:n desde Tarifa hacia Cliente la
representamos como una relación 1:1 desde Cliente hacia Tarifa: en Cliente habrá un
campo que apunte al identificador de la Tarifa.

www.fullengineeringbook.net
1 CREACIÓN DE LA BASE DE DATOS SINTÉTICA
En la sección 3.1 del Capítulo 2 (página 17) creamos la base de datos de objetos
incrustados utilizando el tipo Document. Para variar un poquito, utilizaremos ahora
BsonDocument (recuérdese la jerarquía de herencia de la Figura 11, página 12), que es un
tipo de datos quizá más incómodo de utilizar, pero más versátil y con menos propensión a
introducir errores.
Crearemos por tanto una base de datos a la que llamaremos
telefonosNoIncrustados con las tres colecciones mencionadas.
1.1 Creación de la colección de Tarifas
La creación de esta colección es muy sencilla, pues basta con insertar directamente
4 objetos fijos correspondientes a las 4 tarifas indicadas en el diargama de clases de la
Figura 156.
Como se ve en el código siguiente, tras conectar al servidor con el objeto
mongoCliente y recuperar la base de datos en db (recuérdese que la operación
getDatabase crea, si no existía, la base de datos cuyo nombre se pasa como parámetro), se
crea directamente la colección y se recupera en el objeto tarifas.
Puesto que en la colección almacenaremos los objetos en forma de instancias de
BsonDocument, debemos explicitar este tipo de datos en el segundo parámetro pasado a
db.getCollection, de modo que la sentencia queda así:
MongoCollection<BsonDocument> tarifas=
db.getCollection(“tarifas”, BsonDocument.class);

A continuación, en cada uno de los cuatro bloques creamos directamente un


BsonDocument al que añadimos los tres campos de dominio (cuotaFija, establecimiento e
importePorSegundo) y el identificador (campo _id). Si no explicitamos el valor del _id,
Mongo creará uno por nosotros utilizando el procedimiento indicado en la sección 1.3.2 de
la página 13. Obsérvese también que las llamadas a append que hacemos con cada campo
el BsonDocument toman, como segundo parámetro, un BsonValue de los representados en
la Figura 11. Una vez creado cada objeto, lo insertamos.
public static void main(String[] args) throws IOException {

final int NUMERO_DE_CLIENTES = 1000;


MongoClient mongoClient=new MongoClient(“localhost”, 27017);

MongoDatabase db = mongoClient.getDatabase(“telefonosNoIncrustados”);

if (db.getCollection(“tarifas”)==null)
db.createCollection(“tarifas”);

MongoCollection<BsonDocument> tarifas=
db.getCollection(“tarifas”, BsonDocument.class);

BsonDocument tPlana=new BsonDocument();


tPlana.append(“_id”, new BsonString(“TarifaPlana”));

tPlana.append(“cuotaFija”, new BsonDouble(250));

www.fullengineeringbook.net
tPlana.append(“establecimiento”, new BsonDouble(0.15));

tPlana.append(“importePorSegundo”, new BsonDouble(0));


tarifas.insertOne(tPlana);

BsonDocument t50Minutos=new BsonDocument();


t50Minutos.append(“_id”, new BsonString(“Tarifa50Minutos”));
t50Minutos.append(“cuotaFija”, new BsonDouble(15));

t50Minutos.append(“establecimiento”, new BsonDouble(0.15));

t50Minutos.append(“importePorSegundo”, new BsonDouble(0.01));


tarifas.insertOne(t50Minutos);

BsonDocument tTardes=new BsonDocument();

tTardes.append(“_id”, new BsonString(“TarifaTardes”));


tTardes.append(“cuotaFija”, new BsonDouble(25));

tTardes.append(“establecimiento”, new BsonDouble(0.30));

tTardes.append(“importePorSegundo”, new BsonDouble(0.08));


tarifas.insertOne(tTardes);

BsonDocument tFDS=new BsonDocument();


tFDS.append(“_id”, new BsonString(“TarifaFinDeSemana”));
tFDS.append(“cuotaFija”, new BsonDouble(25));

tFDS.append(“establecimiento”, new BsonDouble(0.35));


tFDS.append(“importePorSegundo”, new BsonDouble(0.01));
tarifas.insertOne(tFDS);


}

Figura 157. Creación de la colección Tarifas

1.2 Creación de la colección de Clientes


En esta colección insertaremos 1000 clientes con datos personales aleatorios. A
cada uno le asignaremos un número de teléfono incremental, empezando en el
600.000.000.
Tras leer los nombres y apellidos de sendos ficheros, se inicia un bucle que,
realmente, terminará cuando la variable contador cont haya alcanzado el valor de la
constante NUMERO_DE_CLIENTES. En cada iteración del bucle se toman un nombre y
dos apellidos al azar, se genera un dni aleatorio, un teléfono incremental y se le asigna una
tarifa aleatoria de las cuatro que se han definido. Se genera también una fecha de
nacimiento aleatoria (usando la función fechaAlAzar() que ya mostramos en la Figura 21,
página 19) y, ya que tenemos toda la información necesaria para construir un cliente, lo
construimos como un BsonDocument y lo insertamos en la colección. Como _id,
asignamos el número de cliente de la compañía, y no el _id que autoasigne Mongo.
public static void main(String[] args) throws IOException {

final int NUMERO_DE_CLIENTES = 1000;

… // Aquí se crean las tarifas ( Figura 157 )


if (db.getCollection(“clientes”)==null)

db.createCollection(“clientes”);

www.fullengineeringbook.net

MongoCollection<BsonDocument> clientes=db.getCollection(“clientes”, BsonDocument.class);


String nombresFileName=“/Users/Maco/…/nombres.txt”;

String apellidosFileName=“/Users/Maco/…/apellidos.txt”;
Vector<String> nombres=read(nombresFileName);
Vector<String> apellidos=read(apellidosFileName);

int cont=0;

Random dado=new Random();


for (int i=0; i<nombres.size()*apellidos.size(); i++) {

int posNombre=dado.nextInt(nombres.size());

int posApellido1=dado.nextInt(apellidos.size());
int posApellido2=dado.nextInt(apellidos.size());

int iDNI=5000000+dado.nextInt(5000000);

int iTelefono=600000000+i;
int tarifa=dado.nextInt(4);

String tipoTarifa=“TarifaPlana”;
if (tarifa==1) tipoTarifa=“Tarifa50Minutos”;
else if (tarifa==2) tipoTarifa=“TarifaTardes”;

else if (tarifa==3) tipoTarifa=“TarifaFinDeSemana”;



GregorianCalendar fechaDeNacimiento = fechaAlAzar();


String nombre=nombres.get(posNombre);
String apellido1=apellidos.get(posApellido1);

String apellido2=apellidos.get(posApellido2);
String dni=””+iDNI;
String telefono=””+iTelefono;

BsonDocument cliente=new BsonDocument();


cliente.append(“_id”, new BsonInt32(cont+1));

cliente.append(“nombre”, new BsonString(nombre));


cliente.append(“apellido1”, new BsonString(apellido1));

cliente.append(“apellido2”, new BsonString(apellido2));

cliente.append(“dni”, new BsonString(dni));


cliente.append(“telefono”, new BsonString(telefono));

cliente.append(“idTarifa”, new BsonString(tipoTarifa));

cliente.append(“fechaDeNacimiento”,
new BsonDateTime(fechaDeNacimiento.getTimeInMillis()));

clientes.insertOne(cliente);

cont++;
if (cont==NUMERO_DE_CLIENTES)

break;

}

Figura 158. Creación de la colección Clientes

www.fullengineeringbook.net
1.3 Creación de la colección de Llamadas
Aquí crearemos 50.000 llamadas que habrán sido realizadas por los 1.000 clientes
(y además entre ellos: es decir, no habrá llamadas a teléfonos de otras compañías)
insertados en la sección anterior.
Iteraremos 50.000 veces creando una llamada en cada iteración: el cliente que la
efectúe será uno de los 1.000 e irá dirigida a un número de otro cliente al azar. La duración
será aleatoria y la fecha será generada aleatoriamente y establecida a uno de los 300
primeros días de 2015, con una versión ligeramente modificada de la función fechaAlAzar.
En esta ocasión, dejaremos que Mongo asigne valor al campo _id, así que no
explicitaremos su valor al construir el BsonDocument.
… // Código de creación de Tarifas y Clientes

if (db.getCollection(“llamadas”)==null)
db.createCollection(“llamadas”);
MongoCollection<BsonDocument> llamadas=db.getCollection(“llamadas”, BsonDocument.class);

int NUMERO_DE_LLAMADAS = 50000;


for (int i=0; i<NUMERO_DE_LLAMADAS; i++) {
int posCliente = dado.nextInt(NUMERO_DE_CLIENTES);

BsonDocument cliente = clientes.find().skip(posCliente).iterator().tryNext();


BsonInt32 idCliente=cliente.getInt32(“_id”);
int telefonoDestino=600000000+dado.nextInt(NUMERO_DE_CLIENTES);

double auxi=dado.nextDouble();
int duracion;
if (auxi<0.4) {

duracion=dado.nextInt(200);
} else if (auxi<0.6) {
duracion=dado.nextInt(800);

} else if (auxi<0.8) {

duracion=dado.nextInt(1800);
} else {

duracion=dado.nextInt(3000);

}
GregorianCalendar fecha = fechaAlAzar(2015, 2015, 300);

BsonDocument llamada=new BsonDocument();

llamada.append(“origen”, cliente.get(“telefono”));
llamada.append(“destino”, new BsonString(””+telefonoDestino));

llamada.append(“duracion”, new BsonInt32(duracion));

llamada.append(“fecha”, new BsonDateTime(fecha.getTimeInMillis()));


llamada.append(“idCliente”, idCliente);

llamadas.insertOne(llamada);

}
mongoClient.close();

www.fullengineeringbook.net

private static GregorianCalendar fechaAlAzar(int yearFrom, int yearTo,


int lastDayOfYear) {

GregorianCalendar result = new GregorianCalendar();


int year = randBetween(yearFrom, yearTo);
result.set(GregorianCalendar.YEAR, year);

int dayOfYear = randBetween(0, lastDayOfYear);

result.set(GregorianCalendar.DAY_OF_YEAR, dayOfYear);
return result;

Figura 159. Creación de Llamadas y otra función fechaAlAzar

www.fullengineeringbook.net
2 DISEÑO DE LA APLICACIÓN
Desarrollaremos una aplicación web que ofrecerá el siguiente conjunto de
funcionalidades:
Gestión de clientes (altas, bajas, modificaciones y consultas).
Consultar las llamadas de un cliente, tanto las totales como las de un
periodo.
Construir facturas de un determinado mes.
Mostrar las facturas de un cliente.
Mantendremos la separación de las capas de presentación y dominio respecto de la
base de datos a través de una capa de persistencia formada por un conjunto de clases
DAO, que serán las únicas con acceso al servidor de MongoDB. Es decir: en la medida de
lo posible, evitaremos utilizar objetos de los paquetes com.mongodb en el dominio y la
presentación. Así, cuando desde una página web del sistema se recupere, por ejemplo, un
cliente, la página obtendrá y mostrará un objeto de tipo Cliente y no un BsonDocument.
Esto supone una mayor carga para el servidor web (que debe hacer transformaciones de
BsonDocument a Cliente, a Llamada, etcétera), pero independiza la presentación y la
lógica del dominio respecto de la tecnología de base de datos utilizada.
Como se ha sugerido, la aplicación se estructura en tres capas: presentación,
dominio y almacenamiento. En la presentación hay un conjunto de páginas jsp (Figura
160) que permiten al usuario interactuar con los datos. La aplicación arranca por la página
index.jsp y, a partir de ahí, el usuario puede listar clientes, buscar clientes y lanzar el
proceso de facturación: desde listarClientes.jsp el usuario puede ver la ficha de un cliente
(recurso fichaCliente.jsp), consultar sus llamadas (verLlamadas.jsp) o ver los detalles de
su tarifa (detalleTarifa.jsp). Cuando se está en fichaCliente.jsp se puede ir también a
verLlamadas.jsp, así como actualizar los datos del cliente mediante actualizarCliente.jsp,
y eliminarlo con eliminarCliente.jsp. Estos dos últimos recursos reciben el cliente que se
desea actualizar, envían la petición al servidor web y le devuelven la respuesta al recurso
solicitante (fichaCliente.jsp) en formato JSON.
Desde index.jsp también se puede buscar a un cliente (buscarCliente.jsp, que lleva
a fichaCliente.jsp) y lanzar el proceso de facturación (facturacion.jsp) que, a su vez, se
ejecuta a través de facturar.jsp, que le devuelve los resultados en JSON. Cuando se han
construido las facturas, éstas pueden consultarse mediante consultarFacturas.jsp.

www.fullengineeringbook.net
Figura 160. Diseño de la capa de presentación

Respecto de las capas de dominio y persistencia, su estructura se muestra en la


Figura 161: la capa de dominio incluye, respecto de figuras anteriores, la clase Factura;
en la figura se ilustra también la relación entre cada clase de dominio y su correspondiente
clase DAO asociada.

Figura 161. Estructura de las capas de dominio y persistencia

Respecto de las relaciones entre las tres capas, se muestran en la figura siguiente:
las flechas de dependencia en azul son las llamadas directas que las páginas jsp hacen
www.fullengineeringbook.net
diractamente a clases DAO. Nótese, no obstante, que la presentación no manipula en
ningún caso objetos de Mongo, sino solo objetos de nuestra aplicación. La dependencia
respecto de la tecnología de almacenamiento de datos reside solamente en las clases DAO.

Figura 162. Relaciones entre las tres capas

www.fullengineeringbook.net
3 UN VISTAZO EN DETALLE A UNA CLASE DAO
Hagamos un seguimiento del listado de clientes, que se corresponde con una serie
de interacciones entre objetos del fragmento del diagrama de clases siguiente:

Figura 163. Clases que intervienen para obtener el listado de clientes

listarClientes.jsp invoca directamente a la operación getClientes de DAOCliente, la


cual le devuelve un Vector<Cliente>. El listado de clientes muestra clientes de n en n, con
lo que getClientes toma dos parámetros: el inicio del listado y el número de clientes (n)
que se desea mostrar en cada página.
La Figura 164 muestra la cabecera y primeras líneas de esta operación: tal vez la
sentencia más interesante sea la última, en la que se hace un encadenamiento de
operaciones find, sort, skip, limit de forma muy parecida a como podríamos hacerlo en la
consola. En la Figura 41 (página 25) mostrábamos cómo iban cambiando los tipos de
datos en la consola según hacíamos operaciones sobre la base de datos y la colección;
desde Java, los tipos de datos van también de manera parecida, aunque no igual.
Comentamos esto dentro de unas pocas líneas.
public static Vector<Cliente> getClientes(int inicio, int n) {
MongoCollection<BsonDocument> colClientes=

MongoBroker.get().getDB().getCollection(“clientes”, BsonDocument.class);

BsonDocument criterioOrdenacion=new BsonDocument(“apellido1”, new BsonInt32(1));

criterioOrdenacion.append(“apellido2”, new BsonInt32(1));


criterioOrdenacion.append(“nombre”, new BsonInt32(1));

MongoCursor<BsonDocument> clientes = colClientes.find().sort(criterioOrdenacion).

skip(inicio).limit(10).iterator();

Figura 164

El objeto principal sobre el que ejecutaremos las operaciones es el


MongoCollection, que recuperamos a través del MongoBroker. Ésta es una clase escrita
por nosotros (es decir, no es de ningún paquete com.mongodb) que representa el punto
único de acceso a la base de datos desde el resto de clases de la aplicación. La
MongoCollection sí que está definida en com.mongodb: se trata de una interfaz que ya
hemos usado al crear las bases de datos sintéticas. Ofrece casi todas las operaciones que
ya hemos utilizado desde la consola. En la Figura 165 muestra casi toda su colección de
operaciones, aunque se han eliminado las menos relevantes: véase que tenemos diversas

www.fullengineeringbook.net
versiones de find, aggregate, mapReduce, etcétera.

Figura 165. Operaciones de la interfaz MongoCollection

Como se ve en la figura anterior, la operación find devuelve un FindIterable, que


ofrece a su vez las operaciones necesarias para ejecutar los comandos limit, skip, sort,
etcétera sobre los objetos contenidos en la colección. FindIterable es también un
MongoIterable; así pues, en FindIterable tenemos también la operación first(), que
devuelve el primer elemento, e iterator(), que devuelve un MongoCursor. Con los cursores
podemos recorrer los elementos de la colección para realizar operaciones con ellos.

www.fullengineeringbook.net
Figura 166. MongoIterable, FindIterable y MongoCursor

A continuación reproducimos el código completo de getClientes. La operación ya


comentada y que hemos resaltado se ejecuta sobre colClientes, una instancia de
MongoCollection. Sobre este objeto ejecutamos un find, que devuelve un FindIterable;
sobre éste, un sort (que no altera el tipo devuelto), un skip (que tampoco), un limit (que
tampoco) y, por fin, un iterator, que devuelve un MongoCursor como el de la figura
anterior.
Como se desea que el listado de clientes venga ordenado por apellidos y nombre,
pasamos como parámetro a sort un BsonDocument con el criterioOrdenacion, que no es
sino un objeto con una estructura similar a loque escribiríamos en la
consola: db.clientes.find().sort({apellido1 : 1, apellido2 :1, nombre : 1}) . En este caso, el objeto se
construye poco a poco, añadiendo en tres sentencias los tres campos de ordenación y sus
valores.
La operación, que es invocada desde listarClientes.jsp, devuelve a la página (véase
Figura 163) un Vector<Cliente>, por lo que por cada objeto (es decir, por cada
BsonDocument) leído de la base de datos se ha de construir un objeto Cliente de nuestra
capa de dominio. El cursor clientes (obtenido a partir de aplicar los ya mencionados find,
sort, skip, limit y, finalmente, iterator a la MongoCollection colClientes) lo recorremos
con hasNext(), y con next() vamos obteniendo, en cada iteración, cada uno de los
BsonDocument que representa cada cliente.
El tratamiento que damos a cada BsonDocument es bien sencillo, pues basta con
recuperar los campos y, con ellos, crear la instancia de nuestro cliente del dominio y
añadirlo al vector del resultado.
public static Vector<Cliente> getClientes(int inicio, int n) {
MongoCollection<BsonDocument> colClientes=

MongoBroker.get().getDB().getCollection(“clientes”, BsonDocument.class);

www.fullengineeringbook.net
BsonDocument criterioOrdenacion=new BsonDocument(“apellido1”, new BsonInt32(1));

criterioOrdenacion.append(“apellido2”, new BsonInt32(1));


criterioOrdenacion.append(“nombre”, new BsonInt32(1));

MongoCursor<BsonDocument> clientes = colClientes.find().sort(criterioOrdenacion).


skip(inicio).limit(10).iterator();
Vector<Cliente> result=new Vector<>();

int id;

String nombre, apellido1, apellido2, dni, telefono;


GregorianCalendar fechaDeNacimiento;

Cliente cliente;

BsonDocument bso;
while (clientes.hasNext()) {

bso=clientes.next();

id=bso.get(“_id”).asInt32().getValue();
nombre=bso.get(“nombre”).asString().getValue();

apellido1=bso.get(“apellido1”).asString().getValue();
apellido2=bso.get(“apellido2”).asString().getValue();
dni=bso.get(“dni”).asString().getValue();

telefono=bso.get(“telefono”).asString().getValue();
fechaDeNacimiento=new GregorianCalendar();
fechaDeNacimiento.setTimeInMillis(

bso.get(“fechaDeNacimiento”).asDateTime().getValue());
cliente=new Cliente(id, nombre, apellido1, apellido2,
dni, telefono, fechaDeNacimiento);

result.add(cliente);
}
return result;

Figura 167. Código completo de getClientes

3.1 Delete
Por otro lado, a la fichaCliente.jsp se llega desde listarClientes.jsp. Desde aquí se
puede eliminar un cliente (y sus llamadas) y modificar sus datos. Estas operaciones se
ejecutan mediante dos operaciones que fichaCliente.jsp envía a eliminarCliente.jsp y a
actualizarCliente.jsp mediante sendas peticiones AJAX.

www.fullengineeringbook.net
Figura 168. Clases involucradas en la modificación y eliminación de clientes

eliminarCliente.jsp ejecuta directamente la operación estática delete(int idCliente)


sobre la clase DAO. Se trata de buscar en la colección de clientes a aquel cuyo _id
coincide con el parámetro pasado y eliminarlo. Mongo no tiene, como sí sucede en bases
de datos relacionales, eliminaciones o actualizaciones en cascada, por lo que debemos
eliminar explícitamente también las llamadas asociadas al cliente. En el código de la
Figura 169 se busca en la colección de clientes aquel objeto cuyo _id coincide con el
idCliente pasado como parámetro y se elimina con deleteOne. A continuación, se eliminan
de llamadas todos aquellos objetos cuyo campo idCliente (que hace las veces de lo que en
el modelo relacional es una clave externa) coincide con el idCliente. Obsérvese la
diferencia entre deleteOne (que borra un objeto) y deleteMany (que borra de la colección
todos aquellos objetos que coinciden con el criterio).
Los resultados de las eliminaciones se devuelven en objetos de tipo DeleteResult.
Como resultado del método, se devuelve un array con el número de objetos eliminados de
las colecciones cliente (a lo sumo se eliminará 1) y llamadas.
public static long[] delete(int idCliente) {
MongoCollection<BsonDocument> colClientes=MongoBroker.get().

getDB().getCollection(“clientes”, BsonDocument.class);
BsonDocument criterio=new BsonDocument(“_id”, new BsonInt32(idCliente));
DeleteResult resultCliente = colClientes.deleteOne(criterio);

MongoCollection<BsonDocument> colLlamadas=MongoBroker.get().
getDB().getCollection(“llamadas”, BsonDocument.class);
criterio=new BsonDocument(“idCliente”, new BsonInt32(idCliente));

DeleteResult resultLlamadas=colLlamadas.deleteMany(criterio);

return new long[] {

resultCliente.getDeletedCount(),

resultLlamadas.getDeletedCount()
};

Figura 169. Eliminación de un cliente y de sus llamadas

El resultado devuelto por delete (es decir, el array de dos long) se devuelve al
recurso web que lo ejecutó: eliminarCliente.jsp recibe estos dos datos y los devuelve, en
formato JSON, al recurso web original (fichaCliente.jsp). Como se ve en la Figura 170,
eliminarCliente.jsp recibe en la request (procedente de fichaCliente.jsp) el idCliente, llama
a delete, recoge el resultado de delete en la variable resultados y coloca los dos valores
que llegan en el objeto jso, que es lo que se devuelve a fichaCliente.jsp.
<%@ page language=“java” contentType=“application/json; charset=UTF-8”
pageEncoding=“UTF-8”%>

<%@ page import=“edu.uclm.esi.mongo.isotel.persistencia.DAOCliente” %>

<%@page import=“org.json.simple.JSONObject, org.json.simple.JSONArray”%>


www.fullengineeringbook.net
<%

String sIdCliente=request.getParameter(“idCliente”);
long[] resultados=DAOCliente.delete(Integer.parseInt(sIdCliente));


JSONObject jso=new JSONObject();
jso.put(“resultado”, “OK”);

jso.put(“clientesEliminados”, resultados[0]);

jso.put(“llamadasEliminadas”, resultados[1]);

%>


<%= jso.toJSONString() %>

Figura 170. Código del recurso eliminarCliente.jsp

El flujo de mensajes se ilustra en la figura siguiente:

Figura 171. Flujo de mensajes desde que el usuario decide eliminar un cliente hasta que efectivamente se elimina

3.2 Update
La actualización de datos es parecida. No obstante, y como se mostraba en la
Figura 168, la operación se ejecuta a través de la clase Cliente: actualizarCliente.jsp
recibe de fichaCliente todos los datos del cliente (entre ellos el _id, que no se puede
cambiar, como tampoco el número de teléfono), lo instancia y ejecuta sobre éste su
operación update. El update de Cliente delega su ejecución a la clase que le gestiona la
persistencia: es decir, a DAOCliente.
<%@ page language=“java” contentType=“application/json; charset=UTF-8”

pageEncoding=“UTF-8”%>
<%@ page import=“edu.uclm.esi.mongo.isotel.dominio.Cliente,

edu.uclm.esi.mongo.isotel.auxiliares.*,

java.util.*” %>

www.fullengineeringbook.net
<%@page import=“org.json.simple.JSONObject”%>

<%
String sIdCliente=request.getParameter(“idCliente”);

String dni=request.getParameter(“dni”);
String nombre=request.getParameter(“nombre”);
String apellido1=request.getParameter(“apellido1”);

String apellido2=request.getParameter(“apellido2”);

String sFechaDeNacimiento=request.getParameter(“fechaDeNacimiento”);
String idTarifa=request.getParameter(“tarifa”);

Integer idCliente=Integer.parseInt(sIdCliente);
Cliente cliente=new Cliente(idCliente);

cliente.setDni(dni);

cliente.setNombre(nombre);
cliente.setApellido1(apellido1);

cliente.setApellido2(apellido2);
GregorianCalendar fechaDeNacimiento=Fechas.getFecha(sFechaDeNacimiento);
cliente.setFechaDeNacimiento(fechaDeNacimiento);

cliente.setTarifa(idTarifa);
cliente.update();

JSONObject jso=new JSONObject();


jso.put(“resultado”, “OK”);
%>

<%= jso.toJSONString() %>

Figura 172. Código de actualizarCliente.jsp

El código del update de Cliente es trivial ya que se trata de un mero paso de


mensaje (Figura 173), mientras que el de DAOCliente, sin ser demasiado complejo, sí que
es algo más largo (Figura 174).
public void update() {

DAOCliente.update(this);

Figura 173. El update de Cliente

En el update de la clase DAO debemos actualizar (con la operación replaceOne) el


cliente que pasan como parámetro a este método. replaceOne toma dos parámetros: el
criterio para buscar el objeto que se desea actualizar (variable criterio en el código, que
toma sólo el campo id del objeto cliente) y los valores con que se debe quedar el objeto
(en este caso el dni, el nombre, los apellidos, el teléfono, la fechaDeNacimiento y el
nombre de su tarifa).
Aunque es posible que uno de los campos (el nombre, por ejemplo) no cambie,
cuando se utilizan los métodos replace es preciso dar valor a todos los campos. Si no,
mongo eliminará del objeto los campos no asignados: replace reemplaza, efectivamente, el

www.fullengineeringbook.net
antiguo objeto por el nuevo. Aunque hemos dicho que el teléfono no puede cambiar, si no
añadiésemos a la variable criterioActualizacion, el cliente perdería su número de teléfono
al actualizar en la base de datos.
public static void update(Cliente cliente) {

MongoCollection<BsonDocument> colClientes=MongoBroker.get().getDB().
getCollection(“clientes”, BsonDocument.class);

BsonDocument criterio=new BsonDocument(“_id”, new BsonInt32(cliente.getId()));


BsonDocument criterioActualizacion=

new BsonDocument(“dni”, new BsonString(cliente.getDNI()));

criterioActualizacion.append(“nombre”, new BsonString(cliente.getNombre()));


criterioActualizacion.append(“apellido1”, new BsonString(cliente.getApellido1()));
criterioActualizacion.append(“apellido2”, new BsonString(cliente.getApellido2()));

criterioActualizacion.append(“telefono”, new BsonString(cliente.getTelefono()));


criterioActualizacion.append(“fechaDeNacimiento”,
new BsonDateTime(cliente.getFechaDeNacimiento().getTimeInMillis()));

criterioActualizacion.append(“idTarifa”,
new BsonString(cliente.getTarifa().getClass().getSimpleName()));


colClientes.replaceOne(criterio, criterioActualizacion);
}

Figura 174. Código de update en la clase DAOCliente

3.3 Materialización de instancias (SELECT)


En nuestro ejemplo, cuando el usuario está en listarClientes.jsp (Figura 175) y
pulsa sobre el _id de un cliente se muestra su ficha.

Figura 175. Fragmento de listarClientes.jsp

En este proceso, fichaCliente.jsp recibe como parámetro el _id del cliente y le pide
a la clase DAO que le devuelva la instancia de Cliente que corresponde a ese _id. Las
clases que participan en este proceso son las de la Figura 176: al llegar a DAO cliente se
ejecuta una operación de materialización una instancia de tipo Cliente, que es lo que se
devuele a fichaCliente.jsp: la materialización consiste en crear una instancia a partir de la
información almacenada en la base de datos.

www.fullengineeringbook.net
Figura 176. Clases involucradas en la recuperación de un cliente

De alguna manera, ya hemos visto cómo materializar instancias cuando, en la


Figura 167, obteníamos el listado de clientes, pues por cada BsonDocument creábamos un
Cliente que colocábamos en el vector de resultados. La operación load del siguiente
ejemplo recibe dos parámetros: el idCliente y el cliente que debe rellenarse a partir de ese
idCliente: tras acceder a la colección, se construye el criterio de búsqueda (un
BsonDocument con un único campo _id y el idCliente pasado como parámetro). Se lanza
la consulta contra la colección utilizando find y se recupera con find el único objeto que
cumple el criterio, guardándolo en el objeto bso, de tipo BsonDocument. A continuación
se leen todos los campos de bso y se asignan a variables que, por último, son asignadas al
cliente pasado como parámetro.
public static void load(int idCliente, Cliente cliente) {
MongoCollection<BsonDocument> colClientes=MongoBroker.get().getDB().

getCollection(“clientes”, BsonDocument.class);
BsonDocument criterio=new BsonDocument(“_id”, new BsonInt32(idCliente));
BsonDocument bso = colClientes.find(criterio).first();

int id;
String nombre, apellido1, apellido2, dni, telefono;
GregorianCalendar fechaDeNacimiento;

id=bso.get(“_id”).asInt32().getValue();

nombre=bso.get(“nombre”).asString().getValue();
apellido1=bso.get(“apellido1”).asString().getValue();

apellido2=bso.get(“apellido2”).asString().getValue();

dni=bso.get(“dni”).asString().getValue();
telefono=bso.get(“telefono”).asString().getValue();

fechaDeNacimiento=new GregorianCalendar();

fechaDeNacimiento.setTimeInMillis(
bso.get(“fechaDeNacimiento”).asDateTime().getValue());

cliente.setIdCliente(id);
cliente.setNombre(nombre);

cliente.setApellido1(apellido1); cliente.setApellido2(apellido2);

cliente.setDni(dni); cliente.setTelefono(telefono);
cliente.setFechaDeNacimiento(fechaDeNacimiento);

www.fullengineeringbook.net
Figura 177. Código para leer y construir (materializar) una instancia

www.fullengineeringbook.net
4 EJECUCIÓN DE OPERACIONES MAPREDUCE
Desde Java, y sobre una colección, podemos ejecutar de dos maneras comandos
MapReduce:
Mediante el comando mapReduce de MongoCollection.
Mediante un objeto MapReduceCommand del tipo DBCollection. Este
mecanismo está actualmente deprecated, por lo que no lo utilizaremos.
Como recordamos del Capítulo 4 (página 55), las reglas MapReduce constan de
una función JavaScript map, otra reduce y una colección de salida, out. Pueden llevar
también una función JavaScript finalize y, también, un scope, en el que se declaran
variables globales que vayan a ser utilizadas por las funciones.
En nuestro ejemplo, el proceso de facturación se ejecutará en dos pasos:
En el primero, una regla MapReduce extraerá las llamadas realizadas por
los clientes y las colocará en un mapa en el que tendremos, por cada
cliente, su número de llamadas y la duración total.
En el segundo paso, construiremos una factura por cada cliente, según su
tarifa y el consumo que haya realizado.
El proceso de facturación se lanzará mediante el botón Facturar de la sencilla
página web de la Figura 178.

Figura 178. facturacion.jsp, desde donde se lanza el proceso de facturación

Al hacer clic en el botón, se envía una petición AJAX a facturar.jsp que, a su vez,
llama al método facturarMR de DAOFactura, que es el que nos interesa en estos
momentos.
Las funciones map y reduce las tenemos almacenadas en una colección scripts en
la base de datos (recuérdese la sección 5 del Capítulo 4, página 62). El código de estas dos
funciones es el de la Figura 179:
La función llamadasDeUnMes (nuestro map) emite (es decir, añade a la
colección correspondiente a cada clave) dos objetos por cada llamada: uno
con las duraciones y el valor 0. El valor 0 se envía porque Mongo no
ejecuta la función reduce para colecciones del mapa en las que haya un
solo valor. Por tanto, si un cliente hubiese realizado solamente una llamada,
la función reduce no se le aplicaría y no todos los objetos de la colección
de facturación resultante quedarían con el mismo formato.
www.fullengineeringbook.net
La función facturacion (nuestra reduce) recibe cada uno de los pares clave,
valor emitidos por llamadasDeUnMes. Puesto que por cada llamada ha
recibido dos valores (la duración de la llamada y el 0), divide por dos para
calcular el número real de llamadas realizadas. La duración total se calcula
sumando las duraciones recibidas en la colección (es decir, en el campo
valor).
// Función map

var llamadasDeUnMes = function() {


if (this.fecha.getMonth()==mes && this.fecha.getFullYear()==year) {

emit(this.idCliente, this.duracion);

emit(this.idCliente, 0);
}

// Función reduce
var facturacion=function(clave, valor) {

var numeroDeLlamadas=valor.length/2;

var duracionTotal = 0;

for (var i=0; i<numeroDeLlamadas; i++) {


duracionTotal=duracionTotal + valor[i];
}

var result = {
numeroDeLlamadas : numeroDeLlamadas,
duracionTotal : duracionTotal

};
return result;
}

Figura 179. Funciones map y reduce para calcular las facturas

En la figura siguiente se muestra cómo, en efecto, si suprimimos el


emit(this.idCliente, 0) de la función map, hay varios clientes que, como solamente han
realizado una llamada en el periodo facturado, quedan con un formato diferente:

Figura 180. La colección resultante, sin el segundo emit

Manteniendo ese segundo emit, sin embargo, los resultados tienen el aspecto que
deseamos:

www.fullengineeringbook.net
Figura 181. La colección resultante, con el segundo emit

El lector habrá observado que la función llamadasDeUnMes (que, como todas las
funciones map, no toma parámetros), compara el mes y el año de la fecha de cada llamada
con dos variables mes y year que, de alguna manera, se han debido pasar en la llamada a
mapReduce. En efecto, estos valores se pasan en el parámetro scope; desde la consola, el
comando es el siguiente:

Figura 182. Ejecución desde consola de un mapReduce con scope

El comando de la Figura 182 declara para las dos funciones utilizadas las variables
mes y year, con valores respectivos de 9 y 2015, para calcular la facturación del mes de
octubre de 2015. Aunque octubre es el 10º mes del año, pasamos un 9 porque, en
JavaScript, la función getMonth() del tipo Date devuelve un valor entre 0 (enero) y 11
(diciembre).
Con todas estas consideraciones estamos ya listos para invocar el comando
mapReduce desde la aplicación Java. Como decíamos, a esta regla se la invoca desde la
operación facturaMR de DAOFactura. Las primeras líneas de este método se muestran en
la Figura 183:
Inicialmente se recupera (en forma de JavaScript: nótense las llamadas a
asJavaScript) de la colección scripts el código de las dos funciones
llamadasDeUnMes y facturacion, que se asignan a dos variables map y
reduce.
A continuación, se recupera una referencia a la colección llamadas en la
variable colLlamadas.
En el bloque grande de instrucciones se prepara la llamada a la regla
mapReduce: sobre colLlamadas llamamos a mapReduce pasando el código
de ambas funciones como parámetros. La ejecución de esta operación aún
no ejecuta la regla, pero devuelve un objeto de tipo MapReduceIterable,
que nos sirve para especificar el contexto de ejecución: el nombre de la
colección de salida (facturacion2015_10, si pasamos 2015 y 10 como
valores de los parámetros mes y year), los valores de las variables mes y
year para la ejecución de la regla (es decir, el valor del scope, según
presentamos unas líneas arriba y mostramos en la Figura 182) y la acción
www.fullengineeringbook.net
que se debe ejecutar. La acción puede ser MERGE, REPLACE o REDUCE.
Las comentamos un poquito más abajo.
Una vez que toda la regla ha sido preparada, para que ésta se ejecute
efectivamente invocamos a la operación first().
Respecto del parámetro pasado a la operación action, MERGE, REPLACE y
REDUCE actúan de la siguiente manera:
Si la colección resultante ya existía, MERGE le añade los nuevos
resultados, respetando los preexistentes. En caso de conflicto en algún _id,
el objeto antiguo es eliminado.
REPLACE reemplaza toda la colección con los nuevos resultados: es decir,
los antiduos desaparecen.
Si la colección resultante ya existía, REDUCE añade también los nuevos
resultados, respetando los preexistentes. No obstante, en caso de conflicto
en algún _id, la función reduce se aplica tanto al nuevo como al antiguo,
sobreescribiéndose el objeto con el resultado nuevo.
public static Vector<Factura> facturarMR(int mes, int year) throws Exception {

MongoCollection<BsonDocument> colScripts=MongoBroker.get().getDB().
getCollection(“scripts”, BsonDocument.class);
BsonDocument criterio=new BsonDocument(“_id”, new BsonString(“llamadasDeUnMes”));

String map=colScripts.find(criterio).first().get(“codigo”).asJavaScript().getCode();
criterio=new BsonDocument(“_id”, new BsonString(“facturacion”));
String reduce=colScripts.find(criterio).first().

get(“codigo”).asJavaScript().getCode();
MongoCollection<BsonDocument> colLlamadas=MongoBroker.get().getDB().
getCollection(“llamadas”, BsonDocument.class);

MapReduceIterable<BsonDocument> reduccion = colLlamadas.mapReduce(map, reduce);


reduccion=reduccion.collectionName(“facturacion”+year+“_”+mes);
BsonDocument scope=new BsonDocument();

scope.append(“year”, new BsonInt32(year));

scope.append(“mes”, new BsonInt32(mes-1));


reduccion=reduccion.scope(scope);

reduccion=reduccion.action(MapReduceAction.REPLACE);


reduccion.first();

Figura 183. Recuperación de scripts, preparación del mapReduce e invocación de la regla

Pero el proceso de facturación no termina aquí: tras ejecutar este MapReduce, en la


colección resultante tenemos solamente una colección como la de la Figura 181: es decir,
una serie de objetos con dos campos: el _id, que es el _id de cada cliente que ha llamado,
y el value, un objeto complejo con dos campos, el numeroDeLlamadas y la duracionTotal.
Ahora debemos procesar esta colección para ir facturando lo que corresponda a cada
www.fullengineeringbook.net
cliente.
Este segundo paso del proceso de facturación se muestra en la figura siguiente, que
completa así la función facturarMR de DAOFactura: se recupera en colFacturacion la
colección creada por la regla MapReduce que acaba de ejecutarse y, en el bucle while, se
itera sobre cada elemento de la colección: recuperamos el _id leyendo directamente el
campo _id de cada BsoDocument, y los campos numeroDeLlamadas y duracionTotal
mediante un acceso previo al campo value.
Nótese que todos los campos son leídos como dobles (asDouble) y no como
enteros, que es lo que se espera. Esto sucede porque el JavaScript ejecutado de las
funciones map y reduce deja los valores como reales, en lugar de cómo enteros.
Con cada BsonDocument leído de, por ejemplo, facturacion2015_10, se construye
un objeto de tipo Factura, que se inserta en la colección correspondiente. El constructor
de Factura requiere el idCliente, el numeroDeLlamadas, la duracionTotal (campos éstos
que recoge de facturacion2015_10), la tarifa (que lee dentro del bucle mediante la
llamada a DAOTarifa.loadTarifa(idCliente)), el mes y el year. Las facturas generadas,
además, se devuelven en el vector result.
public static Vector<Factura> facturarMR(int mes, int year) throws Exception {

MongoCollection<BsonDocument> colFacturacion=MongoBroker.get().getDB().
getCollection(“facturacion”+year+“_”+mes, BsonDocument.class);

MongoCursor<BsonDocument> facturas = colFacturacion.find().iterator();


BsonDocument bso;
int idCliente, numeroDeLlamadas, duracionTotal;

Tarifa tarifa;
Vector<Factura> result=new Vector<>();
while (facturas.hasNext()) {

bso=facturas.next();

idCliente=(int) bso.get(“_id”).asDouble().getValue();
numeroDeLlamadas=(int) bso.get(“value”).asDocument().

get(“numeroDeLlamadas”).asDouble().getValue();

duracionTotal=(int) bso.get(“value”).asDocument().
get(“duracionTotal”).asDouble().getValue();

tarifa=DAOTarifa.loadTarifa(idCliente);

Factura factura=new Factura(idCliente, numeroDeLlamadas,


duracionTotal, tarifa, mes, year);

factura.insert();

result.add(factura);
}

return result;

Figura 184. Código para la segunda parte del proceso de facturación

www.fullengineeringbook.net
De la inserción de las facturas se encarga el método insert de Factura que, a su
vez, llama al insert de DAOFactura. El código de esta clase es muy sencillo (Figura 185),
pues construye un BsonDocument a partir de la información contenida en la instancia que
recibe como parámetro.
Así como llamábamos materialización al hecho de construir una instancia de
nuestro dominio a partir de un BsonDocument, se llama desmaterialización a la
construcción de un BsonDocument a partir de un objeto de nuestro dominio. Son
desmaterializaciones de objetos lo que hacemos en la Figura 185.
public static void insert(Factura factura) {

MongoCollection<BsonDocument> colFacturas=MongoBroker.get().getDB().
getCollection(“facturas”, BsonDocument.class);

BsonDocument bso=new BsonDocument();

bso.append(“idCliente”, new BsonInt32(factura.getCliente().getId()));


bso.append(“dni”, new BsonString(factura.getCliente().getDNI()));

bso.append(“nombre”, new BsonString(factura.getCliente().getNombre()));


bso.append(“apellido1”, new BsonString(factura.getCliente().getApellido1()));
bso.append(“apellido2”, new BsonString(factura.getCliente().getApellido2()));

bso.append(“mes”, new BsonInt32(factura.getMes()));


bso.append(“year”, new BsonInt32(factura.getYear()));
bso.append(“cuotaFija”, new BsonDouble(factura.getCuotaFija()));

bso.append(“establecimientosDeLlamada”,
new BsonDouble(factura.getCostesEstablecimiento()));
bso.append(“importePorConsumo”, new BsonDouble(factura.getCostesPorConsumo()));

bso.append(“tipoDeTarifa”,
new BsonString(factura.getTarifa().getClass().getSimpleName()));
colFacturas.insertOne(bso);

Figura 185. Inserción de una factura

www.fullengineeringbook.net
5 EL MONGOBROKER
El MongoBroker es una clase singleton (es decir, sólo existe una única instancia de
ella) que representa el punto común de acceso a la base de datos: siempre que alguien
quiera acceder al servidor de Mongo, recuperará la conexión a través de esta clase. Su
implementación puede ser complicada pero, para nuestro caso, es tan sencilla como en la
Figura 186:
Al ser un singleton, dispone de un campo estático y privado (yo) del mismo
tipo que la propia clase.
Al ser un singleton, el constructor tiene visibilidad reducida: private, en
este caso.
La única instancia existente se recupera a través del método estático y
público get(): si el objeto yo es nulo (es decir, no existe todavía porque ésta
es la primera vez que se llama a este método), entonces se crea llamando al
constructor privado y se devuelve; si sí existía, se devuelve directamente.
El método getDB es el único método de negocio de la clase: cuando un
objeto cualquiera necesita acceder al servidor, recupera la referencia a la
MongoDatabase mediante una sentencia del tipo MongoDatabase
database=MongoBroker.get().getDB().
package edu.uclm.esi.mongo.isotel.persistencia;

import com.mongodb.MongoClient;

import com.mongodb.client.MongoDatabase;

public class MongoBroker {

private static MongoBroker yo;


private MongoDatabase db;

private MongoBroker() {
MongoClient mongoClient=new MongoClient(“localhost”, 27017);

this.db = mongoClient.getDatabase(“telefonosNoIncrustados”);

}
public static MongoBroker get() {

if (yo==null)

yo=new MongoBroker();
return yo;

public MongoDatabase getDB() {


return db;

Figura 186. Implementación del MongoBroker

www.fullengineeringbook.net
6 UTILIZACIÓN DE RÉPLICAS
En principio, la utilización de una base de datos replicada es transparente para el
cliente. Sin embargo, éste debe ser informado de la ubicación de los servidores que
conforman el conjunto de réplica.
En esta sección crearemos la base de datos telefonosNoIncrustados, con sus
diversas colecciones de clientes, llamadas y tarifas, en un conjunto de réplica creado en
los puertos 27017, 37017 y 47017 de la máquina 192.168.1.130.
Prácticamente todo el código de creación de la base de datos (Figura 157, Figura
158 y Figura 159) sigue siendo válido: el único cambio que tenemos que hacer se
encuentra en las primeras líneas: en lugar de conectar al servidor usando una sola
dirección( MongoClient mongoClient=new MongoClient(“localhost”, 27017); ), usamos varias (Figura 187). El resto
es exactamente igual.
public static void main(String[] args) throws IOException {
final int NUMERO_DE_CLIENTES = 1000;

List<ServerAddress> servidores=new Vector<ServerAddress>();


ServerAddress server1=new ServerAddress(“192.168.1.130”, 27017);
ServerAddress server2=new ServerAddress(“192.168.1.130”, 37017);

ServerAddress server3=new ServerAddress(“192.168.1.130”, 47017);


servidores.add(server1); servidores.add(server2); servidores.add(server3);

MongoClient mongoClient=new MongoClient(servidores);


MongoDatabase db = mongoClient.getDatabase(“telefonosNoIncrustados”);

Figura 187. Conexión a un conjunto de réplica

Respecto de la aplicación web, debemos modificar también la forma de conexión.


Como, afortunadamente, todas las conexiones se realizan a través del MongoBroker, basta
con que introduzcamos en su código el mismo cambio:
package edu.uclm.esi.mongo.isotel.persistencia;

import java.util.List;
import java.util.Vector;

import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;

import com.mongodb.client.MongoDatabase;


public class MongoBroker {

private static MongoBroker yo;

private MongoDatabase db;


private MongoBroker() {

List<ServerAddress> servidores=new Vector<ServerAddress>();

ServerAddress server1=new ServerAddress(“192.168.1.130”, 27017);

www.fullengineeringbook.net
ServerAddress server2=new ServerAddress(“192.168.1.130”, 37017);

ServerAddress server3=new ServerAddress(“192.168.1.130”, 47017);


servidores.add(server1); servidores.add(server2); servidores.add(server3);

MongoClient mongoClient=new MongoClient(servidores);


this.db = mongoClient.getDatabase(“telefonosNoIncrustados”);
}

public static MongoBroker get() {

if (yo==null)
yo=new MongoBroker();

return yo;

}
public MongoDatabase getDB() {

return db;

}
}

Figura 188. El MongoBroker, que ahora accede a un conjunto de réplica

www.fullengineeringbook.net
www.fullengineeringbook.net
Capítulo 9. Índice alfabético

_id, 21, 58, 78


_id (en mapReduce), 105
!=, 23
$in , 22, 30, 31
$ne, 23
$or, 21
action, 73, 104, 105
addToSet , 43
Adición de nodos a una réplica, 81
admin, 70
administración, 74
aggregate, 32, 33, 34, 36, 39, 40, 42, 45, 47, 95
Agregaciones, 1, 27
Almacenamiento de scripts, 62
AND, 23
arbiterOnly, 85
árbitro, 75, 76, 82, 85
Árbol sintáctico, 22
arquitectura de capas, 91
Arrays, 28, 52
Arrays en JavaScript, 28
avg , 43
base de datos sintética, 16
Broker, 106
BsonDateTime, 13
BsonDocument, 87, 88, 90, 96
org.bson.BsonDocument, 9
BSONObject, 7, 15
BsonValue, 12

www.fullengineeringbook.net
búsqueda de texto, 52
cadenas, 51
Caída de un nodo, 80
capped collection, 76
caracteres de escape, 5
changeUserPassword, 74
clases DAO, 91
clave, valor, 56
colecciones del sistema, 68
Collection, 24
comparación, 50
cond, 30
condicionales, 53
conf, 84, 85
configuración de escritura, 85
configuración de la réplica, 83
configuración de lectura, 85
conjuntos, 48
consistencia, 75
consistencia estricta, 75
contraseña, 74
count, 26, 27
Cursores, 24, 96
DAO, 91, 92, 93, 94, 97, 100, 101
dateToString, 37
db.auth, 70
de tuberías y filtros, 32
Delete, 97
diseño por capas, 91
disponibilidad, 75
distinct, 27
distinto, 23

www.fullengineeringbook.net
Document, 87
org.bson.Document, 8
dominio, 91
dominio (capa arquitectónica), 91
dropIndex, 65
elecciones, 80, 81
emit, 57, 103
ensureIndex, 64
escritura (configuración), 85
Fechas, 13, 29, 36, 37, 40, 53, 60, 89, 100, 104
filtros, 45
finalize, 55, 59, 102
find , 20, 25, 95, 101
FindIterable, 95
first , 43
getCollection, 88
getDatabase, 88
getIndexes, 65
grantPrivilegesToRole, 74
group, 27, 28, 29, 31, 33, 36, 38, 39, 42, 43, 54
hasNext, 26, 96, 97, 105
heartbeats, 76
host, 13, 78, 81, 82, 83, 84, 85
initial, 28
initiate, 78, 82, 83
java.util.Calendar, 13
JavaScript, 42, 104
JSON, 5
JSONObject, 7
JSONParser, 7
json-simple, 6
jsp, 91

www.fullengineeringbook.net
key, 9, 10, 28
last , 43
lectura (configuración), 85
length, 26, 28, 32, 36, 42, 71, 72, 103
limit, 26, 33, 41, 55, 94, 95, 96, 97
literales, 53
localhost, 18
majority, 86
map, 52, 55, 56, 57, 58, 60, 61, 102, 103, 104, 105
mapa, 56
MapReduce, 1, 27, 56, 57, 60, 102
MapReduceCommand, 102
match , 32, 33, 35, 42, 45, 46, 49
Materialización, 100
max , 43
Máximo, 43
Media, 43
members, 78, 84
MERGE, 104
min , 43
Mínimo, 43
mongo, 67
MongoBroker, 95, 97, 98, 100, 101, 105, 106, 107, 108
MongoCollection , 18, 88, 89, 90, 95, 96, 97, 98, 100, 101, 102, 105, 106
MongoCursor, 96
mongod, 67, 78, 81, 83
nearest, 77, 85
next, 26, 28, 96, 97, 105
nodo primario, 75, 76, 77, 79, 80, 81, 83, 85
nodo secundario, 75
ObjectId, 13
Operadores aritméticos, 50

www.fullengineeringbook.net
Operadores booleanos, 45
Operadores de cadena, 51
Operadores de comparación, 50
Operadores de totalización, 43
operadores lógicos, 24
oplog, 75, 76, 77
OR, 21, 31
org.bson.Document, 17
out, 55
pares (clave, valor), 56
password, 74
persistencia (capa arquitectónica), 91
ping, 76, 82
pipeline, 45, 50
Pipelines, 32
port, 78, 79, 82
presentación, 91, 92, 93
presentación (capa arqutiectónica), 91
primario, 75
primary, 77, 85
primaryPreferred, 77, 85
prioridad, 76
privileges, 72, 73
project , 32
prompt, 80
Proyecciones, 20
puerto, 79
push , 43
query, 58
read preferences, 77
reconfig, 83, 84
reduce, 28, 29, 55, 56, 57, 59, 60, 61, 102, 103, 104, 105

www.fullengineeringbook.net
REDUCE (action), 104
redundancia, 75
remove, 83
replace, 100, 104
replaceOne, 100
réplica oculta, 76
réplica postergada, 76
replica set, 82
réplicas
uso en la aplicación, 107
rol, 67, 68, 69, 70, 71, 72, 73, 74, 76
rollback, 77
root, 69
rs, 82
rs.add, 81
rs.conf, 84, 85
rs.initiate, 78
rs.reconfig, 83
rs.remove, 83
rs.status, 82
scripts, 62
secondary, 77, 85
secondaryPreferred, 77, 85
secundario, 75, 76, 77, 79, 80, 85
SELECT, 100
separación de las capas, 91
servidor
arranque, 67
servidor secundario, 75
singleton, 106
size, 18, 26, 45, 46, 52, 89
skip , 26, 33, 41, 90, 94, 95, 96, 97

www.fullengineeringbook.net
sort, 25, 26, 28, 33, 39, 55, 94, 95, 96, 97
status, 82
sum , 43
Suma, 43
súperusuario, 68
this, 7, 9, 12, 13, 57, 100, 103, 107, 108
timeout, 85
tipos de datos, 25
totalización, 43, 54
unique, 64
unwind , 33, 34, 35, 38
Update, 99
usuarios
creación, 69
modificación, 70, 74
value (en mapReduce), 105
verbose, 61
VirtualBox, 81
votación, 75, 76, 78, 80
votaciones, 76
w, 85
web, 87
write concern, 77
writeConcern, 85
wtimeout, 85

[1] Los ficheros pueden bajarse en texto plano de: http://www.inf-cr.uclm.es/www/mpolo/mongodb/


[2] 5% del espacio disponible en disco (mínimo 1 Gb, máximo 50 Gb) en Linux,
Solaris y Windows de 64 bits; 48 Mb en sistemas operativos de 32 bits; 183 Mb en OS X.

www.fullengineeringbook.net

You might also like