mongodb - How to update a value inside an array? - mongodb

I am trying to update the following row:
{
"_id": "80a7",
"title": "Is React better than Angular?",
"options": [{
"option": "Yes",
"vote": 0
}, {
"option": "No",
"vote": 0
}],
Assuming in api/vote, the value passed is "Yes", How can I update the vote for Yes by $inc by 1 ?
const {id, option} = req.body;
const db = await getMongoDb();
await db.collection('polls').updateOne(
{
_id: id,
},
{
$set: {
options: ???,
},
},
);
What should be ??? replaced with?

Work with arrayFilters.
await db.collection('polls').updateOne({
_id: id
},
{
$inc: {
"options.$[opt].vote": 1
}
},
{
arrayFilters: [
{
"opt.option": "Yes"
}
]
})
Demo # Mongo Playground

Related

How do I findOndAndUpdate an item of an array?

This is the update statement:
const cart = await Cart.findOneAndUpdate({
userId: userId,
'items._id': itemId,
'items.product': productId,
'items.size': size,
'items.color': color,
}, {
$set: {
'items.$.quantity': quantity
}
}, {
new: true
}).populate({
path: 'items.product',
model: 'Product'
})
This is the new quantity to be applied to the array:
newQuantity {
itemId: '625065c99edbfad52ac3afce',
productId: '6205a4565c0caba6fb39cd5d',
size: '3',
quantity: '3',
color: 'blue2'
}
This is the data in the database, the first item of the array is updated with the new quantity rather than the second item that meets the query criteria shown above.
{
"_id": {
"$oid": "623a1f208ea52c030dc331a5"
},
"userId": {
"$oid": "623a1f208ea52c030dc331a3"
},
"items": [
{
"quantity": 2,
"product": {
"$oid": "6205a4565c0caba6fb39cd5d"
},
"size": "3",
"color": "blue1",
"prodImage": "mom-jeans-3.1.png",
"_id": {
"$oid": "625065c69edbfad52ac3afc2"
}
},
{
"quantity": 1,
"product": {
"$oid": "6205a4565c0caba6fb39cd5d"
},
"size": "3",
"color": "blue2",
"prodImage": "mom-jeans-5.1.png",
"_id": {
"$oid": "625065c99edbfad52ac3afce"
}
},
{
"quantity": 1,
"product": {
"$oid": "6205a4565c0caba6fb39cd5d"
},
"size": "3",
"color": "blue3",
"prodImage": "mom-jeans-4.1.png",
"_id": {
"$oid": "625065cc9edbfad52ac3afdc"
}
}
],
"__v": 0
}
You can use array filters to get the desired output:
The userId into find query will filter across all documents, and the array filter will find the object in the array which match your condition.
db.collection.update({
"userId": userId
},
{
"$set": {
"items.$[item].quantity": quantity
}
},
{
"arrayFilters": [
{
"item._id": itemId,
"item.product": productId,
"item.size": size,
"item.color": color
}
]
})
Example here
'$' operator only update the first element found.
You should use $[] operator:
const cart = await Cart.findOneAndUpdate({
userId: userId,
'items._id': itemId,
'items.product': productId,
'items.size': size,
'items.color': color,
}, {
$set: {
'items.$[].quantity': quantity
}
}, {
new: true
}).populate({
path: 'items.product',
model: 'Product'
})
Try it.

Node mongoose embedded document array

I have my sample userSchema here:
{
"_id": objectId("6092076ba811e50b565497ec"),
"username": "test#gmail.com",
"address_book": [{
"_id": objectId("6092b1120f7e370b954a2708"),
"address": "address1",
"address2": "address2",
}, {
"_id": objectId("6093edcb88796b0a5eba19a3"),
"address": "test1",
"address2": "test2",
}]
}
Can I find user by
objectId("6092076ba811e50b565497ec") and address_book._id object("6093edcb88796b0a5eba19a3")
and it return only the address_book that I selected? my expected return data should look like this
{
"_id": objectId("6092076ba811e50b565497ec"),
"username": "test#gmail.com",
"address_book": {
"_id": objectId("6093edcb88796b0a5eba19a3"),
"address": "test1",
"address2": "test2",
}
}
here is my sample function
let user = [];
await User.findOne({
_id: id,
"address_book._id": address_id,
})
.then((result) => {
console.log(result);
user = result;
})
.catch((err) => console.log(err));
return user;
with this I get all address_book
and also can is there and updateOrCreate function by address_book._id?
Thank you in advance.
You can use aggregation operators in find method starting from MongoDB 4.4,
$filter to iterate loop of address_book array and match _id condition
$first to select first element from above filtered result
await User.findOne({
_id: id,
"address_book._id": address_id
},
{
username: 1,
address_book: {
$first: {
$filter: {
input: "$address_book",
cond: { $eq: ["$$this._id", mongoose.Types.ObjectId(address_id)] }
}
}
}
})
Playground
elemMatch is what you are looking for according to me. elemMatch for projection not just the match.
db.<collection name>.find({
< search using elem match >
}, {
games: {
$elemMatch: {
//put your projection piece here, whatever selective what you want, check the example on documentation
score: {
$gt: 5
}
}
},
//anything else that you would want apart from within array projection
})
Update :
Data
[
{
"_id": "6092076ba811e50b565497ec",
"username": "test#gmail.com",
"address_book": [
{
"_id": "6092b1120f7e370b954a2708",
"address": "address1",
"address2": "address2",
},
{
"_id": "6093edcb88796b0a5eba19a3",
"address": "test1",
"address2": "test2",
}
]
}
]
Command
db.collection.find({},
{
address_book: {
$elemMatch: {
address: "test1"
}
}
})
Result
[
{
"_id": "6092076ba811e50b565497ec",
"address_book": [
{
"_id": "6093edcb88796b0a5eba19a3",
"address": "test1",
"address2": "test2"
}
]
}
]
Playground

Mongodb query sub document $gt [duplicate]

This question already has answers here:
Retrieve only the queried element in an object array in MongoDB collection
(18 answers)
Closed 3 years ago.
I had searched other posts, but what seems to work fine just couldn't work here. I need your advice.
Here is what my document looks like in the database, just one document with a series of tag in it.
I need to just query, the restaurant type which has counter greater than 0, (so the end result will exclude any type with counter 0)
My schema
const tagsSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
details: {
restaurantTypeId: mongoose.Schema.Types.ObjectId,
restaurantTypes: [{
_id: mongoose.Schema.Types.ObjectId,
name: String,
counter: Number,
}],
foodTypeId: mongoose.Schema.Types.ObjectId,
foodTypes: [{
_id: mongoose.Schema.Types.ObjectId,
name: String,
counter: Number,
}]
}
});
I have tried
tags.find({
'details.restaurantTypes.counter': {
$gt: 0
}
}, (err, data) => {
if (err) {
res.send(err);
}
res.json(data);
});
and I got
[
{
"details": {
"restaurantTypeId": "5c01fb57497a896d50f49877",
"restaurantTypes": [
{
"_id": "5c01fb57497a896d50f49879",
"name": "Asian",
"counter": 1
},
{
"_id": "5c01fb57497a896d50f4987a",
"name": "Bakery",
"counter": 0
},
{
"_id": "5c01fb57497a896d50f4987b",
"name": "Barbecue",
"counter": 0
},
{
"_id": "5c01fb57497a896d50f4987c",
"name": "Bars & Pubs",
"counter": 0
},
{
"_id": "5c01fb57497a896d50f4987d",
"name": "Bistro",
"counter": 0
},
and
tags.find({
'details.restaurantTypes.counter': {
$gte: 1
}
}, (err, data) => {
if (err) {
res.send(err);
}
res.json(data);
});
which give me the same result
You can use aggregation pipeline to filter the restaurantTypes
$match - filter the restaurant
$addFields - to overwrite restaurantTypes and $filter the restaurant types by counter
aggregated pipeline
db.res.aggregate([
{$match: {"_id" : ObjectId("5c2187be640edfe094a3b946")}},
{$addFields:{"restaurantTypes" : {$filter : {input : "$restaurantTypes", as : "t", cond : {$ne : ["$$t.counter",0]}}}}}
])
Okay, I found the answer, inspired by Saravana.
here is the answer using aggregate and filter.
tags.aggregate([{
$match: {
"_id": mongoose.Types.ObjectId(id)
}
},
{
$project: {
"details.restaurantTypes": {
$filter: {
input: "$details.restaurantTypes",
as: "resType",
cond: {
$ne: ["$$resType.counter", 0]
}
}
}
}
}
]
this will give me the result
[
{
"_id": "5c01fb57497a896d50f49876",
"details": {
"restaurantTypes": [
{
"_id": "5c01fb57497a896d50f49879",
"name": "Asian",
"counter": 1
},
{
"_id": "5c01fb57497a896d50f498a6",
"name": "Thai",
"counter": 1
},
{
"_id": "5c01fb57497a896d50f498a8",
"name": "Western",
"counter": 1
}
]
}
}
]

Mongoose populate single item in array

I have a model that has an array of dynamic references.
var postSchema = new Schema({
name: String,
targets: [{
kind: String,
item: { type: ObjectId, refPath: 'targets.kind' }
}]
});
I am using the targets property to store references to multiple different models, users, thread, attachments, etc.
Is it possible to populate only the references that I want?
Post.find({}).populate({
// Does not work
// match: { 'targets.kind': 'Thread' }, // I want to populate only the references that match. ex: Thread, User, Attachment
path: 'targets.item',
model: 'targets.kind',
select: '_id title',
});
Thanks
The one big lesson here should be that mongoose.set('debug', true) is your new "best friend". This will show the actual queries issued to MongoDB from the code you are writing, and it's very important because when you actually "see it", then it clears up any misconceptions you likely have.
The Logic Problem
Let's demonstrate why exactly what you are attempting fails:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/polypop';
mongoose.set('debug', true);
mongoose.Promise = global.Promise;
const postSchema = new Schema({
name: String,
targets: [{
kind: String,
item: { type: Schema.Types.ObjectId, refPath: 'targets.kind' }
}]
});
const fooSchema = new Schema({
name: String
})
const barSchema = new Schema({
number: Number
});
const Post = mongoose.model('Post', postSchema);
const Foo = mongoose.model('Foo', fooSchema);
const Bar = mongoose.model('Bar', barSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, { useNewUrlParser: true });
// Clean all data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Create some things
let [foo, bar] = await Promise.all(
[{ _t: 'Foo', name: 'Bill' }, { _t: 'Bar', number: 1 }]
.map(({ _t, ...d }) => mongoose.model(_t).create(d))
);
log([foo, bar]);
// Add a Post
let post = await Post.create({
name: 'My Post',
targets: [{ kind: 'Foo', item: foo }, { kind: 'Bar', item: bar }]
});
log(post);
let found = await Post.findOne();
log(found);
let result = await Post.findOne()
.populate({
match: { 'targets.kind': 'Foo' }, // here is the problem!
path: 'targets.item',
});
log(result);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
So the comment there show the match is the problem with the logic, so let's look at the debug output and see why:
Mongoose: posts.deleteMany({}, {})
Mongoose: foos.deleteMany({}, {})
Mongoose: bars.deleteMany({}, {})
Mongoose: foos.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a0"), name: 'Bill', __v: 0 })
Mongoose: bars.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a1"), number: 1, __v: 0 })
[
{
"_id": "5bdbc70996ed8e3295b384a0",
"name": "Bill",
"__v": 0
},
{
"_id": "5bdbc70996ed8e3295b384a1",
"number": 1,
"__v": 0
}
]
Mongoose: posts.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a2"), name: 'My Post', targets: [ { _id: ObjectId("5bdbc70996ed8e3295b384a4"), kind: 'Foo', item: ObjectId("5bdbc70996ed8e3295b384a0") }, { _id: ObjectId("5bdbc70996ed8e3295b384a3"), kind: 'Bar', item: ObjectId("5bdbc70996ed8e3295b384a1") } ], __v: 0 })
{
"_id": "5bdbc70996ed8e3295b384a2",
"name": "My Post",
"targets": [
{
"_id": "5bdbc70996ed8e3295b384a4",
"kind": "Foo",
"item": {
"_id": "5bdbc70996ed8e3295b384a0",
"name": "Bill",
"__v": 0
}
},
{
"_id": "5bdbc70996ed8e3295b384a3",
"kind": "Bar",
"item": {
"_id": "5bdbc70996ed8e3295b384a1",
"number": 1,
"__v": 0
}
}
],
"__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
{
"_id": "5bdbc70996ed8e3295b384a2",
"name": "My Post",
"targets": [
{
"_id": "5bdbc70996ed8e3295b384a4",
"kind": "Foo",
"item": "5bdbc70996ed8e3295b384a0"
},
{
"_id": "5bdbc70996ed8e3295b384a3",
"kind": "Bar",
"item": "5bdbc70996ed8e3295b384a1"
}
],
"__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
Mongoose: bars.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a1") ] } }, { projection: {} })
Mongoose: foos.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a0") ] } }, { projection: {} })
{
"_id": "5bdbc70996ed8e3295b384a2",
"name": "My Post",
"targets": [
{
"_id": "5bdbc70996ed8e3295b384a4",
"kind": "Foo",
"item": null
},
{
"_id": "5bdbc70996ed8e3295b384a3",
"kind": "Bar",
"item": null
}
],
"__v": 0
}
That's the full output to show that everything else is actually working, and in fact without the match you would get the populated data back for the items. But take a close look at the two queries being issued to the foo and bar collections:
Mongoose: bars.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a1") ] } }, { projection: {} })
Mongoose: foos.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a0") ] } }, { projection: {} })
So the 'targets.kind' that you include under match is actually being searched for on the foo and bar collections, and not in the posts collection as you seem to be expecting. Along with the rest of the output this should be giving you an idea of how populate() actually works, in that nothing ever says to specifically just return the "array entries" which are of kind: 'Foo' as the example goes.
This process of "filtering the array" actually isn't "really" even a natural MongoDB query, and with the exception of the "first and singular match" you actually would typically use .aggregate() and the $filter operator. You can get "singular" via the positional $ operator but if you wanted "all foos" where there was more than one, then it needs the $filter instead.
So the real core issue here is populate() is actually the wrong place and wrong operation to "filter the array". Instead you really want to "smartly" return only the array entries you want before you go doing anything else to "populate" the items.
Structural Problem
Noting from the listing above which is an allegory for what is hinted at in the question, there are "multiple models" being referred to in order to "join" and obtain the overall result. Whilst this may seem logical in "RDBMS land", it's certainly not the case nor practical or efficient to do so with MongoDB and the general "ilk" of "document databases".
The key thing to remember here is that "documents" in a "collection" need not all have the same "table structure" as you would with an RDBMS. The structure can vary, and whilst it's probably advisable to not "vary wildly", it's certainly very valid to store "polymorphic objects" within a single collection. Afterall, you actually want to reference all of these things back to the same parent, so why would they need to be in different collections? Simply put, they don't need to be at all:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/polypop';
mongoose.set('debug', true);
mongoose.Promise = global.Promise;
const postSchema = new Schema({
name: String,
targets: [{
kind: String,
item: { type: Schema.Types.ObjectId, ref: 'Target' }
}]
});
const targetSchema = new Schema({});
const fooSchema = new Schema({
name: String
});
const barSchema = new Schema({
number: Number
});
const bazSchema = new Schema({
title: String
});
const log = data => console.log(JSON.stringify(data, undefined, 2));
const Post = mongoose.model('Post', postSchema);
const Target = mongoose.model('Target', targetSchema);
const Foo = Target.discriminator('Foo', fooSchema);
const Bar = Target.discriminator('Bar', barSchema);
const Baz = Target.discriminator('Baz', bazSchema);
(async function() {
try {
const conn = await mongoose.connect(uri,{ useNewUrlParser: true });
// Clean data - bit hacky but just a demo
await Promise.all(
Object.entries(conn.models).map(([k, m]) => m.deleteMany() )
);
// Insert some things
let [foo1, bar, baz, foo2] = await Promise.all(
[
{ _t: 'Foo', name: 'Bill' },
{ _t: 'Bar', number: 1 },
{ _t: 'Baz', title: 'Title' },
{ _t: 'Foo', name: 'Ted' }
].map(({ _t, ...d }) => mongoose.model(_t).create(d))
);
log([foo1, bar, baz, foo2]);
// Add a Post
let post = await Post.create({
name: 'My Post',
targets: [
{ kind: 'Foo', item: foo1 },
{ kind: 'Bar', item: bar },
{ kind: 'Baz', item: baz },
{ kind: 'Foo', item: foo2 }
]
});
log(post);
let found = await Post.findOne();
log(found);
let result1 = await Post.findOne()
.populate({
path: 'targets.item',
match: { __t: 'Foo' }
});
log(result1);
let result2 = await Post.aggregate([
// Only get documents with a matching entry
{ "$match": {
"targets.kind": "Foo"
}},
// Optionally filter the array
{ "$addFields": {
"targets": {
"$filter": {
"input": "$targets",
"cond": {
"$eq": [ "$$this.kind", "Foo" ]
}
}
}
}},
// Lookup from single source
{ "$lookup": {
"from": Target.collection.name,
"localField": "targets.item",
"foreignField": "_id",
"as": "matches"
}},
// Marry up arrays
{ "$project": {
"name": 1,
"targets": {
"$map": {
"input": "$targets",
"in": {
"kind": "$$this.kind",
"item": {
"$arrayElemAt": [
"$matches",
{ "$indexOfArray": [ "$matches._id", "$$this.item" ] }
]
}
}
}
}
}}
]);
log(result2);
let result3 = await Post.aggregate([
// Only get documents with a matching entry
{ "$match": {
"targets.kind": "Foo"
}},
// Optionally filter the array
{ "$addFields": {
"targets": {
"$filter": {
"input": "$targets",
"cond": {
"$eq": [ "$$this.kind", "Foo" ]
}
}
}
}},
// Lookup from single source with overkill of type check
{ "$lookup": {
"from": Target.collection.name,
"let": { "targets": "$targets" },
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$targets.item" ]
},
"__t": "Foo"
}}
],
"as": "matches"
}},
// Marry up arrays
{ "$project": {
"name": 1,
"targets": {
"$map": {
"input": "$targets",
"in": {
"kind": "$$this.kind",
"item": {
"$arrayElemAt": [
"$matches",
{ "$indexOfArray": [ "$matches._id", "$$this.item" ] }
]
}
}
}
}
}}
]);
console.log(result3);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
That's a bit longer and has a few more concepts to get around, but the basic principle is that instead of using "multiple collections" for the different types we're only going to use one. The "mongoose" method for this uses "discriminators" in the model setup which is all relevant to this part of the code:
const Post = mongoose.model('Post', postSchema);
const Target = mongoose.model('Target', targetSchema);
const Foo = Target.discriminator('Foo', fooSchema);
const Bar = Target.discriminator('Bar', barSchema);
const Baz = Target.discriminator('Baz', bazSchema);
Which is really simply calling .discriminator() from a "base model" for the "singular" collection rather than calling mongoose.model(). The really good thing about this is as far as the rest of your code is concerned, Baz and Bar etc are all just treated like a "model" transparently, but they are actually doing something really cool underneath.
So all of these "related things" ( they really are even if you don't think so yet ) are all actually kept in the same collection, but operations using the individual models take into account an "automatic" kind key. This is __t by default, but you can actually specify whatever you want in options.
The fact that these are all actually in the same collection though is really important, since you can basically readily query the same collection for the different types of data. Put simply:
Foo.find({})
Would actually call
targets.find({ __t: 'Foo' })
And does this automatically. But more importantly
Target.find({ __t: { "$in": [ 'Foo', 'Baz' ] } })
Would be returning all the expected results from a "single collection" with a "single request".
So taking a look at the revised populate() under this structure:
let result1 = await Post.findOne()
.populate({
path: 'targets.item',
match: { __t: 'Foo' }
});
log(result1);
This shows instead in the logs:
Mongoose: posts.findOne({}, { projection: {} })
Mongoose: targets.find({ __t: 'Foo', _id: { '$in': [ ObjectId("5bdbe2895b1b843fba050569"), ObjectId("5bdbe2895b1b843fba05056a"), ObjectId("5bdbe2895b1b843fba05056b"), ObjectId("5bdbe2895b1b843fba05056c") ] } }, { projection: {} })
Note how even though all "four" of the related ObjectId values are sent with the request the additional constraint of __t: 'Foo' also binds which document are actually returned and married up. The result then becomes self evident as only the 'Foo' entries populated. But also note the "catch":
{
"_id": "5bdbe2895b1b843fba05056d",
"name": "My Post",
"targets": [
{
"_id": "5bdbe2895b1b843fba050571",
"kind": "Foo",
"item": {
"__t": "Foo",
"_id": "5bdbe2895b1b843fba050569",
"name": "Bill",
"__v": 0
}
},
{
"_id": "5bdbe2895b1b843fba050570",
"kind": "Bar",
"item": null
},
{
"_id": "5bdbe2895b1b843fba05056f",
"kind": "Baz",
"item": null
},
{
"_id": "5bdbe2895b1b843fba05056e",
"kind": "Foo",
"item": {
"__t": "Foo",
"_id": "5bdbe2895b1b843fba05056c",
"name": "Ted",
"__v": 0
}
}
],
"__v": 0
}
Filtering after Populate
This is actually a longer topic and more fully answered elsewhere, but the basics here as shown in the output above is that populate() really still does absolutely nothing about actually "filtering" the results in the array to only the desired matches.
The other thing is that populate() really isn't that great an idea from a "performance" perspective, since what is really happening is "another query" ( in our second form we optimized to just one more ) or possibly "many queries" depending on your structure are actually being issued to the database and the results are being reconstructed together on the client.
Overall, you end up returning a lot more data than you actually need and at best you are relying on manual client side filtering in order to discard those unwanted results. So the "ideal" position is to have the "server" do that sort of thing instead, and only return the data you actually need.
The populate() method was added as a "convenience" to the mongoose API a "very" long time ago. Since then MongoDB has moved on and now sports $lookup as a "native" way for performing a "join" on the server with a single request.
There are different ways to do this but just touching on "two" closely related to the existing populate() functionality but with improvements:
let result2 = await Post.aggregate([
// Only get documents with a matching entry
{ "$match": {
"targets.kind": "Foo"
}},
// Optionally filter the array
{ "$addFields": {
"targets": {
"$filter": {
"input": "$targets",
"cond": {
"$eq": [ "$$this.kind", "Foo" ]
}
}
}
}},
// Lookup from single source
{ "$lookup": {
"from": Target.collection.name,
"localField": "targets.item",
"foreignField": "_id",
"as": "matches"
}},
// Marry up arrays
{ "$project": {
"name": 1,
"targets": {
"$map": {
"input": "$targets",
"in": {
"kind": "$$this.kind",
"item": {
"$arrayElemAt": [
"$matches",
{ "$indexOfArray": [ "$matches._id", "$$this.item" ] }
]
}
}
}
}
}}
]);
log(result2);
The two basic "optimizations" there are using $filter in order to "pre-discard" items from the array which do not actually match the type we want. This can be totally optional as covered with a bit more detail later, but where possible then it's probably a good thing to do since we won't even be looking for matching _id values in the foreign collection for anything but 'Foo' things.
The other of course is the $lookup itself, which means instead of a separate round trip to the server we actually just make one and the "join" is done before any response is returned. Here we just look for the matching _id values in the foreign collection to the target.items array entry values. We already filtered those for 'Foo', so that is all that gets returned:
{
"_id": "5bdbe6aa2c4a2240c16802e2",
"name": "My Post",
"targets": [
{
"kind": "Foo",
"item": {
"_id": "5bdbe6aa2c4a2240c16802de",
"__t": "Foo",
"name": "Bill",
"__v": 0
}
},
{
"kind": "Foo",
"item": {
"_id": "5bdbe6aa2c4a2240c16802e1",
"__t": "Foo",
"name": "Ted",
"__v": 0
}
}
]
}
For a "slight" variation on that we can actually even inspect the __t value within the $lookup expression using "sub-pipeline" processing with MongoDB 3.6 and greater. The main use case here would be if you choose to remove the kind from the parent Post altogether and simply rely on the "kind" information inherent to discriminator references used in storage:
let result3 = await Post.aggregate([
// Only get documnents with a matching entry
{ "$match": {
"targets.kind": "Foo"
}},
// Optionally filter the array
{ "$addFields": {
"targets": {
"$filter": {
"input": "$targets",
"cond": {
"$eq": [ "$$this.kind", "Foo" ]
}
}
}
}},
// Lookup from single source with overkill of type check
{ "$lookup": {
"from": Target.collection.name,
"let": { "targets": "$targets" },
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$targets.item" ]
},
"__t": "Foo"
}}
],
"as": "matches"
}},
// Marry up arrays
{ "$project": {
"name": 1,
"targets": {
"$map": {
"input": "$targets",
"in": {
"kind": "$$this.kind",
"item": {
"$arrayElemAt": [
"$matches",
{ "$indexOfArray": [ "$matches._id", "$$this.item" ] }
]
}
}
}
}
}}
]);
log(result3);
This has the same "filtered" results and is similarly an "single request" and "single response".
The whole topic gets a bit wider, and even though aggregation pipelines may appear considerably more unwieldy than a simple populate() call, it's fairly trivial to write a wrapper which can abstract from your models and pretty much generate most of the data structure code required. You can see an overview of this in action at "Querying after populate in Mongoose", which in essence is the same question you are basically asking here once we sort out the initial issue of "multiple collection joins" and why you really don't need them.
The over caveat here is that $lookup actually has no way possible to "dynamically" determine which collection to "join" to. You need to include that information statically just as is done here, so this is another reason to actually favor "discriminators" over using multiple collections. It's not only "better performance", but it's actually the only way the most performant options will actually support what you are trying to do.
For reference, the "complete" (truncated due to max post length) output of the second listing would be:
Mongoose: posts.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba050569"), __t: 'Foo', name: 'Bill', __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056a"), __t: 'Bar', number: 1, __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056b"), __t: 'Baz', title: 'Title', __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056c"), __t: 'Foo', name: 'Ted', __v: 0 })
[
{
"_id": "5bdbe2895b1b843fba050569",
"__t": "Foo",
"name": "Bill",
"__v": 0
},
{
"_id": "5bdbe2895b1b843fba05056a",
"__t": "Bar",
"number": 1,
"__v": 0
},
{
"_id": "5bdbe2895b1b843fba05056b",
"__t": "Baz",
"title": "Title",
"__v": 0
},
{
"_id": "5bdbe2895b1b843fba05056c",
"__t": "Foo",
"name": "Ted",
"__v": 0
}
]
Mongoose: posts.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056d"), name: 'My Post', targets: [ { _id: ObjectId("5bdbe2895b1b843fba050571"), kind: 'Foo', item: ObjectId("5bdbe2895b1b843fba050569") }, { _id: ObjectId("5bdbe2895b1b843fba050570"), kind: 'Bar', item: ObjectId("5bdbe2895b1b843fba05056a") }, { _id: ObjectId("5bdbe2895b1b843fba05056f"), kind: 'Baz', item: ObjectId("5bdbe2895b1b843fba05056b") }, { _id: ObjectId("5bdbe2895b1b843fba05056e"), kind: 'Foo', item: ObjectId("5bdbe2895b1b843fba05056c") } ], __v: 0 })
{
"_id": "5bdbe2895b1b843fba05056d",
"name": "My Post",
"targets": [
{
"_id": "5bdbe2895b1b843fba050571",
"kind": "Foo",
"item": {
"_id": "5bdbe2895b1b843fba050569",
"__t": "Foo",
"name": "Bill",
"__v": 0
}
},
{
"_id": "5bdbe2895b1b843fba050570",
"kind": "Bar",
"item": {
"_id": "5bdbe2895b1b843fba05056a",
"__t": "Bar",
"number": 1,
"__v": 0
}
},
{
"_id": "5bdbe2895b1b843fba05056f",
"kind": "Baz",
"item": {
"_id": "5bdbe2895b1b843fba05056b",
"__t": "Baz",
"title": "Title",
"__v": 0
}
},
{
"_id": "5bdbe2895b1b843fba05056e",
"kind": "Foo",
"item": {
"_id": "5bdbe2895b1b843fba05056c",
"__t": "Foo",
"name": "Ted",
"__v": 0
}
}
],
"__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
{
"_id": "5bdbe2895b1b843fba05056d",
"name": "My Post",
"targets": [
{
"_id": "5bdbe2895b1b843fba050571",
"kind": "Foo",
"item": "5bdbe2895b1b843fba050569"
},
{
"_id": "5bdbe2895b1b843fba050570",
"kind": "Bar",
"item": "5bdbe2895b1b843fba05056a"
},
{
"_id": "5bdbe2895b1b843fba05056f",
"kind": "Baz",
"item": "5bdbe2895b1b843fba05056b"
},
{
"_id": "5bdbe2895b1b843fba05056e",
"kind": "Foo",
"item": "5bdbe2895b1b843fba05056c"
}
],
"__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
Mongoose: targets.find({ __t: 'Foo', _id: { '$in': [ ObjectId("5bdbe2895b1b843fba050569"), ObjectId("5bdbe2895b1b843fba05056a"), ObjectId("5bdbe2895b1b843fba05056b"), ObjectId("5bdbe2895b1b843fba05056c") ] } }, { projection: {} })
{
"_id": "5bdbe2895b1b843fba05056d",
"name": "My Post",
"targets": [
{
"_id": "5bdbe2895b1b843fba050571",
"kind": "Foo",
"item": {
"__t": "Foo",
"_id": "5bdbe2895b1b843fba050569",
"name": "Bill",
"__v": 0
}
},
{
"_id": "5bdbe2895b1b843fba050570",
"kind": "Bar",
"item": null
},
{
"_id": "5bdbe2895b1b843fba05056f",
"kind": "Baz",
"item": null
},
{
"_id": "5bdbe2895b1b843fba05056e",
"kind": "Foo",
"item": {
"__t": "Foo",
"_id": "5bdbe2895b1b843fba05056c",
"name": "Ted",
"__v": 0
}
}
],
"__v": 0
}
Mongoose: posts.aggregate([ { '$match': { 'targets.kind': 'Foo' } }, { '$addFields': { targets: { '$filter': { input: '$targets', cond: { '$eq': [ '$$this.kind', 'Foo' ] } } } } }, { '$lookup': { from: 'targets', localField: 'targets.item', foreignField: '_id', as: 'matches' } }, { '$project': { name: 1, targets: { '$map': { input: '$targets', in: { kind: '$$this.kind', item: { '$arrayElemAt': [ '$matches', { '$indexOfArray': [ '$matches._id', '$$this.item' ] } ] } } } } } } ], {})
[
{
"_id": "5bdbe2895b1b843fba05056d",
"name": "My Post",
"targets": [
{
"kind": "Foo",
"item": {
"_id": "5bdbe2895b1b843fba050569",
"__t": "Foo",
"name": "Bill",
"__v": 0
}
},
{
"kind": "Foo",
"item": {
"_id": "5bdbe2895b1b843fba05056c",
"__t": "Foo",
"name": "Ted",
"__v": 0
}
}
]
}
]
Mongoose: posts.aggregate([ { '$match': { 'targets.kind': 'Foo' } }, { '$addFields': { targets: { '$filter': { input: '$targets', cond: { '$eq': [ '$$this.kind', 'Foo' ] } } } } }, { '$lookup': { from: 'targets', let: { targets: '$targets' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$targets.item' ] }, __t: 'Foo' } } ], as: 'matches' } }, { '$project': { name: 1, targets: { '$map': { input: '$targets', in: { kind: '$$this.kind', item: { '$arrayElemAt': [ '$matches', { '$indexOfArray': [ '$matches._id', '$$this.item' ] } ] } } } } } } ], {})

How to get subdocuments of multiple documents in mongoose

I have a Module called Organisation with an array calls users in it that contains UserSchema objects. Now i need a query to get all users from all organisation documents in one array.
As you can see I am a beginner in mongodb and normaly use sql
But without joins I donĀ“t know what to do.
OrganisationModule:
const OrganisationSchema = new Schema({
name: { type: String, required: true },
users: [UserSchema],
version: String,
});
module.exports.Organisation = mongoose.model('Organisation', OrganisationSchema);
UserSchema:
module.exports.UserSchema = new Schema({
name: String,
roles: [String]
})
My first try:
routes.get('/', (req, res, next) => {
Organisation.find().populate('users').exec((err, users) => {
if (err) res.json(err.message)
else { res.json(users) }
});
The result:
[
{
"users": [
{
"roles": [ "coordinator" ],
"_id": "5aafcf80dd248f7ef86e0512",
"name": "Peter"
"__v": 0
}
],
"_id": "5aafcf80dd248f7ef86e05cf",
"name": "DEFAULT",
"__v": 1
},
{
"users": [
{
"roles": [ "admin", "coordinator" ],
"_id": "5aafcf80dd248f7ef86e0500",
"name": "Max"
"__v": 0
}
],
"_id": "5aafcf80dd248f7ef86e05ce",
"name": "Organisation_01",
"__v": 1
}
]
What I need:
[
{
"roles": [ "coordinator" ],
"_id": "5aafcf80dd248f7ef86e0512",
"name": "Peter"
"__v": 0
},
{
"roles": [ "admin", "coordinator" ],
"_id": "5aafcf80dd248f7ef86e0500",
"name": "Max"
"__v": 0
}
]
This
Organization.find(
{},
{_id: 0, users: 1}
)
Will return
[
{
users: {
roles: ['coordinator'],
_id: '5aafcf80dd248f7ef86e0512',
name: 'Peter',
....
},
},
{
users: {
roles: ['admin', 'coordinator'],
_id: '5aafcf80dd248f7ef86e0500',
name: 'Max',
....
},
},
];
This is not precisely what you want, but it is what I have found that most closely matches your need.
You can find more information here :
https://stackoverflow.com/a/42558955/11120444
https://stackoverflow.com/a/9601614/11120444
Else, there is an other approach
// Meaby you will need to add await
const users = Organization.find({}).map((item: any) => item.users);
In mongodb you can use $lookup to perform that operation.
Please study $lookup here: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/
In mongoose you can use populate()
For example:
Organization.find().populate('users')
Study Mongoose's Populate here: http://mongoosejs.com/docs/populate.html