I have a collection of persons with a group property. Group value is a random string, which I don't know when I make my query. Some persons group values will possibly be the same.
I want to write a query to retrieve only one person per group (which person is returned in the group is non relevant for the moment).
I suppose I have to write some kind of aggregation. But I only read about examples aggregating on known values... Instead I need to aggregate on unknown values...
My schema is something like:
const schemaPersons = new Schema({
name: {
type: String,
required: true,
},
group: {
type: String,
},
};
You can write aggregation query using $group stage:
$group stage, group by group field, and get only first person's root document in person field
Persons.aggregate([
{
$group: {
_id: "$group",
person: { $first: "$$ROOT" }
}
}
]);
Playground
Related
Consider I have this document in my MongoDB collection, Workout:
{
_id: ObjectId("60383b491e2a11272c845749") <--- Workout ID
user: ObjectId("5fc7d6b9bbd9473a24d3ab3e") <--- User ID
exercises: [
{
_id: ObjectId("...") <--- Exercise ID
exerciseName: "Bench Press",
sets: [
{
_id: ObjectId("...") <--- Set ID
},
{
_id: ObjectId("...") <--- Set ID
}
]
}
]
}
The Workout object can include many exercise objects in the exercises array and each exercise object can have many set objects in the sets array. I am trying to implement a delete functionality for a certain set. I need to retrieve the workout that the set I want to delete is stored in. I have access to the user's ID (stored in a context), exercise ID and the set ID that I want to delete as parameters for the .findOne() function. However, I'm not sure whether I can traverse through the different levels of arrays and objects within the workout object. This is what I have tried:
const user = checkAuth(context) // Gets logged in user details (id, username)
const exerciseID, setID // Both of these are passed in already and are set to the appropriate values
const workoutLog = Workout.findOne({
user: user.id,
exercises: { _id: exerciseID }
});
This returns an empty array but I am expecting the whole Workout object that contains the set that I want to delete. I would like to omit the exerciseID from this function's parameters and just use the setID but I'm not sure how to traverse through the array of objects to access it's value. Is this possible or should I be going about this another way? Thanks.
When matching against an array, if you specify the query like this:
{ exercises: { _id: exerciseID } }
MongoDB tries to do an exact match on the document. So in this case, MongoDB would only match documents in the exercises array of the exact form { _id: ObjectId("...") }. Because documents in the exercises have other fields, this will never produce a match, even if the _ids are the same.
What you want to do instead is query a field of the documents in the array. The complete query document would then look like this:
{
user: user.id,
"exercises._id": exerciseID
}
You can perform both find and update in one step. Try this:
db.Workout.updateOne(
{
"user": ObjectId("5fc7d6b9bbd9473a24d3ab3e"),
},
{
$pull: {
"exercises.$[exercise].sets": {
"_id": ObjectId("6039709fe0c7d52970d3fa30") // <--- Set ID
}
}
},
{
arrayFilters: [
{
"exercise._id" : ObjectId("6039709fe0c7d52970d3fa2e") // <--- Exercise ID
}
]
}
);
In Mongoose, I have two collections, with one referencing the other. Is it possible to have a find query that selects records based on a value in the other. An example of what I am try to get at (not actual schemas):
const CarModelSchema = new mongoose.Schema({
name: String,
brand: { type: mongoose.Schema.Types.ObjectId, ref: 'CarBrand' }
});
const CarBrandSchema = new mongoose.Schema({
name: String,
country: String
});
I then want to perform a query of the form, without needing to do two queries:
CarModelSchema.find({ 'brand.country': 'GER' });
So far I haven't been able to make this work, so I am wondering whether this can be done in Mongo or whether I am approaching it wrong?
Yes it is possible.
I realize you don't have models for your schemas so add them like this:
const CarModel = mongoose.model('CarModel', CarModelSchema);
const CarBrand = mongoose.model('CarBrand', CarBrandSchema);
Also brands should be defined like this:
brand: [{ type: mongoose.Schema.Types.ObjectId, ref: 'CarBrand' }] //added the brackets
You can then run a find query to filter by country by doing the following:
CarModel.
find(...).
populate({
path: 'brand',
match: { country: { $eq: 'GER' }},
// You can even select the field you want using select like below,
select: 'name -_id',
//Even limit the amount of documents returned in the array
options: { limit: 5 }
}).
exec();
And that should do it, as long as the ObjectIds saved in brands array in the CarModel collection are valid or exist.
Using match in your population will do the work.
CarModel.find()
.populate({
path: 'brand',
model: CarBrandModel,
match: { country: { $eq: 'GER' }},
})
.exec()
Keep in mind you have to define CarModel and CarBrandModel like this:
const CarModel = mongoose.model('CarModel', CarModelSchema)
const CarBrandModel = mongoose.model('CarBrandModel', CarBrandSchema)
Yes, you are doing it wrong.
In CarModelSchema.brand there is not string saved, there is ObjectId saved, therefore you have to find that ObjectId (the reference).
You can do it manually - first finding the CarBrandSchema.find({ 'country': 'GER' }); and then use its ObjectId (=_id), or you can use https://mongoosejs.com/docs/populate.html to populate your CarModel with the CarBrand object.
I have two GraphQL type:
type Author {
id: String!
name: String!
}
type Book {
id: String!
author: Author!
name: String!
}
In my database, it is implemented by a foreign key inside the books table:
table authors (pseudo code)
`id` INTEGER UNSIGNED
`name` STRING
table books (pseudo code)
`id` INTEGER UNSIGNED
`author_id` INTEGER UNSIGNED REFERENCE `authors.id`
`name` STRING
So when I resolve a GraphQL query like:
query allTheBooks {
id
name
author {
id
name
}
}
I would like to do only one SELECT SQL query like:
SELECT books.id, books.name, authors.id, authors.name
FROM books
LEFT JOIN authors ON authors.id = books.author_id
Instead of the current:
SELECT books.id, books.name, books.author_id
FROM books
SELECT authors.id, authors.name
FROM authors
WHERE author.id = [one the the books.author_id of the first SELECT]
SELECT authors.id, authors.name
FROM authors
WHERE author.id = [one the the books.author_id of the first SELECT]
[...]
Is there a way to know which "child fields" are queried in a GraphQL resolver ?
Thank you in advance for your answer !
I just discovered that in the fourth parameter gived at the resolver, there where an array of the queried fields: info.fieldNodes[0].selectionSet.selections.
I didn't found any documentation about this, and I wonder what represent the fieldNodes array... (and dont like to access the first element that way without knowing it...).
The selections object contains an array like this:
[
{
kind: 'Field',
alias: undefined,
name: { kind: 'Name', value: 'id', loc: [Object] },
arguments: [],
directives: [],
selectionSet: undefined,
loc: { start: 36, end: 38 }
},
{
[...]
},
...
]
Here, the value: 'id' match the name of the queried field of the related author.
If I go a level deeped, the selectionSet: undefined becomes an object and the pattern repeat itself recursively...
I'm building a Node.js application to keep an inventory of books and am using MongoDB and mongoose.
When I first modeled my Schema, there was a genre field of type String which held a single value; in other words, a book could only be assigned one genre.
After adding some "books" into my database, I decided to make a slight change to the Schema design and updated the genre field to genres so that I could instead have an Array of strings. Now every new document I add into the database has a genres field of type Array, but now I'm wondering how to update the documents in the database which reflect the design of the earlier model.
I've started to write a script that:
Iterates through documents in the database where the field genre (singular) exists,
Updates the field name to genres AND sets its value to an array containing whatever value the previous genre property was equal to.
So for instance, (for simplicity's sake I'll use an object with a single property of genre), from this:
{genre: "Philosophy"}
I'm trying to get to this
{genres: ["Philosophy"]}
How can I achieve this?
This is the code I have so far
db.books.updateMany({genre: {$exists: true}},{<missing this part>}
You can use aggregation framework to do that:
db.books.aggregate([
{ $match: { genre: { $exists: true } } },
{
$group: {
_id: "$_id",
genres: { $push: "$genre" },
fst: { $first: "$$ROOT" }
}
},
{
$replaceRoot: { newRoot: { $mergeObjects: ["$fst", "$$ROOT" ] } }
},
{ $project: { fst: 0, genre: 0 } },
{ $out: "books" }
])
$group is a way of transforming property to an array (each group will contain only one element since we're grouping by unique _id). Using $replaceRoot with $mergeObjects you can merge all your properties from original object with your new array. Then you need to remove unnecessary fields using $project. Last step which is $out will redirect the output of this aggregation into specified collection, causing updates in this case.
You want to iterate over every element in your db:
db.books.find({genre: {$exists: true}}).forEach(doc => {
Then you want to update that element:
if(Array.isArray(doc.genre)) return;
db.books.updateOne(doc, { $set: { genre:[doc.genre]} });
This schema:
var Order = new Schema({
name: String,
products: [{
product: {
type: Schema.Types.ObjectId,
ref: "Product"
},
qty: Number
}]
});
I would like to find and return the order with only the product that has the matching id. My attempt at $elemMatch have not been successful and seem to return the entire order with all products. I can use the position operator ($) but would like in this case to specifically use $elemMatch.
Note that each product in the order is unique (within that order)
This returns all products:
Order.find({ _id: req.params.id}, { 'products.product': {$elemMatch: { _id: req.params.product }}}, function(err, order) {
This returns empty:
Order.find({ _id: req.params.id}, { 'products': {$elemMatch: { 'product._id': req.params.product }}}, function(err, order) {
What is correct syntax? I would like the order returned with just one (matching) product.
In the products array, the product field directly contains the product's id, so the correct syntax would be:
Order.find(
{ _id: req.params.id},
{ products: {$elemMatch: { product: req.params.product }}},
function(err, order) { ... });
You can use query projection only, if the product in an order is always unique (there can't be more than 1 same product in 1 order). If same product can appear more than one in 1 order, you cannot use query projection since it will only return the first found product in products array.
Instead, use aggregation. Make use of $unwind and $match operator.
It will be something like this in mongo shell javascript.
db.orders.aggregate([
{$match: {_id: Order_id_being_search}},
{$unwind: "$products"},
{$match: {"products.product._id": Product_id_being_search }}
])
It will return the desired product (only the product), in a selected Order document. If you want to return the desired product from all Order, just omit the first $match.
Sorry, I am not used to using "REF" so I assume a manual reference using _id key on the code above. You can modify that part in the code. You can use the algo.
MongoDB uses aggregation not only to find aggregation value like sum, avg, max, etc, like relational ones. It also uses aggregation framework for dealing with subdocuments. Your case is a perfect example of the use case of $unwind operation in aggregation framework.