54

I have FlashcardSchemas and PackageSchemas in my design. One flashcard can belong to different packages and a package can contain different flashcards.

Below you can see a stripped down version of my mongoose schema definitions:

// package-schema.js
var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

var PackageSchema = new Schema({
    id          : ObjectId,
    title       : { type: String, required: true },
    flashcards  : [ FlashcardSchema ]
});

var exports = module.exports = mongoose.model('Package', PackageSchema);

// flashcard-schema.js
var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

var FlashcardSchema = new Schema({
    id      : ObjectId,
    type        : { type: String, default: '' },
    story       : { type: String, default: '' },
    packages    : [ PackageSchema ]
});

var exports = module.exports = mongoose.model('Flashcard', FlashcardSchema);

As you can see from the comments above, these two schema definitions belong to separate files and reference each other.

I get an exception stating that PackageSchema is not defined, as expected. How can I map a many-to-many relation with mongoose?

Élodie Petit
  • 5,774
  • 6
  • 50
  • 88
  • 1
    There's no straightforward way to do this- why are you having packages part of the flashcard schema, and flashcards part of the package schema? what queries are you expecting to run? – Alex Jun 20 '12 at 12:03
  • When I pull a package from the db, I want to populate the cards array and when I pull a card from the db, I want to see which packages that the card belongs. If there is not a straightforward way to do this, should I use a third schema function to store these relations? – Élodie Petit Jun 20 '12 at 13:12
  • There is a more recent and complete answer here: https://stackoverflow.com/a/46020968/438970 – Damien Nov 20 '19 at 15:04

6 Answers6

200

I am new to node, mongoDB, and mongoose, but I think the proper way to do this is:

var PackageSchema = new Schema({
    id: ObjectId,
    title: { type: String, required: true },
    flashcards: [ {type : mongoose.Schema.ObjectId, ref : 'Flashcard'} ]
});

var FlashcardSchema = new Schema({
    id: ObjectId,
    type: { type: String, default: '' },
    story: { type: String, default: '' },
    packages: [ {type : mongoose.Schema.ObjectId, ref : 'Package'} ]
});

This way, you only store the object reference and not an embedded object.

MaxZoom
  • 7,619
  • 5
  • 28
  • 44
gtsouk
  • 5,208
  • 1
  • 28
  • 35
  • Good job finding this link @Ruairi – gtsouk Mar 07 '14 at 20:21
  • 27
    Just want to point out that this is the right way for many-to-many relationships. The accepted answer is wrong. – volatilevar Jan 13 '15 at 05:31
  • Is `ref:'Flashcard'` and `ref:'Package'` on this answer supposed to match the name of the Schema, or is it arbitrary? In other words, should it be `FlashcardSchema`? – TyMayn Sep 02 '15 at 05:47
  • 3
    @TyMayn It should be set to the model name, not the schema name. Usually FlashcardSchema should generate a model named Flashcard. http://mongoosejs.com/docs/models.html – gtsouk Sep 02 '15 at 16:08
  • 3
    @Élodie-petit please make this the accepted answer. – Predrag Stojadinović Dec 13 '16 at 09:56
  • 3
    To quote from the 'populate' document: `It is debatable that we really want two sets of pointers as they may get out of sync. Instead we could skip populating and directly find() the stories we are interested in.` This makes me think there should only be one ref array on one of the schemas. If you do have ref arrays in both schemas you would want to ensure you only save cards with packages OR packages with cards. I am not expert on Mongoose though. – Grant Carthew Apr 25 '18 at 01:13
  • How do you search in this relationship? e.g if you want to find all FlashcardSchema documents that have a specific id in packages list? – DejanG Mar 26 '20 at 12:59
6

You are doing it the right way, however the problem is that you have to include PackageSchema in the the flashcard-schema.js, and vice-versa. Otherwise these files have no idea what you are referencing

var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;
    PackageSchema = require('./path/to/package-schema.js')

var FlashcardSchema = new Schema({
    id      : ObjectId,
    type        : { type: String, default: '' },
    story       : { type: String, default: '' },
    packages    : [ PackageSchema ]
});
Last Rose Studios
  • 2,461
  • 20
  • 30
  • 1
    Won't this result in infinite recursion as each require() loads the other? – Tony O'Hagan Dec 21 '12 at 13:17
  • 5
    @TonyOHagan no, Node handles the require cycle as explained on http://nodejs.org/api/all.html#all_cycles – FGM Jul 13 '13 at 06:28
  • 2
    Be careful when you use this method. As you store on object and not only its ID, if you modify the "package" linked to your "flashcard", it won't be modified in your "flashcard". – Tim Nov 14 '14 at 12:56
  • 9
    WRONG ANSWER. The answer below is correct. This answer is misleading and on top of that accepted as correct answer. – hhsadiq Apr 16 '16 at 16:13
  • @hhsadiq there are 4 other answers. Which one is the correct one? – Hop hop Feb 21 '17 at 13:33
  • 1
    Answer by @gtsouk – hhsadiq Feb 21 '17 at 19:40
2

You could use the Schema.add() method to avoid the forward referencing problem.

This (untested) solution puts the schema in one .js file

models/index.js

var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

// avoid forward referencing
var PackageSchema = new Schema();
var FlashcardSchema = new Schema();

PackageSchema.add({
    id          : ObjectId,
    title       : { type: String, required: true },
    flashcards  : [ FlashcardSchema ]
});

FlashcardSchema.add({
    id      : ObjectId,
    type        : { type: String, default: '' },
    story       : { type: String, default: '' },
    packages    : [ PackageSchema ]
});

// Exports both types
module.exports = {
    Package:   mongoose.model('Package', PackageSchema),
    Flashcard: mongoose.model('Flashcard', FlashcardSchema)
};  
Tony O'Hagan
  • 21,638
  • 3
  • 67
  • 78
0

You're thinking of this too much like a relational data store. If that's what you want, use MySQL (or another RDBMS)

Failing that, then yes, a third schema could be used, but don't forget it'll still only be the id of each object (no joins, remember) so you'll still have to retrieve each other item in a separate query.

Alex
  • 37,502
  • 51
  • 204
  • 332
  • The only place I have to think of this like a relational dbms is this package-card scenario. Otherwise, you are absolutely right. – Élodie Petit Jun 21 '12 at 14:09
  • Per "no joins" comment in this answer, note that MongoDB released in 2015 now has the $lookup aggregator (https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/) which performs a left outer join. – ryanm Jan 11 '18 at 19:21
0
https://www.npmjs.com/package/mongoose-relationship

##Many-To-Many with Multiple paths

var mongoose = require("mongoose"),
    Schema = mongoose.Schema,
    relationship = require("mongoose-relationship");

var ParentSchema = new Schema({
    children:[{ type:Schema.ObjectId, ref:"Child" }]
});
var Parent = mongoose.models("Parent", ParentSchema);

var OtherParentSchema = new Schema({
    children:[{ type:Schema.ObjectId, ref:"Child" }]
});
var OtherParent = mongoose.models("OtherParent", OtherParentSchema);

var ChildSchema = new Schema({
    parents: [{ type:Schema.ObjectId, ref:"Parent", childPath:"children" }]
    otherParents: [{ type:Schema.ObjectId, ref:"OtherParent", childPath:"children" }]
});
ChildSchema.plugin(relationship, { relationshipPathName:['parents', 'otherParents'] });
var Child = mongoose.models("Child", ChildSchema)

var parent = new Parent({});
parent.save();
var otherParent = new OtherParent({});
otherParent.save();

var child = new Child({});
child.parents.push(parent);
child.otherParents.push(otherParent);
child.save() //both parent and otherParent children property will now contain the child's id 
child.remove() 
sudeep_dk
  • 379
  • 3
  • 14
0

This is the problem of cyclic/circular dependency. This is how you make it work in nodejs. For more detail, check out "Cyclic dependencies in CommonJS" at http://exploringjs.com/es6/ch_modules.html#sec_modules-in-javascript

//------ a.js ------
var b = require('b');
function foo() {
    b.bar();
}
exports.foo = foo;

//------ b.js ------
var a = require('a'); // (i)
function bar() {
    if (Math.random()) {
        a.foo(); // (ii)
    }
}
exports.bar = bar;
Lu Tran
  • 401
  • 4
  • 9