Mongoose populate after save
Asked Answered
S

16

119

I cannot manually or automatically populate the creator field on a newly saved object ... the only way I can find is to re-query for the objects I already have which I would hate to do.

This is the setup:

var userSchema = new mongoose.Schema({   
  name: String,
});
var User = db.model('User', userSchema);

var bookSchema = new mongoose.Schema({
  _creator: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  description: String,
});
var Book = db.model('Book', bookSchema);

This is where I am pulling my hair

var user = new User();
user.save(function(err) {
    var book = new Book({
        _creator: user,
    });
    book.save(function(err){
        console.log(book._creator); // is just an object id
        book._creator = user; // still only attaches the object id due to Mongoose magic
        console.log(book._creator); // Again: is just an object id
        // I really want book._creator to be a user without having to go back to the db ... any suggestions?
    });
});

EDIT: latest mongoose fixed this issue and added populate functionality, see the new accepted answer.

Shanley answered 23/11, 2012 at 8:39 Comment(0)
P
156

You should be able to use the Model's populate function to do this: http://mongoosejs.com/docs/api.html#model_Model.populate In the save handler for book, instead of:

book._creator = user;

you'd do something like:

Book.populate(book, {path:"_creator"}, function(err, book) { ... });

Probably too late an answer to help you, but I was stuck on this recently, and it might be useful for others.

Pretermit answered 6/7, 2013 at 17:57 Comment(7)
It would be nice if this worked with virtual attributes. like creator.profileDepository
if user has some virtual attributes they are not included.Depository
This worked for me although I had some problems when I tried to set the reference to null just before I saved. After the save, calling Book.populate incorrectly populated the field with the previous reference value and I can't work out why. The DB successfully contains the null.Aixenprovence
And this will not re-query the database?!Backcross
this is re-querying the databaseOssifrage
Are we suppose to see the whole object or just the object id after saving? I only seem to get objectId('3g3d445555fffff')?Barozzi
For those that want to stay away from callbacks, here's an alternative solution which returns a promise.Endpaper
H
52

The solution for me was to use execPopulate, like so

const t = new MyModel(value)
return t.save().then(t => t.populate('my-path').execPopulate())

Update

Mongoose 6.X and up: .execPopulate() is removed and .populate() is no longer chainable

The above code should be written like:

const t = new MyModel(value)
return t.save().then(t => t.populate('my-path')) // Returns a promise

If you don't want a promise:

return t.save().then(t => t.populate('my-path')).then(t => t)

Since it is no longer chainable, the new way to populate multiple fields is to use an array or an object:

return t.save().then(t => t.populate(['my-path1', 'my-path2'])).then(t => t)

Resources:

Hotfoot answered 14/5, 2018 at 15:29 Comment(2)
Thanks a lot @Francois , You saved my life , I was trying to find solution for this . FInally got that.Waler
Saved my life as well. Wish you all the best, you rock!Sall
O
37

In case that anyone is still looking for this.

Mongoose 3.6 has introduced a lot of cool features to populate:

book.populate('_creator', function(err) {
 console.log(book._creator);
});

or:

Book.populate(book, '_creator', function(err) {
 console.log(book._creator);
});

see more at: https://github.com/LearnBoost/mongoose/wiki/3.6-Release-Notes#population

But this way you would still query for the user again.

A little trick to accomplish it without extra queries would be:

book = book.toObject();
book._creator = user;
Oleate answered 2/1, 2015 at 13:25 Comment(1)
doing book._creator = user; after the save() is the only correct answer among all the current answers, all other answers require an additional query.Welford
E
29

The solution which returns a promise (no callbacks):

Use Document#populate

// These two are same
book.populate('creator').exec();
book.populate('creator');

// summary
doc.populate(options).exec(); // returns a promise
doc.populate(options) // returns promise

Possible Implementation

var populatedDoc = doc.populate(options);
populatedDoc.then(doc => {
   ... 
});

The .exec() method is used on queries to return a true promise, since queries do not return true promises, they are just thenable values that work with await. But using it on .populate() changes nothing, .populate() returns a promise, since Mongoose 6.X. You can see .exec() used alongside .populate() on the official site.

What exactly .exec() does has been answered here

Read about document population here.

Endpaper answered 17/6, 2017 at 3:29 Comment(4)
Good stuff. ThanksIncoordination
IDK you know or not, but this code is just a syntax sugar. It will sends a new query to the database. So this is not a good answer.Rutheruthenia
This is no longer a valid answer for Mongoose 6.X and up. Please update the answer.Honorary
@luzede, would you mind updating the answer if/when you find a solution?Endpaper
S
15

I thought I'd add to this to clarify things for complete noobs like myself.

What's massively confusing if you're not careful is that there are three very different populate methods. They are methods of different objects (Model vs. Document), take different inputs and give different outputs (Document vs. Promise).

Here they are for those that are baffled:

Document.prototype.populate()

See full docs.

This one works on documents and returns a document. In the original example, it would look like this:

book.save(function(err, book) {
    book.populate('_creator', function(err, book) {
        // Do something
    })
});

Because it works on documents and returns a document, you can chain them together like so:

book.save(function(err, book) {
    book
    .populate('_creator')
    .populate('/* Some other ObjectID field */', function(err, book) {
        // Do something
    })
});

But don't be silly, like me, and try to do this:

book.save(function(err, book) {
    book
    .populate('_creator')
    .populate('/* Some other ObjectID field */')
    .then(function(book) {
        // Do something
    })
});

Remember: Document.prototype.populate() returns a document, so this is nonsense. If you want a promise, you need...

Document.prototype.execPopulate()

See full docs.

This one works on documents BUT it returns a promise that resolves to the document. In other words, you can use it like this:

book.save(function(err, book) {
    book
    .populate('_creator')
    .populate('/* Some other ObjectID field */')
    .execPopulate()
    .then(function(book) {
        // Do something
    })
});

That's better. Finally, there's...

Model.populate()

See full docs.

This one works on models and returns a promise. It's therefore used a bit differently:

book.save(function(err, book) {
    Book // Book not book
    .populate(book, { path: '_creator'})
    .then(function(book) {
        // Do something
    })
});

Hope that's helped some other newcomers.

Soph answered 19/7, 2018 at 21:9 Comment(2)
The example looks straightforward but what exactly is the purpose of {path: ':_createor'}? Is _creator related to the OPs' example? If my document has several properties like _Id, Id, title or year - which would be used for path? The equivalent of the primary key?Seneschal
This answer is outdated, the .populate() method now returns a promise, but is not chainable, it accepts an array of paths, object or a string to do the population. .execPopulate() is deprecated and removed. All these after Mongoose 6.X and up. information about the updatesHonorary
F
12

Just to elaborate and give another example, as it helped me out. This might help those who want to to retrieve partially populated objects after save. The method is slightly different as well. Spent more than an hour or two looking for the correct way to do it.

  post.save(function(err) {
    if (err) {
      return res.json(500, {
        error: 'Cannot save the post'
      });
    }
    post.populate('group', 'name').populate({
      path: 'wallUser',
      select: 'name picture'
    }, function(err, doc) {
      res.json(doc);
    });
  });
Forecourse answered 24/7, 2014 at 5:30 Comment(0)
D
2

It's worked for me

    let post = await PostModel.create({
        ...req.body, author: userId
    })

    post = await post.populate('author', 'name')

    res.status(200).json({
        status: 'success',
        data: { post }
    })
Dogface answered 1/11, 2021 at 4:15 Comment(2)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Theorize
Hello. Just keep in mind that this code maybe looks great but what it is doing is sending another query to database. So your code is just a little sugar and shorter. You can enable debug mode and see the generated queriesRutheruthenia
K
1

Unfortunetly this is a long standing issue with mongoose which I believe is not solved yet:

https://github.com/LearnBoost/mongoose/issues/570

What you can do is to write you own custom getter/setter ( and set real _customer in a seperate property ) for this. For example:

var get_creator = function(val) {
    if (this.hasOwnProperty( "__creator" )) {
        return this.__creator;
    }
    return val;
};
var set_creator = function(val) {
    this.__creator = val;
    return val;
};
var bookSchema = new mongoose.Schema({
  _creator: {
     type: mongoose.Schema.Types.ObjectId,
     ref: 'User',
     get: get_creator,
     set: set_creator
  },
  description: String,
});

NOTE: I didn't test it and it might work strangely with .populate and when setting pure id.

Kind answered 23/11, 2012 at 10:10 Comment(3)
it seems they are not looking to fix the issue.Shanley
this issue is fixed in 3.6Bristletail
@Shanley you really need to change the accepted answer to the highest rated one before, as this answer is no longer validThrough
E
1

Mongoose 5.2.7

This works for me (just a lot of headache !)

exports.create = (req, res, next) => {
  const author = req.userData;
  const postInfo = new Post({
    author,
    content: req.body.content,
    isDraft: req.body.isDraft,
    status: req.body.status,
    title: req.body.title
  });
  postInfo.populate('author', '_id email role display_name').execPopulate();
  postInfo.save()
    .then(post => {
      res.status(200).json(post);
    }).catch(error => {
      res.status(500).json(error);
    });
};
Eutherian answered 9/8, 2018 at 17:13 Comment(0)
K
1

In Mongoose 6.x you can simply do:

const t = await MyModel.create(value).then((t) =>
  t.populate('my-path')
);

The execPopulate() method has now been removed.

Kial answered 20/3, 2022 at 18:0 Comment(0)
H
1

The way populate worked for me using async/await was:

const updatedBook = await book.save()
const populatedBook = await updatedBook.populate('field', { name: 1, author: 1, field3: 1})

where fieldName: 1 tells with what fields from 'field' to populate.

With promises it would probably be like:

book.save()
  .then(savedBook => savedBook.populate())
  .then(populatedBook => populatedBook)
Honorary answered 27/8, 2022 at 17:24 Comment(0)
T
0

Probably sth. like

Book.createAsync(bookToSave).then((savedBook) => savedBook.populateAsync("creator"));

Would be the nicest and least problematic way to make this work (Using Bluebird promises).

Tramway answered 5/1, 2017 at 16:33 Comment(0)
F
0

ended up writing some curry-able Promise functions where you declare your schema, query_adapter, data_adapter functions and populate string in advance. For a shorter / simpler implementation easier on.

It's probably not super efficient, but I thought the execution bit was quite elegant.

github file: curry_Promises.js

declartion

const update_or_insert_Item = mDB.update_or_insert({
    schema : model.Item,
    fn_query_adapter : ({ no })=>{return { no }},
    fn_update_adapter : SQL_to_MDB.item,
    populate : "headgroup"
    // fn_err : (e)=>{return e},
    // fn_res : (o)=>{return o}
})

execution

Promise.all( items.map( update_or_insert_Item ) )
.catch( console.error )
.then( console.log )
Fastback answered 4/9, 2018 at 16:7 Comment(0)
S
0

Save document in model and then populate

   chatRoom = await chatRoom.save();
   const data = await chatRoom
  .populate("customer", "email dp")
  .populate({
    path: "admin",
    select: "name logo",
  })
  .execPopulate();
Stigmatic answered 5/4, 2021 at 4:52 Comment(0)
S
0

Nothing worked for me so I saved and then used findById to get that data with populate back again

const data: any = await Model.findById(id)
  .populate("name")
  
Stigmatic answered 14/4, 2022 at 9:50 Comment(0)
I
-1

I'm not adding anything new here.

It's just a cleaner way of writing this using async/await:

const newCourse = new Course(new_course_data);
const populate_options = [
  // Here write your populate options
];
const created_course = await newCourse.save();
await created_course.populate(populate_options).execPopulate();
Irmairme answered 13/6, 2021 at 10:52 Comment(1)
node:30332) UnhandledPromiseRejectionWarning: TypeError: .populate(...).execPopulate is not a functionLotta

© 2022 - 2024 — McMap. All rights reserved.