Mongodb Many to Many relationship - node.js

I am working on a webapp built on mean.js and part of the app is projects that have tags, similar to a blog site that has tags for each blog entry.
I have searched everywhere for a good example/tutorial explaining the best way to create a many to many relationship using mongodb/mongoose, but I can't seem to find anything.
Each project can have multiple tags and I want the users to be able to find all projects with a specific tag.
I would really appreciate any suggestions/examples on the correct way to achieve this. TIA

Keep an array of id's in both collections. For example:
coll1:
{
_id: ObjectId("56784ac717e12e59d600688a"),
coll2_ids: [ObjectId("..."), ObjectId("..."), ObjectId("..."), ...]
}
coll2:
{
_id: ObjectId("56784ac717e12e59d600677b"),
coll1_ids: [ObjectId("..."), ObjectId("..."), ObjectId("..."), ...]
}
The advantage of this approach is that when you have a document from either collection, you can immediately pull all associated documents from the opposite collection simply by doing
obj = db.coll1.findOne({})
db.coll2.find({_id: {$in: obj['coll2_ids']}}) # returns all associated documents
and vice-versa.

For many-to-many relationship, I have found mongoose-relationship really useful.
Here's a sample how you would solve it using mongoose-relationship module:
// In TagsModel.js file ----------------------------------------------
var mongoose = require('mongoose');
var schema = mongoose.Schema;
var relationship = require("mongoose-relationship");
var tagSchema = new schema({
projects: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Project',
childPath: 'tags'
}],
....
});
tagSchema.plugin(relationship, {
relationshipPathName: 'projects'
});
var tagModel = mongoose.model('Tag', tagSchema);
--------------------------------------------------------------------
// In ProjectModel.js file -----------------------------------------
var mongoose = require('mongoose');
var schema = mongoose.Schema;
var Projects = new Schema({
tags : [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Tag'
}],
...
});
var tagModel = mongoose.model('Project', tagSchema);
With this model structure, you will be able to find projects by specific tag and tags by project.

It seems you just want to have an Array of tags within your Project schema.
var Tags = new Schema({
name : String
});
var Projects = new Schema({
tags : [String] // When you save a Tag just save the Name of it here
});
// This way you could easily look it up
// Assuming "name" was passed in as a param to express
Projects.find({ 'tags' : req.params.name }) // voila!
There are other ways as well (such as saving ObjectIds).
This method would be easier if you think Tag names could change often but once again if a Tag is "deleted" you'd have to go look through all Projects to remove that ObjectId
tags : [{ type: Schema.ObjectId, ref : 'Tags' }]
Basically the gist of this is that you are saving the a String or ObjectId reference (of the name) in an Array of "tags" within your Project model.
Just remember when you're going to Edit Tag names / Delete a tag / etc, you'll want to go through and update (if they are saved as Strings) / remove those tags from any Projects that have it in their array.

Related

mongoose own populate with custom query

I'm trying to create a custom query method in mongoose - similar to the populate()-function of mongoose. I've the following two simple schemas:
const mongoose = require('mongoose')
const bookSchema = new mongoose.Schema({
title: String,
author: {type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Author'}
}, {versionKey: false})
const authorSchema = new mongoose.Schema({
name: String
}, {versionKey: false})
Now, I want retrieve authors information and furthermore the books written by the author. As far as I know, mongoose provides custom queries, hence my idea was to write a custom query function like:
authorSchema.query.populateBooks = function () {
// semi-code:
return forAll(foundAuthors).findAll(books)
}
Now, to get all authors and all books, I can simply run:
authorModel.find({}).populateBooks(console.log)
This should result in something like this:
[ {name: "Author 1", books: ["Book 1", "Book 2"]}, {name: "Author 2", books: ["Book 3"] } ]
Unfortunately, it doesn't work because I don't know how I can access the list of authors selected previously in my populateBooks function. What I need in my custom query function is the collection of the previous-selected documents.
For example, authorModel.find({}) already returns a list of authors. In populateBooks() I need to iterate through this list to find all books for all authors. Anyone know how I can access this collection or if it's even possible?
populate: "Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s)" (from the docs i linked).
Based on your question, you're not looking for population. yours is a simple query (the following code is to achieve the example result you gave at the end. note that your books field had a value of an array of strings, I'm assuming those were the titles). Also, do note that the following code will work with the models you've already provided, but this is a bad implementation that i recommend against - for multiple reasons: efficiency, elegance, potential errors (for e.g, authors with identical names), see note after code:
Author.find({}, function(err, foundAuthors){
if (err){
console.log(err); //handle error
}
//now all authors are objects in foundAuthors
//but if you had certain search parameters, foundAuthors only includes those authors
var completeList = [];
for (i=0;i<foundAuthors.length;i++){
completeList.push({name: foundAuthors[i].name, books: []});
}
Book.find({}).populate("author").exec(function(err, foundBooks){
if (err){
console.log(err); //handle err
}
for (j=0;j<foundBooks.length;j++){
for (k=0;k<completeList.length;k++){
if (completeList[k].name === foundBooks[j].author.name){
completeList[k].books.push(foundBooks[j].title);
}
}
}
//at this point, completeList has exactly the result you asked for
});
});
However, as i stated, i recommend against this implementation, this was based on the code you already provided without changing it.
I recommend changing your author schema to include a books property:
var AuthorSchema = new mongoose.Schema({
books: [{
type: mongoose.Schema.Types.ObjectId,
ref: "Book"
}]
//all your other code for the schema
});
And add all books to their respective authors. This way, all you would need to do to get an array of objects, each of which contains an author and all of his books is one query:
Author.find({}).populate("books").exec(function(err, foundAuthors){
//if there's no err, then foundAuthors is an array of authors with their books
});
That is far simpler, more efficient, more elegant and more effective than the earlier possible solution i gave, based on your already existing code without changing it.

Mongoose query parent and include children with reference

I have 2 Mongoose Schemas. Location and Place. I need to be able to pull location information when I query place and it works well with populate. However in another case, I need to find all places that belong to location.
Does this mean that I need to reference Places in Location Schema as well? Multiple places can belong to single location. I can't embed places in location because places will have sub information, and I don't want locations document get too big cause of that.
var LocationSchema = new mongoose.Schema({
name: {
type: String,
},
});
var PlaceSchema = new mongoose.Schema({
location: {type: mongoose.Schema.Types.ObjectId, ref: 'Location'},
type: String,
});
You just need to query by the location id.
Places.find({location: { $in: [location ids] }}).then(places => {...})

Mongoose search for an object's value in referenced property (subdocument)

I have two schemas:
var ShelfSchema = new Schema({
...
tags: [{
type: Schema.Types.ObjectId,
ref: 'Tag'
}]
});
var TagSchema = new Schema({
name: {
type: String,
unique: true,
required: true
}
});
I would like to search for all Shelves where the tags array has a tag with a specific value.
I have tried using:
modelShelf.find({tags 'tags.name': 'mytag'})...
but it does not work. It always returns an empty array.
Any idea?
Looking at db each Shelf instance links only the objectID of the tags.
I have used references because I need to work also with Tag(s) entities.
In mongoDB you essentially can't do this directly as queries target a single collection at a time. Recently there were added new features which allow some kind of join when using the aggregation framework but for your needs that is not necessary.
From your schemas I see that the tags' names are unique so you can first fetch your desired tag with something like
modelTag.find({name: 'mytag'})
in order to get the tag's ID and then query your shelf collection for this tag ID
modelShelf.find({tags: tagId})

Easy way to reference Documents in Mongoose

In my application I have a User Collection. Many of my other collections have an Author (an author contains ONLY the user._id and the user.name), for example my Post Collection. Since I normally only need the _id and the name to display e.g. my posts on the UI.
This works fine, and seems like a good approach, since now everytime I deal with posts I don`t have to load the whole user Object from the database - I can only load my post.author.userId/post.author.name.
Now my problem: A user changes his or her name. Obviously all my Author Objects scattered around in my database still have the old author.
Questions:
is my approuch solid, or should I only reference the userId everywhere I need it?
If I'd go for this solution I'd remove my Author Model and would need to make a User database call everytime I want to display the current Users`s name.
If I leave my Author as is, what would be a good way to implement a solution for situations like the user.name change?
I could write a service which checks every model which has Authors of the current user._id and updates them of course, but this sounds very tedious. Although I'm not sure there's a better solution.
Any pro tipps on how I should deal with problems like this in the future?
Yes, sometime database are good to recorded at modular style. But You shouldn't do separating collection for user/author such as
At that time if you use mongoose as driver you can use populate to get user schema data.
Example, I modeling user, author, post that.
var UserSchema = new mongoose.Schema({
type: { type: String, default: "user", enum: ["user", "author"], required: true },
name: { type: String },
// Author specific values
joinedAt: { type: Date }
});
var User = mongoose.model("User", UserSchema);
var PostSchema = new mongoose.Schema({
author: { type: mongoose.Scheam.Types.ObjectId, ref: "User" },
content: { type: String }
});
var Post = mongoose.model("Post", PostSchema);
In this style, Post are separated model and have to save like that. Something like if you want to query a post including author's name, you can use populate at mongoose.
Post.findOne().populate("author").exce(function(err, post) {
if(err)
// do error handling
if(post){
console.log(post.author.type) // author
}
});
One solution is save only id in Author collection, using Ref on the User collection, and populate each time to get user's name from the User collection.
var User = {
name: String,
//other fields
}
var Author = {
userId: {
type: String,
ref: "User"
}
}
Another solution is when updating name in User collection, update all names in Author collection.
I think first solution will be better.

Mongoose include model with dependencies

I'm creating a node application based off of This example.
server.js has the following:
fs.readdirSync(__dirname + "/app/model").forEach(function (file) {
if (~file.indexOf(".js")) {
require (__dirname + "/app/model" + "/" + file);
}
});
This includes all of the files from app/model. This works, but the problem is that my models have reference dependencies that don't come up in the example. Specifically I have a model like this:
ResourceSchema = new Schema({
"comment": [Comment]
});
However when I run node I get an error that Comment is not defined, which is not really unexpected.
This does not come up in the example even though the schema has a reference because it uses:
user: {type : Schema.ObjectId, ref : 'User'},
My question is, should I use "comment": {type: [Schema.ObjectId], ref: "Comment"} instead (or something else?) Is there a proper way to include the schema reference for Comment in the Resource Schema declaration?
If you want to define an array of references, you should use the following definition:
ResourceSchema = new Schema({
"comment": [{type : Schema.ObjectId, ref : 'Comment'}]
});
The way you defined comments is used to define an array of subdocuments (see mongoose API docs).
So, you should use it only if you want to store all your comments directly inside of the parent document. In this case Comments schema should be already defined, or required from another module.

Resources