I'm using bookshelf.js as my ORM for node. I have 2 models
Post = bookshelf.Model.extend({
tableName: 'posts',
categories: function () {
return this.belongsToMany('Category');
}
});
Category = bookshelf.Model.extend({
tableName: 'categories',
posts: function () {
return this.belongsToMany('Post');
}
});
They are related via categories_posts table, so its a many to many relationship.
Now I want the user to be able to view all posts in certain category. But I dont want just all posts, I want pagination. But I'm unable to fetch the total posts under specific category, it always returns the total posts that exists in the system.
I've tried this Category.forge({id: id}).posts().query().count('*').then(function(total) {}); but total equals the total posts in posts table as opposed to total posts under category identified by id. Is this even possible, or I should make a helper method that directly queries the categories_posts table?
Thanks.
Ok I think I found a solution.
Category.forge({id: id}).fetch({withRelated: [{posts: function(q) {
q.count('* AS count');
}}]})
.then(function(total) {
console.log(total.toJSON().posts[0].count);
});
Will give the correct value of posts under category identified by id.
Related
In Mongoose, I can use a query populate to populate additional fields after a query. I can also populate multiple paths, such as
Person.find({})
.populate('books movie', 'title pages director')
.exec()
However, this would generate a lookup on book gathering the fields for title, pages and director - and also a lookup on movie gathering the fields for title, pages and director as well. What I want is to get title and pages from books only, and director from movie. I could do something like this:
Person.find({})
.populate('books', 'title pages')
.populate('movie', 'director')
.exec()
which gives me the expected result and queries.
But is there any way to have the behavior of the second snippet using a similar "single line" syntax like the first snippet? The reason for that, is that I want to programmatically determine the arguments for the populate function and feed it in. I cannot do that for multiple populate calls.
After looking into the sourcecode of mongoose, I solved this with:
var populateQuery = [{path:'books', select:'title pages'}, {path:'movie', select:'director'}];
Person.find({})
.populate(populateQuery)
.execPopulate()
you can also do something like below:
{path:'user',select:['key1','key2']}
You achieve that by simply passing object or array of objects to populate() method.
const query = [
{
path:'books',
select:'title pages'
},
{
path:'movie',
select:'director'
}
];
const result = await Person.find().populate(query).lean();
Consider that lean() method is optional, it just returns raw json rather than mongoose object and makes code execution a little bit faster! Don't forget to make your function (callback) async!
This is how it's done based on the Mongoose JS documentation http://mongoosejs.com/docs/populate.html
Let's say you have a BookCollection schema which contains users and books
In order to perform a query and get all the BookCollections with its related users and books you would do this
models.BookCollection
.find({})
.populate('user')
.populate('books')
.lean()
.exec(function (err, bookcollection) {
if (err) return console.error(err);
try {
mongoose.connection.close();
res.render('viewbookcollection', { content: bookcollection});
} catch (e) {
console.log("errror getting bookcollection"+e);
}
//Your Schema must include path
let createdData =Person.create(dataYouWant)
await createdData.populate([{path:'books', select:'title pages'},{path:'movie', select:'director'}])
In a situation similar to this one, Getting joined data from strongloop/loopback, where one has Products and product Categories, how does one return the Category Name rather than the id (foreign key) as the default response for /Products? I've been able to hide the id field but not return the name. Thanks.
Supposing you have the relation Product hasOne Category, called productCat
With Node API
Product.find({
include: {
relation: 'productCat', // include the Category object
scope: { // further filter the Category object
fields: 'name', // only show category name
}
}
}, function(err, results) { /* ... */});
With REST API
GET api/Products?filter={"include":{"relation":"productCat","scope":{"fields":"name"}}}
Hope this helps (haven't tested it but it should work)
I'm using BookshelfJS. I have two models users and posts. Obviously, the relationship here is many to many. So I have a pivot table post_user.
Now, given a user_id, I want to find all the posts for that user. So far, I've managed to do this using knex:
knex.select()
.from('post_user')
.where('user_id', user_id)
.then(function (pivots) {
// Now loop through pivots and return all the posts
// Using pivot.post_id
pivots.map(function(pivot) {})
});
Is there a cleaner way of doing this?
You'll want to define the many-to-many relationship in your Bookshelf models. Something like this:
var User = bookshelf.Model.extend({
tableName: 'users',
posts: function() {
return this.belongsToMany(Post);
}
});
var Post = bookshelf.Model.extend({
tableName: 'posts',
users: function() {
return this.belongsToMany(User);
}
});
By convention, the pivot table Bookshelf would be using is posts_users (table names combined with _, starting from the table that is alphabetically first). It should contain a user_id and a post_id (and a coposite PK of those two). If you don't wish to rename the pivot table, see the documentation for belongsToMany for instructions on how to define the table and ids of your pivot table.
After this, you can query your model with Bookshelf:
return new User().where('id', user_id)
.fetch({withRelated: 'posts'})
.then(function (user) {
var postCollection = user.get('posts');
// do something with the posts
});
See also the documentation for fetch.
I'm not sure how to even phrase this question... but here is a try. I'm calling the Book the "Parent" model and the Author the "Child" model.
I have two mongoose models--- Author and Books:
var Author = mongoose.model("Author", {
name: String
});
var Book = mongoose.model("Book", {
title: String,
inPrint: Boolean,
authors: [ { type: mongoose.Schema.ObjectId, ref: "Author"} ]
});
I am trying to run a query which would return all of the authors (child model) who have books (parent model) which are inPrint.
I could think of ways to do it with multiple queries, but I'm wondering if there is a way to do it with one query.
You could use populate as stated in the docs
There are no joins in MongoDB but sometimes we still want references to documents in other collections. This is where population comes in. Read more about how to include documents from other collections in your query results here.
In your case, it would look something like this:
Book.find().populate('authors')
.where('inPrint').equals(true)
.select('authors')
.exec(function(books) {
// Now you should have an array of books containing authors, which can be
// mapped to a single array.
});
I just stumbled upon this problem today and solved it:
Author.find()
.populate({ path: 'books', match: { inPrint: true } })
.exec(function (err, results) {
console.log(results); // Should do the trick
});
The magic occurs in the match option of populate, which refers to a property of the nested document to populate.
Also check my original post
EDIT: I was confusing books for authors, now it's corrected
I have two models in my app: Item and Comment. An Item can have many Comments, and a Comment instance contains a reference to an Item instance with key 'comment', to keep track of the relationship.
Now I have to send a JSON list of all Items with their Comment count when user requests on a particular URL.
function(req, res){
return Item.find()
.exec(function(err, items) {
return res.send(items);
});
};
I am not sure how can I "populate" comment count to the items. This seems to be a common problem and I tend to think there should be some nicer way of doing this job than brute force.
So please share your thoughts. How would you "populate" the Comment count to the Items?
check the MongoDB documentation and look for the method findAndModify() -- with it you can atomically update a document, e.g. add a comment and increment the document counter at the same time.
findAndModify
The findAndModify command atomically modifies and returns a single document. By default, the returned document does not include the modifications made on the update. To return the document with the modifications made on the update, use the new option.
Example
Use the update option, with update operators $inc for the counter, and $addToSet for adding the actual comment to an embedded array of comments.
db.runCommand(
{
findAndModify: "item",
query: { name: "MyItem", state: "active", rating: { $gt: 10 } },
sort: { rating: 1 },
update: { $inc: { commentCount: 1 },
$addToSet: {comments: new_comment} }
}
)
See:
MongoDB: findAndModify
MongoDB: Update Operators
I did some research on this issue and came up with following results. First, MongoDB docs suggest:
In general, use embedded data models when:
you have “contains” relationships between entities.
you have one-to-many relationships where the “many” objects always appear with or are viewed in the context of their parent documents.
So in my situation, it makes much more sense if Comments are embedded into Items, instead of having independent existence.
Nevertheless, I was curious to know the solution without changing my data model. As mentioned in MongoDB docs:
Referencing provides more flexibility than embedding; however, to
resolve the references, client-side applications must issue follow-up
queries. In other words, using references requires more roundtrips to
the server.
As multiple roundtrips are kosher now, I came up with following solution:
var showList = function(req, res){
// first DB roundtrip: fetch all items
return Item.find()
.exec(function(err, items) {
// second DB roundtrip: fetch comment counts grouped by item ids
Comment.aggregate({
$group: {
_id: '$item',
count: {
$sum: 1
}
}
}, function(err, agg){
// iterate over comment count groups (yes, that little dash is underscore.js)
_.each(agg, function( itr ){
// for each aggregated group, search for corresponding item and put commentCount in it
var item = _.find(items, function( item ){
return item._id.toString() == itr._id.toString();
});
if ( item ) {
item.set('commentCount', itr.count);
}
});
// send items to the client in JSON format
return res.send(items);
})
});
};
Agree? Disagree? Please enlighten me with your comments!
If you have a better answer, please post here, I'll accept it if I find it worthy.