How to do lookup with an array in mongoDB - database

today I started working with MongoDB. I have created two collections: Restaurant and OpeningHours. I have inserted data to the database with the following code:
db.OpeningHours.insert({
day: "Sunday",
from: "10.00am",
to: "16.00pm"
});
db.Restaurant.insert({
name: "Restaurant01",
openingHoursId:
[
{id: db.OpeningHours.find({day: "Sunday", from: "10.00am", to: "16.00pm"})[0]._id},
]
});
The restaurant contains an array of OpeningHours ids. I want to write a query with lookup so I get all the data from the Restaurant and the data for corresponding opening hours. Here is my code so far and if I run it I get an error: Command failed...
db.Restaurant.aggregate([
{
$unwind: "$openingHoursId",
$lookup:
{
from: "OpeningHours",
localField: "id",
foreignField: "_id",
as: "RestaurantHours"
}
}
])
The expected result I want is something like this:
{
"_id": ObjectId("5c43b6c8d0fa3ff24621f749"),
"name": "Restaurant01",
"openingHoursId":
[
{
"id": ObjectId("5c43b6c8d0fa3ff2462fg93e")
}
],
"RestaurantHours" :
[
{
"_id": ObjectId("5c43b6c8d0fa3ff2462fg93e"),
"day": "Sunday",
"from": "10.00am",
"to": "16.00pm"
}
]
}

Your localField should be openingHoursId.id not only id
db.Restaurant.aggregate([
{ "$unwind": "$openingHoursId" },
{ "$lookup": {
"from": "OpeningHours",
"localField": "openingHoursId.id",
"foreignField": "_id",
"as": "openingHoursId.RestaurantHours"
}},
{ "$unwind": "$openingHoursId.RestaurantHours" },
{ "$group": {
"_id": "$_id",
"name": { "$first": "name" },
"openingHoursId": { "$push": "openingHoursId" }
}}
])

Related

Join collection with array object field with another collection in MongoDB

I'm working in MongoDB and getting stuck at one aggregation case. Let me show you my collection.
First collection (data):
[
{
"_id": "8e7b3fa0-4230-448c-8f70-1d7300632834",
"data": [
{
"animal" : "7d44251a-b308-4deb-875a-33ef0a69fe2b",
"place": "Chennai"
},
{
"animal" : "fcfdd527-5885-48b0-a91f-03f72f78528f",
"place": "Kolkata"
}
]
}
]
Second collection (Animal):
[
{
"_id": "7d44251a-b308-4deb-875a-33ef0a69fe2b",
"name": "Dog"
},
{
"_id": "7d44251a-b308-4deb-875a-33ef0a69fe2b",
"name": "Cat"
}
]
I'm using this query:
db.data.aggregate([
{
"$lookup": {
"from": "animal",
"localField": "data.animal",
"foreignField": "_id",
"as": "doc"
}
},
{
"$unwind": "$doc"
},
{
"$project": {
"_id": 1,
"data.animal": "$doc.name",
"data.place": 1
}
}
])
and it result me this
[
{
"_id": "8e7b3fa0-4230-448c-8f70-1d7300632834",
"data": [
{
"animal": "Dog",
"place": "Chennai"
},
{
"animal": "Dog",
"place": "Kolkata"
}
]
},
{
"_id": "8e7b3fa0-4230-448c-8f70-1d7300632834",
"data": [
{
"animal": "Cat",
"place": "Chennai"
},
{
"animal": "Cat",
"place": "Kolkata"
}
]
}
]
Where I'm expecting like this
[
{
"_id": "8e7b3fa0-4230-448c-8f70-1d7300632834",
"data": [
{
"animal": "Dog",
"place": "Chennai"
},
{
"animal": "Cat",
"place": "Kolkata"
}
]
}
]
Mongo Playground
Also sharing this question in Mongo playgroud. Thanks in advance!!
Solution 1
$unset - Deconstruct the data array into multiple documents.
$lookup - Perform join with animal collection.
$project - Decorate the output document. For data.animal field, get the first value via $first.
$group - Group by _id. Push the data document into the data array.
db.data.aggregate([
{
"$unwind": "$data"
},
{
"$lookup": {
"from": "animal",
"localField": "data.animal",
"foreignField": "_id",
"as": "doc"
}
},
{
"$project": {
"_id": 1,
"data.animal": {
$first: "$doc.name"
},
"data.place": 1
}
},
{
$group: {
_id: "$_id",
data: {
$push: "$data"
}
}
}
])
Demo Solution 1 # Mongo Playground
Solution 2
$lookup
$set - Set data field.
2.1. $map - Iterate the data array and returns a new array.
2.1.1. $mergeObjects - Merge current iterated document with place field and the document from 2.1.1.1.
2.1.1.1. $first - Get the first document from the filtered doc arrays by matching the ids via $filter.
$unset - Remove _id and animals._id fields.
db.data.aggregate([
{
"$lookup": {
"from": "animal",
"localField": "data.animal",
"foreignField": "_id",
"as": "doc"
}
},
{
$set: {
data: {
$map: {
input: "$data",
as: "data",
in: {
$mergeObjects: [
{
place: "$$data.place"
},
{
$first: {
$filter: {
input: "$doc",
cond: {
$eq: [
"$$this._id",
"$$data.animal"
]
}
}
}
}
]
}
}
}
}
},
{
$unset: [
"doc",
"data._id"
]
}
])
Demo Solution 2 # Mongo Playground

Update all objects in nested array with values from other collection

I have a collection of vehicles with the following car structure:
{
"_id": {}
brand : ""
model : ""
year : ""
suppliers : [
"name": "",
"contact": ""
"supplierId":"",
"orders":[], <-- Specific to the vehicles collection
"info":"" <-- Specific to the vehicles collection
]
}
And a Suppliers collection with a structure like:
{
"name":"",
"contact":"",
"_id":{}
"internalId":"",
"address":"",
...
}
I need to add a new field in the suppliers array within each document in the vehicles collection with the internalId field from the supplier in the suppliers collection that has the same _id.
if the supplier array has a document with the id 123, i should go to the suppliers collection and look for the supplier with the id 123 and retrieve the internalId. afterwards should create the field in the supplier array with that value.
So that i end up with the vehicles collection as:
{
"_id": {}
brand : ""
model : ""
year : ""
suppliers : [
"name": "",
"contact": ""
"supplierId":""
"internalId":"" <-- the new field
]
}
Tried:
db.vehicles.aggregate([
{
"$unwind": { "path": "$suppliers", "preserveNullAndEmptyArrays": false }
},
{
"$project": { "supplierObjId": { "$toObjectId": "$suppliers.supplierId" } }
},
{
"$lookup":
{
"from": "suppliers",
"localField": "supplierObjId",
"foreignField": "_id",
"as": "supplierInfo"
}
},{
"$set": {
"suppliers.internalId": "$supplierInfo.internalid"
}}
])
But it is adding the new field, to the returned values instead to the array item at the collection.
How can i achieve this?
But it is adding the new field, to the returned values instead to the array item at the collection.
The .aggregate method does not update documents, but it will just format the result documents,
You have to use 2 queries, first aggregate and second update,
I am not sure i guess you want to execute this query for one time, so i am suggesting a query you can execute in mongo shell,
Aggregation query:
$lookup with pipeline, pass suppliers.supplierId in let
$toString to convert object id to string type
$match the $in condition
$project to show required fields
$map to iterate loop of suppliers array
$reduce to iterate loop of suppliers_data array and find the matching record by supplierId
$mergeObjects to merge current object properties with new property internalId
Loop the result from aggregate query using forEach
Update Query to update suppliers array
db.vehicles.aggregate([
{
$lookup: {
from: "suppliers",
let: { supplierId: "$suppliers.supplierId" },
pipeline: [
{
$match: {
$expr: {
$in: [{ $toString: "$_id" }, "$$supplierId"]
}
}
},
{
$project: {
_id: 0,
supplierId: { $toString: "$_id" },
internalId: 1
}
}
],
as: "suppliers_data"
}
},
{
$project: {
suppliers: {
$map: {
input: "$suppliers",
as: "s",
in: {
$mergeObjects: [
"$$s",
{
internalId: {
$reduce: {
input: "$suppliers_data",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this.supplierId", "$$s.supplierId"] },
"$$this.internalId",
"$$value"
]
}
}
}
}
]
}
}
}
}
}
])
.forEach(function(doc) {
db.vehicles.updateOne({ _id: doc._id }, { $set: { suppliers: doc.suppliers } });
});
Playground for aggregation query, and Playground for update query.
It looks like one way to solve this is by using $addFields and $lookup. We first flatten any matching suppliers, then add the property, then regroup.
You can find a live demo here via Mongo Playground.
Database
Consider the following database structure:
[{
// Collection
"vehicles": [
{
"_id": "1",
brand: "ford",
model: "explorer",
year: "1999",
suppliers: [
{
name: "supplier1",
contact: "john doe",
supplierId: "001"
},
{
name: "supplier2",
contact: "jane doez",
supplierId: "002"
}
]
},
{
"_id": "2",
brand: "honda",
model: "accord",
year: "2002",
suppliers: [
{
name: "supplier1",
contact: "john doe",
supplierId: "001"
},
]
}
],
// Collection
"suppliers": [
{
"name": "supplier1",
"contact": "john doe",
"_id": "001",
"internalId": "999-001",
"address": "111 main street"
},
{
"name": "supplier2",
"contact": "jane doez",
"_id": "002",
"internalId": "999-002",
"address": "222 north street"
},
{
"name": "ignored_supplier",
"contact": "doesnt matter",
"_id": "xxxxxxx",
"internalId": "xxxxxxx",
"address": "0987 midtown"
}
]
}]
Query
This is the query that I was able to get working. I'm not sure how efficient it is, or if it can be improved, but this seemed to do the trick:
db.vehicles.aggregate([
{
$unwind: "$suppliers"
},
{
$lookup: {
from: "suppliers",
localField: "suppliers.supplierId",
foreignField: "_id", // <---- OR MATCH WHATEVER FIELD YOU WANT
as: "vehicle_suppliers"
}
},
{
$unwind: "$vehicle_suppliers"
},
{
$addFields: {
"suppliers.internalId": "$vehicle_suppliers.internalId"
}
},
{
$group: {
_id: "$_id",
brand: {
$first: "$brand"
},
model: {
$first: "$model"
},
year: {
$first: "$year"
},
suppliers: {
$push: "$suppliers"
}
}
}
])
Results
Which returns:
[
{
"_id": "2",
"brand": "honda",
"model": "accord",
"suppliers": [
{
"contact": "john doe",
"internalId": "999-001",
"name": "supplier1",
"supplierId": "001"
}
],
"year": "2002"
},
{
"_id": "1",
"brand": "ford",
"model": "explorer",
"suppliers": [
{
"contact": "john doe",
"internalId": "999-001",
"name": "supplier1",
"supplierId": "001"
},
{
"contact": "jane doez",
"internalId": "999-002",
"name": "supplier2",
"supplierId": "002"
}
],
"year": "1999"
}
]

MongoDB How to perform an aggregation lookup on array of an array

is it possible to aggregate and join match data like so:
application (Document) -> roles (DBRef array) -> permissions (DBRef array) -> name (String)
The application has roles, roles have permissions and permissions have a name.
I have been trying to figure out how I can make an aggregation operation that would achieve this type of complex join. What I want to do is choose the application that has the role, that has the permission of a given name.
Here is the basic example documents I have:
application:
{
"_id": {
"$numberLong": "11"
},
"name": "my-module",
"roles": [{
"$ref": "role",
"$id": {
"$numberLong": "17"
}
}
}
role:
{
"_id": {
"$numberLong": "17"
},
"name": "AdminRole",
"application": {
"$ref": "application",
"$id": {
"$numberLong": "11"
}
},
"permissions": [{
"$ref": "permission",
"$id": {
"$numberLong": "46"
}
}, {
"$ref": "permission",
"$id": {
"$numberLong": "49"
}
}]
}
permission:
{
"_id": {
"$numberLong": "46"
},
"name": "TestPermission1"
},
{
"_id": {
"$numberLong": "49"
},
"name": "TestPermission2"
}
I have figured out how to aggregate one level of data from the roles array:
$lookup:
{
from: 'role',
localField: 'roles.$id',
foreignField: '_id',
as: '_roles'
}
$match: /**
* query: The query in MQL.
*/
{
$or: [{
"_roles": {
"$elemMatch": {
state: 'DISABLED'
/* how can also look through the roles permissions (array of DBRef) for permission data? */
}
},
/* other checks */
}]
}
Any help regarding this issue is appreciated!
Thanks!
With this data structure you'd better search for permissions and lookup for roles and the apps after that. This way you can benefit from indexed name in permissions collection.
Something like this:
db.permission.aggregate([
{
"$match": {
name: "TestPermission1"
}
},
{
"$lookup": {
"from": "role",
"localField": "_id",
"foreignField": "permissions.$id",
"as": "permissions"
}
},
{
"$lookup": {
"from": "application",
"localField": "permissions.application.$id",
"foreignField": "_id",
"as": "app"
}
},
{
"$unwind": "$app"
},
{
"$replaceRoot": {
"newRoot": "$app"
}
}
])
As a side note you don't have to use DBRefs, which can make it a bit simpler to use.

Filter based on values after join operation - MongoDB

I have two collections in the following format -
collection 1
{
"_id": "col1id1",
"name": "col1doc1",
"properties": [ "<_id1>", "<_id2>", "<_id3>"]
}
collection 2
{
"_id": "<_id1>",
"name": "doc1",
"boolean_field": false
}
{
"_id": "<_id2>",
"name": "doc2",
"boolean_field": true
}
{
"_id": "<_id3>",
"name": "doc3",
"boolean_field" : false
}
the desired output is -
{
"_id": "col1id1",
"name": "col1doc1",
"property_names": ["doc1", "doc3"]
}
The field proerties of document in collection1 has three IDs of documents in collection2 but the output after join operation should contain only those which have the boolean_field value as false. How can I perform this filter with join operation in MongoDB?
$lookup can be used along with $unwind to achieve this.
db.col1.aggregate([
{
"$unwind": "$properties"
},
{
"$lookup": {
from: "col2",
localField: "properties",
"foreignField": "_id",
"as": "property_names"
}
},
{
"$match": {
"property_names": {
"$elemMatch": {
"bool_field": false
}
}
}
},
{
"$unwind": "$property_names"
},
{
"$group": {
"_id": "$_id",
"properties": {
"$push": "$properties"
},
"property_names": {
"$push": "$property_names"
}
}
},
{
"$project": {
"_id": 1,
"name": 1,
"property_names": {
"name": 1
}
}
}
]);

MongoDB how to $project (limit fields) from the $lookup remote collection?

I want to $lookup for a remote collection like SQL Join but with Mongo. And I don't want all of the keys from the remote document to be pulled to the origin collection - just some specific keys.
This is what I have tried:
[
{
$lookup: {
from: "tables",
localField: "type",
foreignField: "_id",
as: "type"
}
},
{
$unwind: "$type"
},
},
{
$project: {
"type.title": 1
}
}
]
However this prints only "type.title" and ignores all of other keys even from the origin document.
Is there any way to tell MongoDB to pull only specific fields from the remote collection?
You can use below aggregation with mongodb 3.6 and above
[
{ "$lookup": {
"from": "tables",
"let": { "type": "$type" },
"pipeline": [
{ "$addFields": { "owners": { "$cond": { "if": { "$ne": [ { "$type": "$owners" }, "array" ] }, "then": [], "else": "$owners" } } }},
{ "$match": { "$expr": { "$eq": ["$_id", "$$type"] }}},
{ "$project": { "title": 1 }}
],
"as": "type"
}},
{ "$unwind": "$type" }
]

Resources