-1

So i have two schemas, Article and Event Both have an image field.

For Article,

featured_image: {
    type: String,
    default: '',
}

For Event,

featured_image: {
    type: Schema.ObjectId,
    ref: 'Medium'
}

I have another schema, Card, like this

 type: {
    type: String,
    enum: ['Article', 'Event']
 },
 data: {
    type: Schema.ObjectId,
    refPath: 'type'    
 }

I am trying to populate the cards, like this

Card
    .find(query)
    .populate({
            path: 'data',
            populate: [{
                path: 'featured_image',
                model: 'Medium',
                select: 'source type'
            }]
    };)

However, it keeps giving me a cast error, because when card is of type Event, it populates fine, but when it's of type 'Article', featured_image field is of string type and hence cannot be populated.

How do i populate featured_image field only if card is of type Event or it's a reference id, instead of string.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
Shreya Batra
  • 730
  • 1
  • 6
  • 15
  • Is there something in the provided answer that you believe does not address your question? If so then please comment on the answer to clarify what exactly needs to be addressed that has not. If it does in fact answer the question you asked then please note to [Accept your Answers](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work) to the questions you ask – Neil Lunn Jul 09 '17 at 08:57
  • "Bump". Still not response? – Neil Lunn Jul 13 '17 at 04:49

1 Answers1

2

Instead of what you are attempting to do you should be using "discriminators", which is in fact the correct way to handle a relationship where the object types vary in the reference given.

You use discriminators by the different way in which you define the model, which instead constructs from a "base model" and schema as in:

const contentSchema = new Schema({
  name: String
});

const articleSchema = new Schema({
  image: String,
});

const eventSchema = new Schema({
  image: { type: Schema.Types.ObjectId, ref: 'Medium' }
});

const cardSchema = new Schema({
  name: String,
  data: { type: Schema.Types.ObjectId, ref: 'Content' }
});

const Medium = mongoose.model('Medium', mediumSchema);
const Card = mongoose.model('Card', cardSchema )

const Content = mongoose.model('Content', contentSchema);
const Article = Content.discriminator('Article', articleSchema);
const Event = Content.discriminator('Event', eventSchema);

So instead you define a "base model" such as Content here which you actually point the references to within Event.

The next part is that the differing schema are actually registered to this model via the .discriminator() method from the base model, as opposed to the .model() method. This registers the schema with the general Content model in such a way that when you refer to any model instance defined with .discriminator() that a special __t field is implied to exist in that data, using the registered model name.

Aside from enabling mongoose to .populate() on different types, this also has the advantage of being a "full schema" attached to the different types of items. So you have have different validation and other methods as well if you like. It is indeed "polymorphism" at work in a database context, with helpful schema objects attached.

Therefore we can demonstrate both the varied "joins" that are done, as well as that you can now both use the individual models for Article and Event which would deal with only those items in all queries and operations. And not only can you use "individually", but since the mechanism for this actually stores the data in the same collection, there is also a Content model which gives access to both these types. Which is in essence how the main relation works in the definition to the Event schema.

As a full listing

const async = require('async'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.set('debug',true);
mongoose.Promise = global.Promise;

mongoose.connect('mongodb://localhost/cards');

const mediumSchema = new Schema({
  title: String
});

const contentSchema = new Schema({
  name: String
});

const articleSchema = new Schema({
  image: String,
});

const eventSchema = new Schema({
  image: { type: Schema.Types.ObjectId, ref: 'Medium' }
});

const cardSchema = new Schema({
  name: String,
  data: { type: Schema.Types.ObjectId, ref: 'Content' }
});

const Medium = mongoose.model('Medium', mediumSchema);
const Card = mongoose.model('Card', cardSchema )

const Content = mongoose.model('Content', contentSchema);
const Article = Content.discriminator('Article', articleSchema);
const Event = Content.discriminator('Event', eventSchema);

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

async.series(
  [
    // Clean data
    (callback) =>
      async.each(mongoose.models,(model,callback) =>
        model.remove({},callback),callback),

    // Insert some data
    (callback) =>
      async.waterfall(
        [
          (callback) =>
            Medium.create({ title: 'An Image' },callback),

          (medium,callback) =>
            Content.create(
              [
                { name: "An Event", image: medium, __t: 'Event' },
                { name: "An Article", image: "A String", __t: 'Article' }
              ],
              callback
            ),

          (content,callback) =>
            Card.create(
              [
                { name: 'Card 1', data: content[0] },
                { name: 'Card 2', data: content[1] }
              ],
              callback
            )
        ],
        callback
      ),

    // Query and populate
    (callback) =>
      Card.find()
        .populate({
          path: 'data',
          populate: [{
            path: 'image'
          }]
        })
        .exec((err,cards) => {
        if (err) callback(err);
        log(cards);
        callback();
      }),

    // Query on the model for the discriminator
    (callback) =>
      Article.findOne({},(err,article) => {
        if (err) callback(err);
        log(article);
        callback();
      }),

    // Query on the general Content model
    (callback) =>
      Content.find({},(err,contents) => {
        if (err) callback(err);
        log(contents);
        callback();
      }),


  ],
  (err) => {
    if (err) throw err;
    mongoose.disconnect();
  }
);

And the sample output for different queries

Mongoose: cards.find({}, { fields: {} })
Mongoose: contents.find({ _id: { '$in': [ ObjectId("595ef117175f6850dcf657d7"), ObjectId("595ef117175f6850dcf657d6") ] } }, { fields: {} })
Mongoose: media.find({ _id: { '$in': [ ObjectId("595ef117175f6850dcf657d5") ] } }, { fields: {} })
[
  {
    "_id": "595ef117175f6850dcf657d9",
    "name": "Card 2",
    "data": {
      "_id": "595ef117175f6850dcf657d7",
      "name": "An Article",
      "image": "A String",
      "__v": 0,
      "__t": "Article"
    },
    "__v": 0
  },
  {
    "_id": "595ef117175f6850dcf657d8",
    "name": "Card 1",
    "data": {
      "_id": "595ef117175f6850dcf657d6",
      "name": "An Event",
      "image": {
        "_id": "595ef117175f6850dcf657d5",
        "title": "An Image",
        "__v": 0
      },
      "__v": 0,
      "__t": "Event"
    },
    "__v": 0
  }
]
Mongoose: contents.findOne({ __t: 'Article' }, { fields: {} })
{
  "_id": "595ef117175f6850dcf657d7",
  "name": "An Article",
  "image": "A String",
  "__v": 0,
  "__t": "Article"
}
Mongoose: contents.find({}, { fields: {} })
[
  {
    "_id": "595ef117175f6850dcf657d6",
    "name": "An Event",
    "image": "595ef117175f6850dcf657d5",
    "__v": 0,
    "__t": "Event"
  },
  {
    "_id": "595ef117175f6850dcf657d7",
    "name": "An Article",
    "image": "A String",
    "__v": 0,
    "__t": "Article"
  }
]
Neil Lunn
  • 148,042
  • 36
  • 346
  • 317