Capítulo 4. Consulta de

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Este capítulo de trata en detalle las consultas. Las principales áreas tratadas son las siguientes:

  • Puedes consultar rangos, inclusión de conjuntos, desigualdades y mucho más utilizando las condicionales de$.

  • Las consultas devuelven un cursor de base de datos, que devuelve perezosamente lotes de documentos a medida que los necesitas.

  • Hay muchas metaoperaciones que puedes realizar en un cursor, como omitir un determinado número de resultados, limitar el número de resultados devueltos y ordenar los resultados.

Introducción al hallazgo

El método find se utiliza para realizar consultas en MongoDB. La consulta devuelve un subconjunto de documentos de una colección, desde ningún documento hasta toda la colección. Los documentos que se devuelven vienen determinados por el primer argumento de find, que es un documento que especifica los criterios de consulta.

Un documento de consulta vacío (es decir, {}) coincide con todo lo que hay en la colección. Si a find no se le asigna un documento de consulta, por defecto será {}. Por ejemplo, lo siguiente:

> db.c.find()

coincide con todos los documentos de la colección c (y devuelve estos documentos por lotes).

Cuando empezamos a añadir pares clave/valor al documento de consulta, empezamos a restringir nuestra búsqueda. Esto funciona de forma sencilla para la mayoría de los tipos: los números coinciden con los números, los booleanos con los booleanos y las cadenas con las cadenas. Consultar un tipo sencillo es tan fácil como especificar el valor que buscas. Por ejemplo, para encontrar todos los documentos en los que el valor de "age" sea 27, podemos añadir ese par clave/valor al documento de consulta:

> db.users.find({"age" : 27})

Si tenemos una cadena que queremos que coincida, como una clave "username" con el valor "joe", utilizamos ese par clave/valor en su lugar:

> db.users.find({"username" : "joe"})

Múltiples condiciones pueden encadenarse añadiendo más pares clave/valor al documento de consulta, lo que se interpreta como "condition1 Y condition2 Y ... Y conditionN." Por ejemplo, para obtener todos los usuarios de 27 años con el nombre de usuario "joe", podemos consultar lo siguiente:

> db.users.find({"username" : "joe", "age" : 27})

Especificar qué teclas devolver

A veces no necesita que se devuelvan todos los pares clave/valor de un documento. Si es así, puedes pasar un segundo argumento a find (o findOne) especificando las claves que desees. Esto reduce tanto la cantidad de datos enviados por cable como el tiempo y la memoria utilizados para descodificar los documentos en el lado del cliente.

Por ejemplo, si tienes una colección de usuarios y sólo te interesan las claves "username" y "email", podrías devolver sólo esas claves con la siguiente consulta:

> db.users.find({}, {"username" : 1, "email" : 1})
{
    "_id" : ObjectId("4ba0f0dfd22aa494fd523620"),
    "username" : "joe",
    "email" : "joe@example.com"
}

Como puedes ver en la salida anterior, la clave "_id" se devuelve por defecto, aunque no se solicite específicamente.

También puedes utilizar este segundo parámetro para excluir determinados pares clave/valor de los resultados de una consulta. Por ejemplo, puedes tener documentos con varias claves, y lo único que sabes es que nunca quieres devolver la clave "fatal_weakness":

> db.users.find({}, {"fatal_weakness" : 0})

Esto también puede evitar que se devuelva "_id":

> db.users.find({}, {"username" : 1, "_id" : 0})
{
    "username" : "joe",
}

Limitaciones

En existen algunas restricciones para las consultas. El valor de un documento de consulta debe ser una constante en lo que respecta a la base de datos. (Es decir, no puede hacer referencia al valor de otra clave del documento. Por ejemplo, si lleváramos un inventario y tuviéramos las claves "in_stock"y "num_sold", no podríamos comparar sus valores consultando lo siguiente:

> db.stock.find({"in_stock" : "this.num_sold"}) // doesn't work

Hay formas de hacerlo (ver "Consultas $where"), pero normalmente obtendrás un mejor rendimiento reestructurando ligeramente tu documento, de forma que baste con una consulta "normal". En este ejemplo, podríamos utilizar en su lugar las claves "initial_stock" y "in_stock". Entonces, cada vez que alguien compre un artículo, disminuimos en uno el valor de la clave "in_stock". Por último, podemos hacer una consulta sencilla para comprobar qué artículos están agotados:

> db.stock.find({"in_stock" : 0})

Criterios de consulta

Las consultas pueden ir más allá de la coincidencia exacta descrita en el apartado anterior; pueden coincidir con criterios más complejos, como rangos, cláusulas OR y negación.

Condicionales de consulta

"$lt", "$lte", "$gt", y "$gte" son todos operadores de comparación , correspondientes a <, <=, > y >=, respectivamente. Se pueden combinar para buscar un rango de valores. Por ejemplo, para buscar usuarios que tengan entre 18 y 30 años, podemos hacer lo siguiente:

> db.users.find({"age" : {"$gte" : 18, "$lte" : 30}})

Así se encontrarían todos los documentos en los que el campo "age" fuera mayor o igual que 18 Y menor o igual que 30.

Este tipo de consultas de rango suelen ser útiles para las fechas. Por ejemplo, para encontrar personas que se inscribieron antes del 1 de enero de 2007, podemos hacer lo siguiente:

> start = new Date("01/01/2007")
> db.users.find({"registered" : {"$lt" : start}})

Dependiendo de cómo crees y almacenes las fechas, una coincidencia exacta puede ser menos útil, ya que las fechas se almacenan con una precisión de milisegundos. A menudo quieres un día, una semana o un mes entero, lo que hace necesaria una consulta de rango.

Para buscar documentos en los que el valor de una clave no sea igual a un determinado valor, debes utilizar otro operador condicional de, "$ne", que significa "no igual". Si quieres encontrar a todos los usuarios que no tengan el nombre de usuario "joe", puedes consultarlos utilizando esto

> db.users.find({"username" : {"$ne" : "joe"}})

"$ne" puede utilizarse con cualquier tipo.

Consultas OR

Existen dos formas de realizar una consulta OR en MongoDB. "$in" se puede utilizar para consultar una variedad de valores para una sola clave. "$or" es más general; se puede utilizar para consultar cualquiera de los valores dados a través de múltiples claves.

Si tienes más de un valor posible para una sola clave, utiliza una matriz de criterios con "$in". Por ejemplo, supongamos que estamos organizando un sorteo y los números del boleto ganador son 725, 542 y 390. Para encontrar estos tres documentos, podemos construir la siguiente consulta:

> db.raffle.find({"ticket_no" : {"$in" : [725, 542, 390]}})

"$in" es muy flexible y te permite especificar criterios de distintos tipos, así como valores. Por ejemplo, si estamos migrando gradualmente nuestro esquema para utilizar nombres de usuario en lugar de números de ID de usuario, podemos consultar cualquiera de los dos utilizando esto:

> db.users.find({"user_id" : {"$in" : [12345, "joe"]}})

Esto coincide con documentos con un "user_id" igual a 12345 y documentos con un "user_id" igual a "joe".

Si a "$in" se le da una matriz con un único valor, se comporta igual que si coincidiera directamente con el valor. Por ejemplo, {ticket_no : {$in : [725]}}coincide con los mismos documentos que {ticket_no : 725}.

El opuesto a "$in" es "$nin", que devuelve los documentos que no coinciden con ninguno de los criterios de la matriz. Si queremos devolver todas las personas que no ganaron nada en el sorteo, podemos consultarlas con esto:

> db.raffle.find({"ticket_no" : {"$nin" : [725, 542, 390]}})

Esta consulta devuelve a todos los que no tenían entradas con esos números.

"$in" te proporciona una consulta OR para una sola clave, pero ¿qué ocurre si necesitamos encontrar documentos en los que "ticket_no" sea 725 o "winner" sea true? Para este tipo de consulta, tendremos que utilizar la condicional "$or". "$or" toma una matriz de criterios posibles. En el caso del sorteo, utilizar "$or" quedaría así:

> db.raffle.find({"$or" : [{"ticket_no" : 725}, {"winner" : true}]})

"$or" puede contener otros condicionales. Si, por ejemplo, queremos que coincida cualquiera de los tres valores de "ticket_no" o la clave "winner", podemos utilizar esto:

> db.raffle.find({"$or" : [{"ticket_no" : {"$in" : [725, 542, 390]}},
...                        {"winner" : true}]})

Con una consulta normal de tipo AND, quieres reducir al máximo los resultados con el menor número de argumentos posible. Las consultas de tipo OR son lo contrario: son más eficaces si los primeros argumentos coinciden con el mayor número posible de documentos.

Aunque "$or" siempre funcionará, utiliza "$in" siempre que sea posible, ya que el optimizador de consultas lo gestiona de forma más eficiente.

$no

"$not" es un metacondicional: puede aplicarse sobre cualquier otro criterio. Como ejemplo, consideremos el operador de módulo, "$mod". "$mod" busca claves cuyos valores, al dividirlos por el primer valor dado, tengan un resto del segundo valor:

> db.users.find({"id_num" : {"$mod" : [5, 1]}})

La consulta anterior devuelve usuarios con "id_num"s de 1, 6, 11, 16, etc. Si queremos, en cambio, devolver usuarios con "id_num"s de 2, 3, 4, 5, 7, 8, 9, 10, 12, etc., podemos utilizar "$not":

> db.users.find({"id_num" : {"$not" : {"$mod" : [5, 1]}}})

"$not" puede ser especialmente útil junto con expresiones regulares para encontrar todos los documentos que no coincidan con un patrón determinado (el uso de expresiones regulares se describe en, en la sección "Expresiones regulares").

Consultas específicas de tipo

Como se explica en el Capítulo 2, MongoDB tiene una amplia variedad de tipos que pueden utilizarse en un documento. Algunos de estos tipos tienen un comportamiento especial al realizar consultas.

null

null se comporta de forma un poco extraña en. Sí se hace coincidir, de modo que si tenemos una colección con los siguientes documentos

> db.c.find()
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

podemos buscar documentos cuya clave "y" sea null de la forma esperada:

> db.c.find({"y" : null})
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }

Sin embargo, null también coincide con "no existe". Así, la consulta de una clave con el valor null devolverá todos los documentos que carezcan de esa clave:

> db.c.find({"z" : null})
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

Si sólo queremos encontrar claves cuyo valor sea null, podemos comprobar que la clave es null y existe utilizando la condicional "$exists":

> db.c.find({"z" : {"$eq" : null, "$exists" : true}})

Expresiones regulares

"$regex" proporciona funciones de expresiones regulares en para hacer coincidir patrones de cadenas en las consultas. Las expresiones regulares son útiles para hacer coincidir cadenas de forma flexible. Por ejemplo, si queremos encontrar a todos los usuarios con el nombre "Joe" o "joe", podemos utilizar una expresión regular para hacer coincidencias sin distinguir entre mayúsculas y minúsculas:

> db.users.find( {"name" : {"$regex" : /joe/i } })

Los indicadores de expresión regular (por ejemplo, i) están permitidos pero no son obligatorios. Si queremos que coincidan no sólo varias mayúsculas de "joe", sino también "joey", podemos seguir mejorando nuestra expresión regular:

> db.users.find({"name" : /joey?/i})

MongoDB utiliza la biblioteca Perl Compatible Regular Expression (PCRE) para hacer coincidir expresiones regulares; cualquier sintaxis de expresión regular permitida por PCRE está permitida en MongoDB. Es una buena idea comprobar la sintaxis con el intérprete de comandos JavaScript antes de utilizarla en una consulta, para asegurarte de que coincide con lo que crees que coincide.

Nota

MongoDB puede aprovechar un índice para realizar consultas sobre expresiones regulares prefijadas (por ejemplo, /^joey/). Los índices no pueden utilizarse para búsquedas sin distinción entre mayúsculas y minúsculas (/^joey/i). Una expresión regular es una "expresión prefija" cuando empieza por un signo de intercalación (^) o por un ancla izquierda (\A). Si la expresión regular utiliza una consulta que distingue entre mayúsculas y minúsculas, si existe un índice para el campo, las coincidencias pueden realizarse con los valores del índice. Si además es una expresión prefija, entonces la búsqueda puede limitarse a los valores dentro del rango creado por ese prefijo a partir del índice.

Las expresiones regulares también pueden coincidir consigo mismas. Muy poca gente inserta expresiones regulares en la base de datos, pero si insertas una, puedes hacerla coincidir consigo misma:

> db.foo.insertOne({"bar" : /baz/})
> db.foo.find({"bar" : /baz/})
{
    "_id" : ObjectId("4b23c3ca7525f35f94b60a2d"),
    "bar" : /baz/
}

Consulta de matrices

La consulta a de los elementos de un array está diseñada para comportarse como lo hace la consulta de escalares. Por ejemplo, si la matriz es una lista de frutas, como ésta:

> db.food.insertOne({"fruit" : ["apple", "banana", "peach"]})

la siguiente consulta coincidirá correctamente con el documento:

> db.food.find({"fruit" : "banana"})

Podemos consultarlo del mismo modo que si tuviéramos un documento parecido al documento (ilegal) {"fruit" : "apple", "fruit" : "banana", "fruit" : "peach"}.

"$todos"

Si necesitas hacer coincidir matrices por más de un elemento, puedes utilizar "$all". Esto te permite hacer coincidir una lista de elementos. Por ejemplo, supongamos que creamos una colección con tres elementos:

> db.food.insertOne({"_id" : 1, "fruit" : ["apple", "banana", "peach"]})
> db.food.insertOne({"_id" : 2, "fruit" : ["apple", "kumquat", "orange"]})
> db.food.insertOne({"_id" : 3, "fruit" : ["cherry", "banana", "apple"]})

A continuación, podemos encontrar todos los documentos con los elementos "apple" y "banana" consultando con "$all":

> db.food.find({fruit : {$all : ["apple", "banana"]}})
{"_id" : 1, "fruit" : ["apple", "banana", "peach"]}
{"_id" : 3, "fruit" : ["cherry", "banana", "apple"]}

El orden no importa. Observa que "banana" está antes que "apple" en el segundo resultado. Utilizar una matriz de un elemento con "$all" equivale a no utilizar "$all". Por ejemplo, {fruit : {$all : ['apple']} coincidirá con los mismos documentos que {fruit : 'apple'}.

También puedes consultar por coincidencia exacta utilizando toda la matriz. Sin embargo, la coincidencia exacta no coincidirá con un documento si falta algún elemento o es superfluo. Por ejemplo, esto coincidirá con el primero de nuestros tres documentos:

> db.food.find({"fruit" : ["apple", "banana", "peach"]})

Pero esto no:

> db.food.find({"fruit" : ["apple", "banana"]})

y esto tampoco:

> db.food.find({"fruit" : ["banana", "apple", "peach"]})

Si quieres consultar un elemento concreto de una matriz, puedes especificar un índice utilizando la sintaxis key.index:

> db.food.find({"fruit.2" : "peach"})

Las matrices siempre tienen índice 0, por lo que coincidiría el tercer elemento de la matriz con la cadena "peach".

"$tamaño"

Una condicional útil para consultar matrices es "$size", que te permite consultar matrices de un tamaño determinado. Aquí tienes un ejemplo:

> db.food.find({"fruit" : {"$size" : 3}})

Una consulta habitual es obtener un rango de tamaños. "$size" no puede combinarse con otra condicional $ (en este ejemplo, "$gt"), pero esta consulta puede realizarse añadiendo una clave "size" al documento. Luego, cada vez que añadas un elemento a la matriz, incrementa el valor de "size". Si la actualización original tenía este aspecto:

> db.food.update(criteria, {"$push" : {"fruit" : "strawberry"}})

se puede cambiar simplemente por esto:

> db.food.update(criteria,
... {"$push" : {"fruit" : "strawberry"}, "$inc" : {"size" : 1}})

El incremento es extremadamente rápido, por lo que cualquier penalización de rendimiento es insignificante. Almacenar así los documentos te permite hacer consultas como ésta:

> db.food.find({"size" : {"$gt" : 3}})

Por desgracia, esta técnica no funciona tan bien con el operador "$addToSet".

"$corte"

Como ya se ha mencionado anteriormente en este capítulo, el segundo argumento opcional de find especifica las claves que se van a devolver. El operador especial "$slice" puede utilizarse para devolver un subconjunto de elementos de una clave de matriz.

Por ejemplo, supongamos que tenemos un documento de una entrada de blog y queremos devolver los 10 primeros comentarios:

> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : 10}})

Alternativamente, si quisiéramos los 10 últimos comentarios, podríamos utilizar −10:

> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -10}})

"$slice" también puede devolver páginas en medio de los resultados tomando un desplazamiento y el número de elementos a devolver:

> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : [23, 10]}})

Esto omitiría los 23 primeros elementos y devolvería del 24º al 33º. Si hubiera menos de 33 elementos en la matriz, devolvería tantos como fuera posible.

A menos que se especifique lo contrario, se devuelven todas las claves de un documento cuando se utiliza "$slice". Esto no ocurre con los demás especificadores de clave, que suprimen la devolución de las claves no mencionadas. Por ejemplo, si tuviéramos un documento de entrada de blog con este aspecto

{
    "_id" : ObjectId("4b2d75476cc613d5ee930164"),
    "title" : "A blog post",
    "content" : "...",
    "comments" : [
        {
            "name" : "joe",
            "email" : "joe@example.com",
            "content" : "nice post."
        },
        {
            "name" : "bob",
            "email" : "bob@example.com",
            "content" : "good post."
        }
    ]
}

e hiciéramos un "$slice" para obtener el último comentario, obtendríamos esto:

> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -1}})
{
    "_id" : ObjectId("4b2d75476cc613d5ee930164"),
    "title" : "A blog post",
    "content" : "...",
    "comments" : [
        {
            "name" : "bob",
            "email" : "bob@example.com",
            "content" : "good post."
        }
    ]
}

Se siguen devolviendo "title" y "content", aunque no se hayan incluido explícitamente en el especificador de clave.

Devuelve un elemento de matriz coincidente

"$slice" es útil cuando conoces el índice del elemento, pero a veces quieres cualquier elemento de la matriz que coincida con tus criterios. Puedes devolver el elemento coincidente con el operador $. Dado el ejemplo del blog anterior, podrías obtener el comentario de Bob con:

> db.blog.posts.find({"comments.name" : "bob"}, {"comments.$" : 1})
{
    "_id" : ObjectId("4b2d75476cc613d5ee930164"),
    "comments" : [
        {
            "name" : "bob",
            "email" : "bob@example.com",
            "content" : "good post."
        }
    ]
}

Ten en cuenta que esto sólo devuelve la primera coincidencia de cada documento: si Bob hubiera dejado varios comentarios en este post, sólo se devolvería el primero de la matriz "comments".

Interacciones de consulta de matrices y rangos

Los escalares (elementos que no son matrices) de los documentos deben coincidir con cada cláusula de los criterios de una consulta. Por ejemplo, si buscas {"x" : {"$gt" : 10, "$lt" : 20}}, "x" tendría que ser a la vez mayor que 10 y menor que 20. Sin embargo, si el campo "x" de un documento es una matriz, el documento coincide si hay un elemento de "x" que coincida con cada parte de los criterios , pero cada cláusula de la consulta puede coincidir con un elemento de matriz diferente.

La mejor forma de entender este comportamiento es ver un ejemplo. Supongamos que tenemos los siguientes documentos:

{"x" : 5}
{"x" : 15}
{"x" : 25}
{"x" : [5, 25]}

Si quisiéramos encontrar todos los documentos en los que "x" está entre 10 y 20, podríamos estructurar ingenuamente una consulta como db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}) y esperar que nos devolviera un documento: {"x" : 15}. Sin embargo, ejecutando esto, obtenemos dos:

> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}})
{"x" : 15}
{"x" : [5, 25]}

Ni 5 ni 25 están entre 10 y 20, pero se devuelve el documento porque 25 coincide con la primera cláusula (es mayor que 10) y 5 coincide con la segunda cláusula (es menor que 20).

Este hace que las consultas de rango contra matrices sean esencialmente inútiles: un rango coincidirá con cualquier matriz de varios elementos. Hay un par de formas de obtener el comportamiento esperado.

En primer lugar, puedes utilizar "$elemMatch" para obligar a MongoDB a comparar ambas cláusulas con un único elemento de matriz. Sin embargo, el inconveniente es que "$elemMatch" no comparará elementos que no sean matrices:

> db.test.find({"x" : {"$elemMatch" : {"$gt" : 10, "$lt" : 20}}})
> // no results

El documento {"x" : 15} ya no coincide con la consulta, porque el campo "x" no es un array. Dicho esto, debes tener una buena razón para mezclar valores de matriz y escalares en un campo. Muchos casos de uso no requieren la mezcla. Para ellos, "$elemMatch" ofrece una buena solución para consultas de rango sobre elementos de matrices.

Si tienes un índice sobre el campo que estás consultando (véase el capítulo 5), puedes utilizar min y max para limitar el rango del índice recorrido por la consulta a sus valores "$gt" y "$lt":

> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}).min({"x" : 10}).max({"x" : 20})
{"x" : 15}

Ahora esto sólo recorrerá el índice de 10 a 20, omitiendo las entradas 5 y 25. Sin embargo, sólo puedes utilizar min y max cuando tengas un índice sobre el campo que estás consultando, y debes pasar todos los campos del índice a min y max.

Utilizar min y max al consultar rangos sobre documentos que pueden incluir matrices suele ser una buena idea. Los límites del índice para una consulta "$gt"/"$lt" sobre una matriz son ineficaces. Básicamente acepta cualquier valor, por lo que buscará en todas las entradas del índice, no sólo en las del rango.

Consulta de documentos incrustados

En hay dos formas de consultar un documento incrustado: consultar el documento completo o consultar sus pares clave/valor individuales.

La consulta de un documento incrustado completo funciona de forma idéntica a una consulta normal. Por ejemplo, si tenemos un documento con el siguiente aspecto

{
    "name" : {
        "first" : "Joe",
        "last" : "Schmoe"
    },
    "age" : 45
}

podemos buscar a alguien llamado Joe Schmoe con lo siguiente:

> db.people.find({"name" : {"first" : "Joe", "last" : "Schmoe"}})

Sin embargo, una consulta para un subdocumento completo debe coincidir exactamente con el subdocumento. Si Joe decide añadir un campo de segundo nombre, de repente esta consulta ya no funcionará: ¡no coincide con todo el documento incrustado! Este tipo de consulta también es sensible al orden: {"last" : "Schmoe", "first" : "Joe"} no sería una coincidencia.

Si es posible, suele ser una buena idea consultar sólo una clave o claves concretas de un documento incrustado. Así, si tu esquema cambia, todas tus consultas no se romperán de repente porque ya no coincidan exactamente. En puedes consultar las claves incrustadas utilizando la notación por puntos:

> db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"})

Ahora, si Joe añade más claves, esta consulta seguirá coincidiendo con su nombre y apellidos.

Esta notación de puntos es la principal diferencia entre los documentos de consulta y otros tipos de documentos. Los documentos de consulta pueden contener puntos, que significan "llegar a un documento incrustado". La notación por puntos es también la razón por la que los documentos que se insertan no pueden contener el carácter .. A menudo, la gente de se encuentra con esta limitación cuando intenta guardar URLs como claves. Una forma de evitarlo es realizar siempre un reemplazo global antes de insertar o después de recuperar, sustituyendo el carácter punto por un carácter que no sea legal en las URL.

Las coincidencias de documentos incrustados pueden resultar un poco complicadas a medida que la estructura del documento se complica. Por ejemplo, supongamos que estamos almacenando entradas de un blog y queremos encontrar comentarios de Joe que hayan obtenido al menos un 5. Podríamos modelar la entrada de la siguiente manera:

> db.blog.find()
{
    "content" : "...",
    "comments" : [
        {
            "author" : "joe",
            "score" : 3,
            "comment" : "nice post"
        },
        {
            "author" : "mary",
            "score" : 6,
            "comment" : "terrible post"
        }
    ]
}

Ahora, no podemos consultar utilizando db.blog.find({"comments" : {"author" : "joe", "score" : {"$gte" : 5}}}). Las coincidencias de documentos incrustados tienen que coincidir con todo el documento, y esto no coincide con la clave "comment". Tampoco funcionaría hacer db.blog.find({"comments.author" : "joe", "comments.score" : {"$gte" : 5}}), porque el criterio de autor podría coincidir con un comentario distinto del criterio de puntuación. Es decir, devolvería el documento mostrado arriba: coincidiría con "author" : "joe" en el primer comentario y con "score" : 6 en el segundo comentario.

Para agrupar correctamente los criterios sin necesidad de especificar cada clave, utiliza "$elemMatch". Este condicional de nombre impreciso te permite especificar parcialmente los criterios para que coincidan con un único documento incrustado en una matriz. La consulta correcta tiene este aspecto

> db.blog.find({"comments" : {"$elemMatch" : 
... {"author" : "joe", "score" : {"$gte" : 5}}}})

"$elemMatch" te permite "agrupar" tus criterios. Como tal, sólo se necesita cuando tienes más de una clave con la que quieres coincidir en un documento incrustado.

Consultas $where

Los pares clave/valor son una forma bastante expresiva de realizar consultas, pero hay algunas consultas que no pueden representar. Para las consultas que no se pueden hacer de otra forma, existen las cláusulas "$where", que te permiten ejecutar JavaScript arbitrario como parte de tu consulta. Esto te permite hacer (casi) cualquier cosa dentro de una consulta. Por motivos de seguridad, el uso de las cláusulas "$where" debe restringirse mucho o eliminarse. Nunca debe permitirse a los usuarios finales ejecutar cláusulas "$where"arbitrarias.

El caso más habitual para utilizar "$where" es comparar los valores de dos claves de un documento. Por ejemplo, supongamos que tenemos documentos con el siguiente aspecto:

> db.foo.insertOne({"apple" : 1, "banana" : 6, "peach" : 3})
> db.foo.insertOne({"apple" : 8, "spinach" : 4, "watermelon" : 4})

Por ejemplo, en el segundo documento, "spinach" y "watermelon" tienen el mismo valor, por lo que queremos que se devuelva ese documento. Es poco probable que MongoDB tenga alguna vez una condicional $ para esto, así que podemos utilizar una cláusula "$where" para hacerlo con JavaScript:

> db.foo.find({"$where" : function () {
... for (var current in this) {
...     for (var other in this) {
...         if (current != other && this[current] == this[other]) {
...             return true;
...         }
...     }
... }
... return false;
... }});

Si la función devuelve true, el documento formará parte del conjunto de resultados; si devuelve false, no.

"$where" no deben utilizarse a menos que sea estrictamente necesario: son mucho más lentas que las consultas normales. Hay que convertir cada documento de BSON a un objeto JavaScript y luego pasarlo por la expresión "$where". Tampoco pueden utilizarse índices para satisfacer una "$where". Por tanto, sólo debes utilizar "$where" cuando no haya otra forma de realizar la consulta. Puedes reducir la penalización utilizando otros filtros de consulta en combinación con "$where". Si es posible, se utilizará un índice para filtrar en función de las cláusulas no$where; la expresión "$where" sólo se utilizará para afinar los resultados. MongoDB 3.6 añadió el operador $expr, que permite utilizar expresiones de agregación con el lenguaje de consulta de MongoDB. Es más rápido que $where, ya que no ejecuta JavaScript, y se recomienda como sustituto de este operador siempre que sea posible.

Otra forma de realizar consultas complejas es utilizar una de las herramientas de agregación, que se tratan en el Capítulo 7.

Cursores

La base de datos devuelve los resultados de find utilizando un cursor. Las implementaciones del lado del cliente de los cursores suelen permitirte controlar mucho la salida final de una consulta. Puedes limitar el número de resultados, saltarte algún número de resultados, ordenar los resultados por cualquier combinación de claves en cualquier dirección, y realizar otra serie de potentes operaciones.

Para crea un cursor con el intérprete de comandos, pon algunos documentos en una colección, haz una consulta sobre ellos y asigna los resultados a una variable local (las variables definidas con "var" son locales). Aquí, creamos una colección muy simple y la consultamos, almacenando los resultados en la variable cursor:

> for(i=0; i<100; i++) {
...     db.collection.insertOne({x : i});
... }
> var cursor = db.collection.find();

La ventaja de hacer esto es que puedes ver un resultado cada vez. Si almacenas los resultados en una variable global o no almacenas ninguna variable, el intérprete de comandos de MongoDB iterará automáticamente y mostrará el primer par de documentos. Esto es lo que hemos estado viendo hasta ahora, y a menudo es el comportamiento que quieres para ver lo que hay en una colección, pero no para programar con el intérprete de comandos.

Para iterar por los resultados, puedes utilizar el método next en el cursor. Puedes utilizar hasNext para comprobar si hay otro resultado. Un típico bucle a través del resultado tiene el siguiente aspecto:

> while (cursor.hasNext()) {
...     obj = cursor.next();
...     // do stuff
... }

cursor.hasNext() comprueba que existe el siguiente resultado, y cursor.next()lo obtiene.

La clase cursor también implementa la interfaz de iteradores de JavaScript, por lo que puedes utilizarla en un bucle forEach:

> var cursor = db.people.find();
> cursor.forEach(function(x) {
...     print(x.name);
... });
adam
matt
zak

Cuando llamas a find, el shell no consulta la base de datos inmediatamente. Espera a que empieces a solicitar resultados para enviar la consulta, lo que te permite encadenar opciones adicionales a una consulta antes de que se realice. Casi todos los métodos de un objeto cursor devuelven el propio cursor, por lo que puedes encadenar opciones en cualquier orden. Por ejemplo, todos los siguientes son equivalentes:

> var cursor = db.foo.find().sort({"x" : 1}).limit(1).skip(10);
> var cursor = db.foo.find().limit(1).sort({"x" : 1}).skip(10);
> var cursor = db.foo.find().skip(10).limit(1).sort({"x" : 1});

En este punto, la consulta aún no se ha ejecutado. Todas estas funciones se limitan a construir la consulta. Ahora, supongamos que llamamos a

> cursor.hasNext()

En este punto, la consulta se enviará al servidor. El shell obtiene los primeros 100 resultados o los primeros 4 MB de resultados (lo que sea menor) de una vez, de modo que las siguientes llamadas a next o hasNext no tengan que hacer viajes al servidor. Cuando el cliente haya agotado el primer conjunto de resultados, el shell volverá a ponerse en contacto con la base de datos y pedirá más resultados con una petición a getMore. Las peticiones a getMore contienen básicamente un identificador para el cursor y preguntan a la base de datos si hay más resultados, devolviendo el siguiente lote en caso afirmativo. Este proceso continúa hasta que se agota el cursor y se devuelven todos los resultados.

Límites, saltos y clasificaciones

Las opciones de consulta más comunes son limitar el número de resultados devueltos, omitir una serie de resultados y ordenar. Todas estas opciones deben añadirse antes de enviar una consulta a la base de datos.

Para establecer un límite, encadena la función limit a tu llamada a find. Por ejemplo, para devolver sólo tres resultados, utiliza esto:

> db.c.find().limit(3)

Si hay menos de tres documentos que coincidan con tu consulta en la colección, sólo se devolverá el número de documentos coincidentes; limit establece un límite superior, no un límite inferior.

skip funciona de forma similar a limit:

> db.c.find().skip(3)

Esto omitirá los tres primeros documentos coincidentes y devolverá el resto de coincidencias. Si hay menos de tres documentos en tu colección, no devolverá ningún documento.

sort toma un objeto: un conjunto de pares clave/valor donde las claves son los nombres de las claves y los valores son las direcciones de ordenación. La dirección de ordenación puede ser 1 (ascendente) o −1 (descendente). Si se dan varias claves, los resultados se ordenarán en ese orden. Por ejemplo, para ordenar los resultados por "username" ascendente y "age" descendente, hacemos lo siguiente:

> db.c.find().sort({username : 1, age : -1})

Estos tres métodos pueden combinarse. Esto suele ser útil para la paginación. Por ejemplo, supongamos que tienes una tienda online y alguien busca mp3. Si quieres 50 resultados por página ordenados por precio de mayor a menor, puedes hacer lo siguiente:

> db.stock.find({"desc" : "mp3"}).limit(50).sort({"price" : -1})

Si esa persona hace clic en Página siguiente para ver más resultados, puedes añadir simplemente un salto a la consulta, que saltará las 50 primeras coincidencias (que el usuario ya vio en la página 1):

> db.stock.find({"desc" : "mp3"}).limit(50).skip(50).sort({"price" : -1})

Sin embargo, los saltos grandes no son muy eficaces; en la siguiente sección se sugieren formas de evitarlos.

Orden de comparación

MongoDB tiene una jerarquía en cuanto a la comparación de tipos. A veces tendrás una única clave con varios tipos: por ejemplo, enteros y booleanos, o cadenas y nulos. Si realizas una ordenación en una clave con una mezcla de tipos, existe un orden predefinido en el que se ordenarán. De menor a mayor valor, este orden es el siguiente:

  1. Valor mínimo

  2. Nulo

  3. Números (enteros, largos, dobles, decimales)

  4. Cuerdas

  5. Objeto/documento

  6. Matriz

  7. Datos binarios

  8. ID del objeto

  9. Booleano

  10. Fecha

  11. Marca de tiempo

  12. Expresión regular

  13. Valor máximo

Evitar grandes saltos

Utilizar skip para un pequeño número de documentos está bien. Pero para un gran número de resultados, skip puede ser lento, ya que tiene que encontrar y luego descartar todos los resultados omitidos. La mayoría de las bases de datos guardan más metadatos en el índice para ayudar con las omisiones, pero MongoDB aún no lo admite, por lo que deben evitarse las omisiones grandes. A menudo puedes calcular los resultados de la siguiente consulta basándote en la anterior.

Paginar resultados sin salto

La forma más sencilla de hacer la paginación es devolver la primera página de resultados utilizando limit y luego devolver cada página posterior como un desplazamiento desde el principio:

> // do not use: slow for large skips
> var page1 = db.foo.find(criteria).limit(100)
> var page2 = db.foo.find(criteria).skip(100).limit(100)
> var page3 = db.foo.find(criteria).skip(200).limit(100)
...

Sin embargo, dependiendo de tu consulta, normalmente puedes encontrar una forma de paginar sin saltos. Por ejemplo, supongamos que queremos mostrar los documentos en orden descendente basándonos en "date". Podemos obtener la primera página de resultados con lo siguiente:

> var page1 = db.foo.find().sort({"date" : -1}).limit(100)

Entonces, suponiendo que la fecha sea única, podemos utilizar el valor "date" del último documento como criterio para obtener la página siguiente:

var latest = null;

// display first page
while (page1.hasNext()) {
   latest = page1.next();
   display(latest);
}

// get next page
var page2 = db.foo.find({"date" : {"$lt" : latest.date}});
page2.sort({"date" : -1}).limit(100);

Ahora la consulta no necesita incluir un salto.

Encontrar un documento al azar

Un problema bastante común es cómo obtener un documento aleatorio de una colección. La solución ingenua (y lenta) es contar el número de documentos y luego hacer un find, saltando un número aleatorio de documentos entre cero y el tamaño de la colección:

> // do not use
> var total = db.foo.count()
> var random = Math.floor(Math.random()*total)
> db.foo.find().skip(random).limit(1)

En realidad, es muy ineficaz obtener un elemento aleatorio de esta forma: tienes que hacer un recuento (que puede ser caro si utilizas criterios), y omitir un gran número de elementos puede llevar mucho tiempo.

Requiere un poco de previsión, pero si sabes que vas a buscar un elemento aleatorio en una colección, hay una forma mucho más eficaz de hacerlo. El truco consiste en añadir una clave aleatoria adicional a cada documento cuando se inserta. Por ejemplo, si estamos utilizando el shell, podríamos utilizar la función Math.random() (que crea un número aleatorio entre 0 y 1):

> db.people.insertOne({"name" : "joe", "random" : Math.random()})
> db.people.insertOne({"name" : "john", "random" : Math.random()})
> db.people.insertOne({"name" : "jim", "random" : Math.random()})

Ahora, cuando queramos encontrar un documento aleatorio de la colección, podemos calcular un número aleatorio y utilizarlo como criterio de consulta, en lugar de utilizar skip:

> var random = Math.random()
> result = db.people.findOne({"random" : {"$gt" : random}})

Existe una pequeña posibilidad de que random sea mayor que cualquiera de los valores "random"de la colección, y no se devuelva ningún resultado. Podemos evitarlo simplemente devolviendo un documento en la otra dirección:

> if (result == null) {
...     result = db.people.findOne({"random" : {"$lte" : random}})
... }

Si no hay ningún documento en la colección, esta técnica acabará devolviendo null, lo cual tiene sentido.

Esta técnica puede utilizarse con consultas arbitrariamente complejas; sólo tienes que asegurarte de tener un índice que incluya la clave aleatoria. Por ejemplo, si queremos encontrar un fontanero aleatorio en California, podemos crear un índice sobre "profession", "state" y "random":

> db.people.ensureIndex({"profession" : 1, "state" : 1, "random" : 1})

Esto nos permite encontrar rápidamente un resultado aleatorio (para más información sobre la indexación, consulta el capítulo 5 ).

Cursores inmortales

En hay dos lados de un cursor: el cursor del lado del cliente y el cursor de la base de datos que representa el del lado del cliente. Hasta ahora hemos estado hablando del del lado del cliente, pero vamos a echar un breve vistazo a lo que ocurre en el servidor.

En el lado del servidor, un cursor ocupa memoria y recursos. Una vez que un cursor se queda sin resultados o el cliente envía un mensaje indicándole que muera, la base de datos puede liberar los recursos que estaba utilizando. Liberar estos recursos permite a la base de datos utilizarlos para otras cosas, lo cual es bueno, por lo que queremos asegurarnos de que los cursores puedan liberarse rápidamente (dentro de lo razonable).

Hay un par de condiciones que pueden provocar la muerte (y posterior limpieza) de un cursor. En primer lugar, cuando un cursor termina de iterar por los resultados coincidentes, se limpia a sí mismo. Otra forma es que, cuando un cursor sale del ámbito en el lado del cliente, los controladores envían a la base de datos un mensaje especial para hacerle saber que puede matar ese cursor. Por último, aunque el usuario no haya iterado por todos los resultados y el cursor siga en el ámbito, tras 10 minutos de inactividad, un cursor de la base de datos "morirá" automáticamente. De esta forma, si un cliente se bloquea o tiene un error, MongoDB no se quedará con miles de cursores abiertos.

Esta "muerte por tiempo de espera" suele ser el comportamiento deseado: muy pocas aplicaciones esperan que sus usuarios permanezcan sentados durante minutos esperando resultados. Sin embargo, a veces puedes saber que necesitas que un cursor dure mucho tiempo. En ese caso, muchos controladores han implementado una función llamada immortal, o un mecanismo similar, que indica a la base de datos que no temporice el cursor. Si desactivas el tiempo de espera de un cursor, debes iterar por todos sus resultados o matarlo para asegurarte de que se cierra. De lo contrario, permanecerá en la base de datos acaparando recursos hasta que se reinicie el servidor.

Get MongoDB: La Guía Definitiva, 3ª Edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.