Una de las preguntas más frecuentes que recibo cuando se trata de NoSQL es sobre el tema de unir datos de múltiples documentos en un único resultado de consulta. Aunque esta pregunta la plantean con más frecuencia los desarrolladores de RDBMS, también la recibo de desarrolladores de NoSQL.
Cuando se trata de unir datos, cada base de datos lo hace de forma diferente, algunas de las cuales requieren que se haga a través de la capa de aplicación, en lugar de la capa de base de datos. Vamos a explorar algunas opciones de unión de datos entre tecnologías de bases de datos.
En este blog, compararemos el proceso de unir documentos NoSQL utilizando el operador $lookup de MongoDB frente a CouchbaseEl lenguaje de consulta N1QL es más intuitivo.
Datos de la muestra
Para este ejemplo, nos basaremos tanto en MongoDB como en Couchbase en dos documentos de ejemplo. Supongamos que estamos trabajando con un ejemplo clásico de pedido e inventario. Para el inventario, nuestros documentos podrían ser algo como esto:
1 2 3 4 5 6 |
{ "id": "producto-1", "tipo": "producto", "Nombre": "Pokemon Rojo", "precio": 29.99 } |
Si bien es plano, el documento anterior puede explicar adecuadamente un producto en particular. Tiene un identificador único que intervendrá durante el proceso de unión. Para los pedidos, podríamos tener un documento con el siguiente aspecto:
1 2 3 4 5 6 7 8 9 10 |
{ "id": "order-1", "tipo": "orden", "productos": [ { "product_id": "producto-1", "cantidad": 2 } ] } |
El objetivo aquí será unir estos dos documentos en una sola consulta utilizando tanto MongoDB como Couchbase. Sin embargo, dejando a un lado el lenguaje de consulta, estos documentos siempre se pueden unir a través de la capa de aplicación a través de múltiples consultas. Sin embargo, este no es el resultado que buscamos.
Unir documentos con MongoDB y el operador $lookup
En versiones recientes de MongoDB, las consultas join implican un 1TP4Búsqueda
que forma parte de las consultas de agregación. Según el manual de MongoDB documentación, este operador se comporta como sigue:
Realiza una unión exterior izquierda con una colección no separada de la misma base de datos para filtrar los documentos de la colección "unida" para su procesamiento. La etapa $lookup realiza una comparación de igualdad entre un campo de los documentos de entrada y un campo de los documentos de la colección "unida".
Para utilizar el 1TP4Búsqueda
operador, tendrías algo así:
1 2 3 4 5 6 7 8 9 10 11 |
db.collection.aggregate([ { $lookup: { de: , localField: , foreignField: , como: } } ]) |
Esto está muy bien, pero no funciona con las relaciones que se encuentran en las matrices. Esto significa que el 1TP4Búsqueda
no puede unirse a la operación producto_id
que se encuentra en el productos
a otro documento. En su lugar, la matriz debe "desenrollarse" o "desanidarse" primero, lo que añade complejidad a nuestra consulta:
1 2 3 4 5 6 7 8 9 10 11 |
db.pedidos.agregar([ { $unwind: "$products" }, { 1TP4Búsqueda: { de: "productos", localField: "products.product_id", foreignField: "_id", como: "productoObjetos" } } ]) |
En $unwind
aplanará la matriz y luego hará una unión en los objetos ahora planos que se produjeron. El resultado de esta consulta sería el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "_id" : ObjectId("58a3869acbf64c4ace55e713"), "productos" : { "product_id" : ObjectId("58a3851b2f14a900caa7a731"), "cantidad" : 2 }, "productoObjetos" : [ { "_id" : ObjectId("58a3851b2f14a900caa7a731"), "nombre" : "Pokemon Rojo", "precio" : 29,99 } ] } |
Si hubiera habido más de una referencia en la matriz, se habrían devuelto más resultados. Sin embargo, lo que se devuelve no es muy atractivo. Seguimos teniendo el antiguo productos
y ahora un productosObjeto
array. Es necesario realizar más manipulaciones en el flujo de datos.
En productosObjeto
debe ser "desenrollado" y luego reconstruido como queremos. Esto se puede lograr haciendo lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
db.pedidos.agregar([ { $unwind: "$products" }, { 1TP4Búsqueda: { de: "productos", localField: "products.product_id", foreignField: "_id", como: "productoObjetos" } }, { $unwind: "$productObjects"}, { $project: { productos: { "cantidad": "$products.quantity", "name": "$productObjects.name", "price": "$productObjects.price" } } } ]) |
Obsérvese que el agregado
La consulta se vuelve más compleja. Después de realizar la unión, el resultado se "desenrolla" y se reconstruye utilizando la función Proyecto $
operador.
En este punto se pueden realizar otras manipulaciones del resultado, como agrupar los resultados de forma que el productos
se convierten de nuevo en un único array. Cada manipulación del conjunto de datos requiere más código de agregación, que puede volverse desordenado, complicado y difícil de leer.
Aquí es donde Couchbase N1QL se vuelve mucho más agradable para trabajar.
Uso de Couchbase y N1QL para unir documentos NoSQL
Usemos el mismo ejemplo de documento que usamos para MongoDB. Esta vez vamos a escribir consultas SQL con N1QL para hacer el trabajo.
Lo primero que se nos ocurre puede ser utilizar un ÚNASE A
en SQL. Nuestra consulta podría ser algo así
1 2 3 4 |
SELECT pedidos.*, producto FROM ejemplo AS pedidos JOIN example AS product ON KEYS pedidos.productos[*].product_id WHERE pedidos.tipo = 'pedido' |
En el ejemplo anterior, ambos documentos existen en el mismo Bucket de Couchbase. A ÚNASE A
con los identificadores de los documentos se basa en producto_id
que se encuentran en el productos
array. La consulta anterior arrojaría resultados como los siguientes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[ { "id": "order-1", "producto": { "id": "producto-1", "Nombre": "Pokemon Rojo", "precio": 29.99, "tipo": "producto" }, "productos": [ { "product_id": "producto-1", "cantidad": 2 } ], "tipo": "orden" } ] |
Al igual que con MongoDB, habrá un resultado para cada elemento de la base de datos productos
que coincida. Para ser justos, aunque la versión N1QL era más fácil de escribir, no era necesariamente más difícil que el Lenguaje de Consulta MongoDB en este punto. A medida que manipulamos más los datos, Couchbase se vuelve mucho más fácil en comparación.
Por ejemplo, supongamos que queremos limpiar los resultados:
1 2 3 4 5 |
SELECT pedidos.id, pedidos.tipo, OBJECT_PUT(producto, "cantidad", productos.cantidad) AS producto FROM ejemplo AS pedidos UNNEST pedidos.productos COMO productos JOIN ejemplo COMO producto ON KEYS productos.product_id WHERE pedidos.tipo = 'pedido' |
Hay algunas diferencias importantes en lo que estamos haciendo en lo anterior, pero diferencias menores en cómo lo estamos haciendo. En lugar de unir directamente en la matriz, primero estamos aplanando o "unnesting" la matriz, como lo que vimos en el MongoDB $unwind
operador. La unión se produce ahora en cada uno de los resultados aplanados. Por último, el operador cantidad
del objeto original se añade al nuevo objeto.
El resultado de la consulta anterior sería algo parecido a esto:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[ { "id": "order-1", "producto": { "id": "producto-1", "Nombre": "Pokemon Rojo", "precio": 29.99, "cantidad": 2, "tipo": "producto" }, "tipo": "orden" } ] |
Digamos que el original productos
tenía más de una referencia de producto. En lugar de devolver varios objetos basados en el ÚNASE A
que vimos anteriormente, podría tener sentido volver a empaquetar esa matriz original.
1 2 3 4 5 6 |
SELECT pedidos.id, pedidos.tipo, ARRAY_AGG(OBJECT_PUT(producto, "cantidad", productos.cantidad)) AS productos FROM ejemplo AS pedidos UNNEST pedidos.productos COMO productos JOIN ejemplo COMO producto ON KEYS productos.product_id WHERE pedidos.tipo = 'pedido' GROUP BY pedidos |
En la consulta anterior sólo hemos añadido ARRAY_AGG
y un GRUPO POR
pero, como resultado, cada documento unido aparece en la carpeta productos
en lugar del valor id.
No quiero usar un ÚNASE A
¿operador? Pruebe a utilizar una subconsulta SQL.
Conclusión
En NoSQL, unir datos es una preocupación muy popular para los desarrolladores que son veteranos RDBMS. Como MongoDB es una tecnología NoSQL muy popular, pensé que sería bueno usarla para comparar cómo Couchbase maneja la unión de documentos. Para operaciones ligeras, 1TP4Búsqueda
es tolerable, pero a medida que las consultas join en MongoDB se vuelven más complejas, puede que necesite dar un paso atrás. Con N1QL, escribir consultas complejas que incluyen operaciones de unión se vuelve muy fácil y sigue siendo fácil independientemente de lo compleja que sea la consulta.
Para obtener más información sobre N1QL y Couchbase, visite la página Portal para desarrolladores de Couchbase.