Mongodb $lookup dynamic collection - node.js

I have following schema where the item type might vary, and is mentioned in connections.kind.
var userSchema = new Schema({
name: String,
connections: [{
kind: String,
item: { type: ObjectId, refPath: 'connections.kind' }
}]
});
var organizationSchema = new Schema({ name: String });
I am trying to do a dynamic lookup so that the item object is populated. But this doesn't seem to work.
db.users.aggregate([
{
$lookup:{
from: '$connections.kind',
localField: 'connections.item',
foreignField: '_id',
as: 'items'
}
}
])
I know I can do it with mongoose.populate, but want to know if it is possible with $lookup

As of now, you can't. The from field cannot be an expression and must be a string literal. However, there is an open issue which you can track here that appears to be exactly what you need: https://jira.mongodb.org/browse/SERVER-22497.

Related

mongoose - I want to find a documents that contain a nested reference?

I'm new to mongoose and MongoDB and I have a question,
I want to find documents that contain a reference of reference.
I have 2 models like this:
1: Posts with ref to categories:
const postsSchema = new Schema({
post_title: {
type: String,
required:true
},
post_categories: [{
type: Schema.Types.ObjectId,
ref: 'categories',
required:true
}],
});
2: categories with ref to categories
const categoriesSchema = new Schema({
categoryName: {
type: String,
required: true
},
categoryParent: {
type: Schema.Types.ObjectId,
ref: 'categoriesSchema',
}
});
I want to find all posts which have a category parent for example (news), I try this:
Posts.find({'post_categories.categoryParent.categoryName': 'news'});
but I got an empty array [].
Is there a way to find documents that contain a reference of reference?
I found the solution by using MongoDB aggregate, I don't know if it optimal solution, but it solved my issue:
var allnews = await postsDB.aggregate([
{$unwind: "$post_categories"},
{$lookup: {
from: "categories",
localField: "post_categories",
foreignField: "_id",
as: "newCategoryName"
}},
{ $unwind: "$newCategoryName"
},
{$lookup: {
from: "categories",
localField: "newCategoryName.categoryParent",
foreignField: "_id",
as: "newCategoryParent"
}},
{ $unwind: "$newCategoryParent"
},
{
$match: {
"newCategoryParent.categoryName": "News"
}}
]);
Now I have all posts which have the parent category is "News" whatever child category is

How to perform FIND, then perform AND, then perform OR with POPULATE in Mongoose

I have a query that I wish to perform something similar as:
Archive does not exists AND
Owner Email in schema is in Query OR
Owner Email in Populated's schema is in Query
Below is what I have tried as of my understanding
let docs = await Document.find({ archive: { $exists: false }})
.and([{ owner_email: { $regex: localQuery } }])
.or()
.populate('owner_id', null, {
email: { $regex: localQuery },
});
So what I wish to do is, I have two schema, the user and the documents, User sometimes is shared along [as a librarian], then I wish to return, both, which matches the populated email or the actual owner's email.
As mongoose's populate() method does not really "join" collections and rather makes another query to the database to populate after the find() operation, you can switch to an aggregation pipeline and use $lookup in order to match the email in the referenced field. So assuming your models look like:
const Document = mongoose.model('Document', {
name: String,
archive: String,
owner_email: String,
owner: {type: Schema.Types.ObjectId, ref: 'Person'}
});
const Person = mongoose.model('Person', {
firstName: String,
lastName: String,
email: String
});
Then, you can do:
const result = await Document.aggregate([
{
$lookup: {
from: Person.collection.name,
localField: "owner",
foreignField: "_id",
as: "referencedOwner"
}
},
{
$match: {
archive: {$exists: false},
$or: [
{"referencedOwner.email": {$regex: localQuery}},
{"owner_email": {$regex: localQuery}}]
}
}
]);
Here's a working example on mongoplayground: https://mongoplayground.net/p/NqAvKIgujbm

Mongoose populate ObjectID from multiple possible collections

I have a mongoose model that looks something like this
var LogSchema = new Schema({
item: {
type: ObjectId,
ref: 'article',
index:true,
},
});
But 'item' could be referenced from multiple collections. Is it possible to do something like this?
var LogSchema = new Schema({
item: {
type: ObjectId,
ref: ['article','image'],
index:true,
},
});
The idea being that 'item' could be a document from the 'article' collection OR the 'image' collection.
Is this possible or do i need to manually populate?
Question is old, but maybe someone else still looks for similar issues :)
I found in Mongoose Github issues this:
mongoose 4.x supports using refPath instead of ref:
var schema = new Schema({
name:String,
others: [{ value: {type:mongoose.Types.ObjectId, refPath: 'others.kind' } }, kind: String }]
})
In #CadeEmbery case it would be:
var logSchema = new Schema({
item: {type: mongoose.Types.ObjectId, refPath: 'kind' } },
kind: String
})
But I did't try it yet...
First of all some basics
The ref option says mongoose which collection to get data for when you use populate().
The ref option is not mandatory, when you do not set it up, populate() require you to give dynamically a ref to him using the model option.
#example
populate({ path: 'conversation', model: Conversation }).
Here you say to mongoose that the collection behind the ObjectId is Conversation.
It is not possible to gives populate or Schema an array of refs.
Some others Stackoverflow people asked about it.
Soluce 1: Populate both (Manual)
Try to populate one, if you have no data, populate the second.
Soluce 2: Change your schema
Create two link, and set one of them.
var LogSchema = new Schema({
itemLink1: {
type: ObjectId,
ref: 'image',
index: true,
},
itemLink2: {
type: ObjectId,
ref: 'article',
index: true,
},
});
LogSchema.find({})
.populate('itemLink1')
.populate('itemLink2')
.exec()
Dynamic References via refPath
Mongoose can also populate from multiple collections based on the value of a property in the document. Let's say you're building a schema for storing comments. A user may comment on either a blog post or a product.
body: { type: String, required: true },
on: {
type: Schema.Types.ObjectId,
required: true,
// Instead of a hardcoded model name in `ref`, `refPath` means Mongoose
// will look at the `onModel` property to find the right model.
refPath: 'onModel'
},
onModel: {
type: String,
required: true,
enum: ['BlogPost', 'Product']
}
});
const Product = mongoose.model('Product', new Schema({ name: String }));
const BlogPost = mongoose.model('BlogPost', new Schema({ title: String }));
const Comment = mongoose.model('Comment', commentSchema);

How can I save multiple items to an array field when creating a new record in MongoDB?

I have the following schema that is used for a "Groups" collection. I want to be able to create this record and push an arbitrary number of "members" to this group when it is first created. I am unable to get the "members" field to populate when I save the record. All other fields are saved without a problem.
var groupSchema = mongoose.Schema({
creator : String,
name : String,
members: [{
type: String,
ref: 'User'
}],
created : {
type: Date,
default: Date.now
}
});
app.post('/create-group', function(req, res) {
//req.body.users = ['12345', '23456', '34567'] for example
var group = new Group({
name : req.body.group_name,
creator : req.user._id,
members: {$push: req.body.users}
});
group.save(function (err, data) {
if (!err) {
return res.json(data);
}
});
});
No results are ever stored in "members", even though all other fields are saved correctly. What am I doing wrong?
EDIT
In your schema when you wrote ref :"User" it means that you have to provide a User schema and not a string or integer. If you just want an array of string you can simply use [String].
According to the doc you have to use an objectID http://mongoosejs.com/docs/populate.html
members: [{ type: Schema.Types.ObjectId, ref: 'User' }]
In the link provided above you will be able to check how to save your group and adding an arbitrary number of members.
members: [{ type: Schema.Types.ObjectId, ref: 'User' }]
As Su4p has pointed out. Don't worry about req.body.users containing strings instead of ObjectIds. mongoose will cast the strings into objectIDs.
There's also another mistake,
members: {$push: req.body.users}
should be
members: req.body.users
$push is an update operator. It's not meant for assigning arrays.

Populate a mongoose model with a field that isn't an id

Is it possible to populate a mongoose model with a field of a reference model that isn't the _id ... e.g. a username.
so something like
var personSchema = Schema({
_id : Number,
name : String,
age : Number,
stories : { type: String, field: "username", ref: 'Story' }
});
This is supported since Mongoose 4.5, and is called virtuals population.
You have to define your foreign keys relationships after your schemas definitions and before creating models, like this:
// Schema definitions
BookSchema = new mongoose.Schema({
...,
title: String,
authorId: Number,
...
},
// schema options: Don't forget this option
// if you declare foreign keys for this schema afterwards.
{
toObject: {virtuals:true},
// use if your results might be retrieved as JSON
// see http://stackoverflow.com/q/13133911/488666
//toJSON: {virtuals:true}
});
PersonSchema = new mongoose.Schema({id: Number, ...});
// Foreign keys definitions
BookSchema.virtual('author', {
ref: 'Person',
localField: 'authorId',
foreignField: 'id',
justOne: true // for many-to-1 relationships
});
// Models creation
var Book = mongoose.model('Book', BookSchema);
var Person = mongoose.model('Person', PersonSchema);
// Querying
Book.find({...})
// if you use select() be sure to include the foreign key field !
.select({.... authorId ....})
// use the 'virtual population' name
.populate('author')
.exec(function(err, books) {...})
It seems they enforce to use _id, and maybe we can customize it in the future.
Here is the issue on Github https://github.com/LearnBoost/mongoose/issues/2562
This is an example of using the $lookup aggregate to populate a model called Invite with the respective User based on the corresponding email field:
Invite.aggregate(
{ $match: {interview: req.params.interview}},
{ $lookup: {from: 'users', localField: 'email', foreignField: 'email', as: 'user'} }
).exec( function (err, invites) {
if (err) {
next(err);
}
res.json(invites);
}
);
It's probably quite similar to what you're trying to do.
To add to Frosty's answer, if you're looking to refer to an array of documents of another collection you would make changes like so.
BookSchema = new mongoose.Schema(
{
title: String,
authorId: [Number],
},
// schema options: Don't forget this option
// if you declare foreign keys for this schema afterwards.
{
toObject: { virtuals: true },
// use if your results might be retrieved as JSON
// see http://stackoverflow.com/q/13133911/488666
toJSON: {virtuals:true}
});
PersonSchema = new mongoose.Schema({ id: Number, name: String });
BookSchema.virtual("author", {
ref: "Person",
localField: ["authorId"],
foreignField: ["id"],
// justOne: true, // Needs to be commented out in this scenario,
});
You may use the populate() API.
The API is more flexible, you don't have to specify ref and field in the Schema.
http://mongoosejs.com/docs/api.html#document_Document-populate
http://mongoosejs.com/docs/api.html#model_Model.populate
You can mix and match with find().

Resources