'Multiple schema references in single schema array - mongoose

Can you populate an array in a mongoose schema with references to a few different schema options?

To clarify the question a bit, say I have the following schemas:

var scenarioSchema = Schema({
  _id     : Number,
  name    : String,
  guns : []
});

var ak47 = Schema({
  _id     : Number
  //Bunch of AK specific parameters
});

var m16 = Schema({
  _id     : Number
  //Bunch of M16 specific parameters
});

Can I populate the guns array with a bunch of ak47 OR m16? Can I put BOTH in the same guns array? Or does it require a populate ref in the assets array, like this, which limits it to a single specific type?

guns: [{ type: Schema.Types.ObjectId, ref: 'm16' }]

I know I could just have separate arrays for different gun types but that will create an insane amount of extra fields in the schema as the project scales, most of which would be left empty depending on the loaded scenario.

var scenarioSchema = Schema({
  _id     : Number,
  name    : String,
  ak47s : [{ type: Schema.Types.ObjectId, ref: 'ak47' }],
  m16s: [{ type: Schema.Types.ObjectId, ref: 'm16' }]
});

So back to the question, can I stick multiple schema references in a single array?



Solution 1:[1]

What you are looking for here is the mongoose .discriminator() method. This basically allows you to store objects of different types in the same collection, but have them as distinquishable first class objects.

Note that the "same collection" principle here is important to how .populate() works and the definition of the reference in the containing model. Since you really can only point to "one" model for a reference anyway, but there is some other magic that can make one model appear as many.

Example listing:

var util = require('util'),
    async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

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

//mongoose.set("debug",true);

var scenarioSchema = new Schema({
  "name": String,
  "guns": [{ "type": Schema.Types.ObjectId, "ref": "Gun" }]
});

function BaseSchema() {
  Schema.apply(this, arguments);

  // Common Gun stuff
  this.add({
    "createdAt": { "type": Date, "default": Date.now }
  });
}

util.inherits(BaseSchema, Schema);

var gunSchema = new BaseSchema();

var ak47Schema = new BaseSchema({
  // Ak74 stuff
});

ak47Schema.methods.shoot = function() {
  return "Crack!Crack";
};

var m16Schema = new BaseSchema({
  // M16 Stuff
});

m16Schema.methods.shoot = function() {
  return "Blam!!"
};


var Scenario = mongoose.model("Scenario", scenarioSchema);

var Gun = mongoose.model("Gun", gunSchema );
var Ak47 = Gun.discriminator("Ak47", ak47Schema );
var M16 = Gun.discriminator("M16", m16Schema );


async.series(
  [
    // Cleanup
    function(callback) {
      async.each([Scenario,Gun],function(model,callback) {
        model.remove({},callback);
      },callback);
    },

    // Add some guns and add to scenario
    function(callback) {
      async.waterfall(
        [
          function(callback) {
            async.map([Ak47,M16],function(gun,callback) {
              gun.create({},callback);
            },callback);
          },
          function(guns,callback) {
            Scenario.create({
              "name": "Test",
              "guns": guns
            },callback);
          }
        ],
        callback
      );
    },

    // Get populated scenario
    function(callback) {
      Scenario.findOne().populate("guns").exec(function(err,data) {

        console.log("Populated:\n%s",JSON.stringify(data,undefined,2));

        // Shoot each gun for fun!
        data.guns.forEach(function(gun) {
          console.log("%s says %s",gun.__t,gun.shoot());
        });

        callback(err);
      });
    },

    // Show the Guns collection
    function(callback) {
      Gun.find().exec(function(err,guns) {
        console.log("Guns:\n%s", JSON.stringify(guns,undefined,2));
        callback(err);
      });
    },

    // Show magic filtering
    function(callback) {
      Ak47.find().exec(function(err,ak47) {
        console.log("Magic!:\n%s", JSON.stringify(ak47,undefined,2));
        callback(err);
      });
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

And output

Populated:
{
  "_id": "56c508069d16fab84ead921d",
  "name": "Test",
  "__v": 0,
  "guns": [
    {
      "_id": "56c508069d16fab84ead921b",
      "__v": 0,
      "__t": "Ak47",
      "createdAt": "2016-02-17T23:53:42.853Z"
    },
    {
      "_id": "56c508069d16fab84ead921c",
      "__v": 0,
      "__t": "M16",
      "createdAt": "2016-02-17T23:53:42.862Z"
    }
  ]
}
Ak47 says Crack!Crack
M16 says Blam!!
Guns:
[
  {
    "_id": "56c508069d16fab84ead921b",
    "__v": 0,
    "__t": "Ak47",
    "createdAt": "2016-02-17T23:53:42.853Z"
  },
  {
    "_id": "56c508069d16fab84ead921c",
    "__v": 0,
    "__t": "M16",
    "createdAt": "2016-02-17T23:53:42.862Z"
  }
]
Magic!:
[
  {
    "_id": "56c508069d16fab84ead921b",
    "__v": 0,
    "__t": "Ak47",
    "createdAt": "2016-02-17T23:53:42.853Z"
  }
]

You can also uncomment the mongoose.set("debug",true) line in the listing to see how mongoose is actually constructing the calls.

So what this demonstrates is that you can apply different schemas to different first class objects, and even with different methods attached to them just like real objects. Mongoose is storing these all in a "guns" collection with the attached model, and it will contain all "types" refernced by the discriminator:

var Gun = mongoose.model("Gun", gunSchema );
var Ak47 = Gun.discriminator("Ak47", ak47Schema );
var M16 = Gun.discriminator("M16", m16Schema );

But also each different "type" is referenced with it's own model in a special way. So you see that when mongoose stores and reads the object, there is a special __t field which tells it which "model" to apply, and hence attached schema.

As one example we call the .shoot() method, which is defined differently for each model/schema. And also you can still use each as a model by itself for queries or other operations, since Ak47 will automatically apply the __t value in all query/upates.

So though the storage is in one collection it can appear to be many collections, but also has the benefit of keeping them together for other useful operations. This is how you can apply the kind of "polymorphism" you are looking for.

Solution 2:[2]

I post my solution to this question. With the same concept of using discriminators

  • baseSchema: Main model collection that contains the array field with mutiples schemas
  • itemSchema: Schema parent for discriminator use
  • fizzSchema & buzzSchema: Multiples schemas are wanted to use in array

Model

const itemSchema = Schema({
  foo: String,
}, { discriminatorKey: 'kind', _id: false});

const fizzSchema = Schema({
  fizz: String,
}, { _id: false });

const buzzSchema = Schema({
  buzz: String,
}, { _id: false });

const baseSchema = Schema({
  items: [itemSchema],
});

baseSchema.path('items').discriminator('fizz', fizzSchema);
baseSchema.path('items').discriminator('buzz', buzzSchema);

const List = model('list', baseSchema);

Testbench

const body = {
  items: [
    { foo: 'foo'},
    { foo: 'foo', fizz: 'fizz'},
    { foo: 'foo', kind: 'fizz', fizz: 'fizz'},
    { foo: 'foo', kind: 'buzz', buzz: 'buzz'},
    { kind: 'buzz', buzz: 'buzz'},
  ]
};

const doc = new List(body);

console.log(doc);

Output

{
  items: [
    { foo: 'foo' },
    { foo: 'foo' },
    { fizz: 'fizz', foo: 'foo', kind: 'fizz' },
    { buzz: 'buzz', foo: 'foo', kind: 'buzz' },
    { buzz: 'buzz', kind: 'buzz' }
  ],
  _id: new ObjectId("626a7b1cf2aa28008d2be5ca")
}

Can be executed here: https://replit.com/@Gabrirf/Mongoose

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Blakes Seven
Solution 2 Gabriel Rodríguez Flores