66

I'm working on designing a database structure for a new project, and I'm pretty new to MongoDB, and obviously Mongoose.

I've read Mongooses population documentation, where it has a one-to-many relationship, with one Person document to many Story documents, but the part that confuses me is where instead of the Story documents referencing what Person document it belongs to, the Person schema has it setup so it has an array of what Story documents it 'owns'.

I'm setting up something very similar to this. But I keep thinking it would be easier when creating new Story documents to have the Person document ID. But maybe thats just because I'm more familiar with MySQL relationships using joins.

If this is the best way to do it (and I'm sure it is, since its in the docs), when new Story documents are created, whats the best way to update the array of stories in the associated People document it belongs to? I looked but couldn't find any examples of updating existing documents to add references to other documents (or deleting them for that matter)

I'm sure this is an easy solution that I just overlooked or something, but any help would be great. Thanks!

Justin
  • 1,959
  • 5
  • 22
  • 40

6 Answers6

72

Refer to population, here extract an example from Mongoose.

var mongoose = require('mongoose')
, Schema = mongoose.Schema

var personSchema = Schema({
  _id     : Schema.Types.ObjectId,
  name    : String,
  age     : Number,
  stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

var storySchema = Schema({
  _creator : { type: Schema.Types.ObjectId, ref: 'Person' },
  title    : String,
  fans     : [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});

var Story  = mongoose.model('Story', storySchema);
var Person = mongoose.model('Person', personSchema);

So the example about, Story model stores related Person._id in Story._creator. When you find a document of Story, you can use populate() method to define which attribute in Person model you want to retrieve at the same time, such as:

Story.findOne({_id: 'xxxxxxx'}).populate('person', 'name age').exec(function(err, story) {
  console.log('Story title: ', story.title);
  console.log('Story creator', story.person.name);
});

I believe this is what you looking for. Or else, you can use nested collections instead.

Keiran Tai
  • 948
  • 5
  • 10
  • 8
    Ok, so the `Story._creator` will reference the parent `Person._id` automatically? Or do you have to update both the `Story._creator` and the `Person.stories` values separately? What I'm actually more concerned about, is when you create a new story.. Whats the best way to update the parents `stories` array? Is there a way to push the new Id to it with a single query? Or do you have to query for the entire document, then push the new story to the `Person.stories` array, then update the `Person` document? – Justin Jan 25 '16 at 08:57
  • Do you have to update both the `Story._creator` and the `Person.stories` values separately? Or is there a way to have the `Person.stories` automatically populate when you create a new `Story` document? – Justin Jan 25 '16 at 09:07
  • 1
    Yes, you have to update both. [mongoose populate](http://mongoosejs.com/docs/populate.html) section about Refs to children – Manasov Daniel Jan 25 '16 at 09:08
  • 4
    That worries me about the possibility of them going out of sync, if I have other scripts working with that database, or if someone messes with it manually.. – Justin Jan 25 '16 at 14:55
  • 1
    You may use [$push](https://docs.mongodb.org/manual/reference/operator/update/push/) to update stories to control it. Also the [atomicity doc](https://docs.mongodb.org/manual/core/write-operations-atomicity/) might help. – Keiran Tai Jan 25 '16 at 16:12
  • You should consider fixing your typos in this answer. There is no `Store` model or `Store._creator` -- it's `Story` and it's confusing because you also use the actual word "store". – Jacob Beauchamp Jul 07 '17 at 23:29
  • Is there a reason why you changed the Person._id to a Number rather than what the docs have as a Schema.Types.ObjectId? – Isaac Pak Jan 10 '19 at 16:41
  • @IsaacPak I cannot recall why, it's been a while. Probably the old doc from Mongoose is using "Number" too i.e. https://mongoosejs.com/docs/3.8.x/docs/populate.html – Keiran Tai Jan 14 '19 at 03:27
51

The previous answers to this question were helpful, but it may be useful to see more detailed code. The below code is from my Express.js backend for my application. My application allows users to write reviews. When querying the user, I return all of the reviews that the user has made.

user_model.js

import mongoose, { Schema } from 'mongoose';


const UserSchema = new Schema({
  firstname: String,
  lastname: String,
  username: { type: String, unique: true },
  reviews: [{ type: Schema.Types.ObjectId, ref: 'Review' }],
}, {
  toJSON: {
    virtuals: true,
  },
});

const UserModel = mongoose.model('User', UserSchema);
export default UserModel;

review_model.js

import mongoose, { Schema } from 'mongoose';

const ReviewSchema = new Schema({
  body: String,
  username: String,
  rating: Number,
}, {
  toJSON: {
    virtuals: true,
  },
});

const ReviewModel = mongoose.model('Review', ReviewSchema);
export default ReviewModel;

review_controller.js

// . . .
export const createReview = (req, res) => {
    const review = new Review();
    review.username = req.body.username;
    review.rating = req.body.rating;
    review.body = req.body.body;
    review.save()
      .then((result) => {
        User.findOne({ username: review.username }, (err, user) => {
            if (user) {
                // The below two lines will add the newly saved review's 
                // ObjectID to the the User's reviews array field
                user.reviews.push(review);
                user.save();
                res.json({ message: 'Review created!' });
            }
        });
      })
      .catch((error) => {
        res.status(500).json({ error });
      });
};

user_controller.js

 export const createUser = (req, res) => {
   const user = new User();
   user.username = req.body.username;
   user.email = req.body.email;
   user.save()
       .then((result) => {
            res.json({ message: 'User created!', result });
        })
        .catch((error) => {
          res.status(500).json({ error });
        });
    };

// . . .
// returns the user object associated with the username if any
// with the reviews field containing an array of review objects 
// consisting of the reviews created by the user
export const getUser = (req, res) => {
    User.findOne({ username: req.params.username })
      .populate('reviews')
      .then((result) => {
        res.json(result);
      })
      .catch((error) => {
        res.status(500).json({ error });
      });
  };
College Student
  • 1,719
  • 16
  • 15
10

As in population docs said

var aaron = new Person({ _id: 0, name: 'Aaron', age: 100 });

aaron.save(function (err) {
  if (err) return handleError(err);

  var story1 = new Story({
    title: "Once upon a timex.",
    _creator: aaron._id    // assign the _id from the person
  });

  story1.save(function (err) {
    if (err) return handleError(err);
    // thats it!
  });
  //then add story to person
  aaron.stories.push(story1);
  aaron.save(callback);
});
Manasov Daniel
  • 1,348
  • 9
  • 17
1

One way or two way relation

There is one more possibility you might think about: Do you really need the two way association? Or would it be enough to only store the _creator in each Story. And do not store the list of stories for each Person. The list of stories can still be queried for in a search:

let allStoriesOfOneCreator = Stories.find({_creator: person._id});

https://docs.mongodb.com/manual/tutorial/model-referenced-one-to-many-relationships-between-documents/

In the end this depends on the requirements of your app. How often do you need the stories of a creator?

Robert
  • 1,579
  • 1
  • 21
  • 36
1

Here is a great way to create one-to-many relationships.

  1. First, we define Comment model in Comment.js.
const mongoose = require("mongoose");

const Comment = mongoose.model(
  "Comment",
  new mongoose.Schema({
    username: String,
    text: String,
    createdAt: Date
  })
);

module.exports = Comment;
  1. In Tutorial.js, add comments array like this:


const mongoose = require("mongoose");

const Tutorial = mongoose.model(
  "Tutorial",
  new mongoose.Schema({
    title: String,
    author: String,
    images: [],
    comments: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: "Comment"
      }
    ]
  })
);

module.exports = Tutorial;
  1. In server.js, add createComment function.

const createComment = function(tutorialId, comment) {
  return db.Comment.create(comment).then(docComment => {
    console.log("\n>> Created Comment:\n", docComment);

    return db.Tutorial.findByIdAndUpdate(
      tutorialId,
      { $push: { comments: docComment._id } },
      { new: true, useFindAndModify: false }
    );
  });
};
MD SHAYON
  • 7,001
  • 45
  • 38
0

// if you are type script user then:

import mongoose from 'mongoose';

interface PromotionAttrs {
  price: number;
  startDate: Date;
  endDate: Date;
}

export interface PromotionDoc extends mongoose.Document {
  price: number;
  startDate: string;
  endDate: string;
}

interface PromotionModel extends mongoose.Model<PromotionDoc> {
  build(attrs: PromotionAttrs): PromotionDoc;
}

const promotionSchema = new mongoose.Schema({
  price: {
    type: Number,
  },
  startDate: {
    type: mongoose.Schema.Types.Date,
  },
  endDate: {
    type: mongoose.Schema.Types.Date,
  },
});

promotionSchema.statics.build = (attrs: PromotionAttrs) => {
  return new Promotion(attrs);
};

const Promotion = mongoose.model<PromotionDoc, PromotionModel>(
  'Promotion',
  promotionSchema
);

export { Promotion };
import mongoose from 'mongoose';
import { PromotionDoc } from './promotion';

interface ProductAttrs {
  name: string;
  promotions?: PromotionDoc[];
}

interface ProductModel extends mongoose.Model<ProductDoc> {
  build(attrs: ProductAttrs): any;
}
interface ProductDoc extends mongoose.Document {
  name: string;
  promotions?: PromotionDoc[];
}
const productSchema = new mongoose.Schema({
  promotions: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Promotion',
    },
  ],
});

productSchema.statics.build = (attrs: ProductAttrs) => {
  return new Product(attrs);
};
const Product = mongoose.model<ProductDoc, ProductModel>(
  'Product',
  productSchema
);

export { Product };
const product = await Product.findById(productId);

    if (!product) {
      throw new NotFoundError();
    }
const promotion = Promotion.build({
        price,
        startDate,
        endDate,
      });
      await promotion.save();
      product.promotions?.push();
      await product.save();
Rafiq
  • 8,987
  • 4
  • 35
  • 35