mongodb query an array within an array within an array - arrays

I have a collection of users. Each of those have an array of bookmarks. Each bookmark has an array of categories it belongs to. Leading to a structure like this:
[
{name: "Bob",
bookmarks: [
{url: "http://duckduckgo.com",
categories: [
"Search",
"Ducks",
],
},
],
},
]
Now given a name and a url and a category name. I want to delete said category of the respective bookmark. But my problem is that all attempts return the whole user or delete the whole bookmark and not just the category.
This is my best attempt using the mgo driver so far:
type arbitraryJson map[string]interface{}
user := "Bob"
bookmarkURL := url.Parse("http://duckduckgo.com")
tagName := "Search"
err = userDB.Update(
arbitraryJson{
"name": user,
"bookmarks.url": bookmarkURL.String(),
},
arbitraryJson{
"$pull": arbitraryJson{
"bookmarks.categories": tagName,
},
},
)
Which translates (I think) to the mongo query:
db.users.updateOne(
{ name: "Bob",
bookmarks.url: "http://duckduckgo.com" }
{
$pull: { bookmarks.categories: "search" }
}
)

Related

Create a new array of objects in a aggregate query in Mongodb

I'm running a query on Mongodb to get the combine data from two different collections: User and Store.
User collection has a property named as store_ids, which is an array that contains a list of ObjectIds of each store that the User has access to.
I'm trying to add the name of each store in the query result.
Example:
User Document:
{
_id: '58ebf8f24d52e9ab59b5538b',
store_ids: [
ObjectId("58dd4bb10e2898b0057be648"),
ObjectId("58ecd57d1a2f48e408ea2a30"),
ObjectId("58e7a0766de7403f5118afea"),
]
}
Store Documents:
{
_id: "58dd4bb10e2898b0057be648",
name: "Store A",
},
{
_id: "58ecd57d1a2f48e408ea2a30",
name: "Store B",
},
{
_id: "58e7a0766de7403f5118afea",
name: "Store C"
}
I'm looking for a query that returns an output like this:
{
_id: '58ebf8f24d52e9ab59b5538b',
stores: [
{
_id: ObjectId("58dd4bb10e2898b0057be648"),
name: "Store A"
},
{
id: ObjectId("58ecd57d1a2f48e408ea2a30"),
name: "Store B"
},
{
_id: ObjectId("58e7a0766de7403f5118afea"),
name: "Store C"
}
]
}
I've already tried operations like $map and $set. I don't know if I'm applying them in the right way because they didn't work for my case.
You can use an aggregate query:
db.users.aggregate([
{
$lookup: {
from: "stores", //Your store collection
localField: "store_ids",
foreignField: "_id",
as: "stores"
}
},
{
$project: {
store_ids: 0
}
}
])
You can see a working example here: https://mongoplayground.net/p/ICsEEsmRcg0
We can achieve this with a simple $lookup and with $project.
db.user.aggregate({
"$lookup": {
"from": "store",
"localField": "store_ids",
"foreignField": "_id",
"as": "stores"
}
},
{
"$project": {
store_ids: 0
}
})
$lookup will join with store table on with the store_ids array where the _id matches
$project removes the store_ids array from the resulting objects
Playground

Query to aggregate a field from one collection to another in MongoDB

I have these two collections.
collectionname : req
{
reqId: "A123",
status: "1",
location: "hyd"
}
collection name : reqUser
{
req: "A123"
userId: "U1787"
designation: "employee"
}
Need an aggregate filtering the req collection based on location as hyd & aggregating the user field based on reqId.
Need Like this:
{
reqId: "A123",
status: "1",
userId: "U1787"
}
I used query on req collection with $match to filter based on location & $project to display only required fields from req collection and $lookup to match the reqId's from both collection but the issue is I am getting the whole reqUser object.
I am getting Something like this:
{
reqId: "A123",
status: "1",
reqUser:[
{
req: "A123"
userId: "U1787"
designation: "employee"
}
]
}
I need only userId field. Can anyone help me with obtaining the data as per my requirement mentioned above.
I am using mongo version 3.4.
Assuming req and reqUser has one-to-one relations, you could use the following query:
db.req.aggregate([
{
"$lookup": {
"from": "reqUser",
"localField": "reqId",
"foreignField": "req",
"as": "user"
}
},
{
$set: {
userId: {
$arrayElemAt: [
"$user.userId",
0
]
}
}
},
{
"$project": {
user: 0
}
}
])
Mongo playground ref: https://mongoplayground.net/p/V7bUdOXjnFC

MongoDb: What's the best approach to modify a model field from an array of strings to an array of ids that would refer to another model?

I have a Profile model, that contains this field:
interests: {
type: [String],
},
My app has been running for a while. So this means for several documents, this field has already been filled with an array of strings.
In order to achieve certain goals, I need to create a model Interest with a field name and then refer to it in the Profile like this:
interests: [{
type: Schema.Types.ObjectId,
ref: "interests",
}],
The field name should contain the already existing string interests in Profile.interests.
This is the approach that I think I will follow:
Create Interest model.
Fill name field with the existing Profile.interests strings.
a. When doing this replace Profile.interests with the _ids of the newly created Interest documents.
b. Make sure Interest.name is unique.
c. Remove spaces.
Wherever interests in the app are used in the backend, use populate to fill them.
This doesn't feel like a safe operation. So I would like to hear your thoughts on it. Is there a better approach? Should I avoid doing this?
Thank you.
Step 1:
Create a Model for interests,
specify your desired fields fir interests schema and set properties for particular fields
specify collection name in options as per your requirement
create a model and specify your desired name in model
const InterestsSchema = new mongoose.Schema(
{ name: { type: String } },
{ collection: "interests" }
);
const Interests = mongoose.model("Interests", InterestsSchema);
Instead of removing interests field add new field interest (you can choose desired field), for safe side whenever you feel the current update working properly you can remove it, Update profile schema,
update interest field as per your requirement, now newly added field is interest
interests: {
type: [String]
},
interest: [{
type: Schema.Types.ObjectId,
ref: "interests"
}],
Step 2:
Wherever interests in the app are used in the backend, use interest and populate to fill them.
Step 3: (just execute the query)
Make a collection for interests and store all unique interests string from profile collection, so write a aggregation query to select unique string and store in interests collection, you can execute this query in mongo shell or any editor that you are using after specifying your original profile collection name,
$project to show interests field only because we are going to deconstruct it
$unwind to deconstruct interests array
$group by interests and select unique field, and trim white space from interests string
$project to show name field and if you want to then add your desired fields
$out will create a new collection interests and write all interests with newly generated _id field
db.getCollection('profile').aggregate([
{ $project: { interests: 1 } },
{ $unwind: "$interests" },
{ $group: { _id: { $trim: { input: "$interests" } } } },
{ $project: { _id: 0, name: "$_id" } },
{ $out: "interests" }
])
Playground
You have example input:
[
{
"_id": 1,
"interests": ["sports","sing","read"]
},
{
"_id": 2,
"interests": ["tracking","travel"]
}
]
After executing above query the output/result in interests / new collection would be something like:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "travel"
},
{
"_id": ObjectId("5a934e000102030405000001"),
"name": "sports"
},
{
"_id": ObjectId("5a934e000102030405000002"),
"name": "read"
},
{
"_id": ObjectId("5a934e000102030405000003"),
"name": "tracking"
},
{
"_id": ObjectId("5a934e000102030405000004"),
"name": "sing"
}
]
Step 4: (just execute the query)
Add new field interest with reference _ids from interests collection in profile collection, there are sequence to execute queries,
find profile query and project only required fields _id and interests when interest (new field) field is not exists and iterate loop using forEach
trim interests string iterating loop through map
find the interests reference _id by its name field from created interests collection
update query for add interest field that have _ids in profile collection
db.getCollection('profile').find(
{ interest: { $exists: false } },
{ _id: 1, interests: 1 }).forEach(function(profileDoc) {
// TRIM INTEREST STRING
var interests = profileDoc.interests.map(function(i){
return i.trim();
});
// FIND INTERESTS IDs
var interest = [];
db.getCollection('interests').find(
{ name: { $in: interests } },
{ _id: 1 }).forEach(function(interestDoc){
interest.push(interestDoc._id);
});
// UPDATE IDS IN PROFILE DOC
db.getCollection('profile').updateOne(
{ _id: profileDoc._id },
{ $set: { interest: interest } }
);
});
You have example input:
[
{
"_id": 1,
"interests": ["sports","sing","read"]
},
{
"_id": 2,
"interests": ["tracking","travel"]
}
]
After executing above query the result in your profile collection would be something like:
[
{
"_id": 1,
"interests": ["sports","sing","read"],
"interest": [
ObjectId("5a934e000102030405000001"),
ObjectId("5a934e000102030405000002"),
ObjectId("5a934e000102030405000004")
]
},
{
"_id": 2,
"interests": ["tracking","travel"],
"interest": [
ObjectId("5a934e000102030405000000"),
ObjectId("5a934e000102030405000003")
]
}
]
Step 5:
Now you have completed all the steps and you have newly added interest field
and also old field interests field is still in safe mode, just make sure everything is working properly you can delete old interests field,
remove old field interests field from all profiles
db.getCollection('profile').updateMany(
{ interests: { $exists: true } },
{ $unset: { "interests": 1 } }
);
Playground
Warning:
Test this steps in your local/development server before executing in production server.
Take backup of your database collections before executing queries.
Field and schema names are predicted you can replace with your original name.

Update array of subdocuments in MongoDB

I have a collection of students that have a name and an array of email addresses. A student document looks something like this:
{
"_id": {"$oid": "56d06bb6d9f75035956fa7ba"},
"name": "John Doe",
"emails": [
{
"label": "private",
"value": "private#johndoe.com"
},
{
"label": "work",
"value": "work#johndoe.com"
}
]
}
The label in the email subdocument is set to be unique per document, so there can't be two entries with the same label.
My problems is, that when updating a student document, I want to achieve the following:
adding an email with a new label should simply add a new subdocument with the given label and value to the array
if adding an email with a label that already exists, the value of the existing should be set to the data of the update
For example when updating with the following data:
{
"_id": {"$oid": "56d06bb6d9f75035956fa7ba"},
"emails": [
{
"label": "private",
"value": "me#johndoe.com"
},
{
"label": "school",
"value": "school#johndoe.com"
}
]
}
I would like the result of the emails array to be:
"emails": [
{
"label": "private",
"value": "me#johndoe.com"
},
{
"label": "work",
"value": "work#johndoe.com"
},
{
"label": "school",
"value": "school#johndoe.com"
}
]
How can I achieve this in MongoDB (optionally using mongoose)? Is this at all possible or do I have to check the array myself in the application code?
You could try this update but only efficient for small datasets:
mongo shell:
var data = {
"_id": ObjectId("56d06bb6d9f75035956fa7ba"),
"emails": [
{
"label": "private",
"value": "me#johndoe.com"
},
{
"label": "school",
"value": "school#johndoe.com"
}
]
};
data.emails.forEach(function(email) {
var emails = db.students.findOne({_id: data._id}).emails,
query = { "_id": data._id },
update = {};
emails.forEach(function(e) {
if (e.label === email.label) {
query["emails.label"] = email.label;
update["$set"] = { "emails.$.value": email.value };
} else {
update["$addToSet"] = { "emails": email };
}
db.students.update(query, update)
});
});
Suggestion: refactor your data to use the "label" as an actual field name.
There is one straightforward way in which MongoDB can guarantee unique values for a given email label - by making the label a single separate field in itself, in an email sub-document. Your data needs to exist in this structure:
{
"_id": ObjectId("56d06bb6d9f75035956fa7ba"),
"name": "John Doe",
"emails": {
"private": "private#johndoe.com",
"work" : "work#johndoe.com"
}
}
Now, when you want to update a student's emails you can do an update like this:
db.students.update(
{"_id": ObjectId("56d06bb6d9f75035956fa7ba")},
{$set: {
"emails.private" : "me#johndoe.com",
"emails.school" : "school#johndoe.com"
}}
);
And that will change the data to this:
{
"_id": ObjectId("56d06bb6d9f75035956fa7ba"),
"name": "John Doe",
"emails": {
"private": "me#johndoe.com",
"work" : "work#johndoe.com",
"school" : "school#johndoe.com"
}
}
Admittedly there is a disadvantage to this approach: you will need to change the structure of the input data, from the emails being in an array of sub-documents to the emails being a single sub-document of single fields. But the advantage is that your data requirements are automatically met by the way that JSON objects work.
After investigating the different options posted, I decided to go with my own approach of doing the update manually in the code using lodash's unionBy() function. Using express and mongoose's findById() that basically looks like this:
Student.findById(req.params.id, function(err, student) {
if(req.body.name) student.name = req.body.name;
if(req.body.emails && req.body.emails.length > 0) {
student.emails = _.unionBy(req.body.emails, student.emails, 'label');
}
student.save(function(err, result) {
if(err) return next(err);
res.status(200).json(result);
});
});
This way I get the full flexibility of partial updates for all fields. Of course you could also use findByIdAndUpdate() or other options.
Alternate approach:
However the way of changing the schema like Vince Bowdren suggested, making label a single separate field in a email subdocument, is also a viable option. In the end it just depends on your personal preferences and if you need strict validation on your data or not.
If you are using mongoose like I do, you would have to define a separate schema like so:
var EmailSchema = new mongoose.Schema({
work: { type: String, validate: validateEmail },
private: { type: String, validate: validateEmail }
}, {
strict: false,
_id: false
});
In the schema you can define properties for the labels you already want to support and add validation. By setting the strict: false option, you would allow the user to also post emails with custom labels. Note however, that these would not be validated. You would have to apply the validation manually in your application similar to the way I did it in my approach above for the merging.

How to check before updating an array element in MongoDB/NodeJS

In my sample document, I have a campaign document that contains the _id of the document and an importData array. importData is an array of objects containing a unique date and source value.
My goal is to have an object updated with a unique date/source pair. I would like to have the new object replace any matching object. In the example below, Fred may have originally donated a TV, but I want my application to update the object to reflect he donated both a TV and a radio.
// Events (sample document)
{
"_id" : "Junky Joe's Jubilee",
"importData" : [
{
"date": "2015-05-31",
"source": "Fred",
"items": [
{item: "TV", value: 20.00},
{item: "radio", value: 5.34}
]
},
{
"date": "2015-05-31",
"source": "Mary",
"items": [
{item: "Dresser", value: 225.00}
]
}
]
}
My original thought was to do something like the code below, but not only am I updating importData with Fred's donations, I'm also blowing away anything else in the importData array:
var collection = db.collection("events");
collection.update(
{_id: "Junky Joe's Jubilee",
importData: {
date: "2015-05-31",
source: 'Fred'
},
}, // See if we can find a campaign object with this name
{
$set:
{"importData":
{
date: "2015-05-31",
source: 'Fred',
items: [
{item: "TV", value: 20.00},
{item: "radio", value: 5.34}
]
}
}
},
{upsert: true}); // Create a document if one does not exist for this campaign
When I tried pushing (instead of $set), I was getting multiple entries for the date/source combos (e.g. Fred would appear to have donated two items multiple times on "2015-05-31").
How would I go about doing that with the MongoDB native driver and NodeJS?
Try this
var collection = db.collection("events");
collection.update(
{_id: "Junky Joe's Jubilee",
importData: {
date: "2015-05-31",
source: 'Fred'
},
}, // See if we can find a campaign object with this name
{
$set:
{"importData.$":
{
date: "2015-05-31",
source: 'Fred',
items: [
{item: "TV", value: 20.00},
{item: "radio", value: 5.34}
]
}
}
},
{upsert: true}); // Create a document if one does not exist for this campaign
According to the documentation under Array update operators this should only modify the first element in the array, which matches the query.

Resources