Search code examples
node.jsmongodbmongoosemongoose-schemamongoose-populate

Mongoose Many to many relations


I'm new to mongoDB and Mongoose, and I have some problems with relations. My schema has 3 tables (User / Person / Family), you can see it below.

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

var userSchema = Schema({
    _id       : Schema.Types.ObjectId,
    email     : String,
    person    : [{ type: Schema.Types.ObjectId, ref: 'Person' }] // A user is linked to 1 person
});

var personSchema = Schema({
    _id       : Schema.Types.ObjectId,
    name      : String,
    user      : [{ type: Schema.Types.ObjectId, ref: 'User' }]   // A person is linked to 1 user
    families  : [{ type: Schema.Types.ObjectId, ref: 'Family' }] // A person have 1,n families
});

var familySchema = Schema({
    _id       : Schema.Types.ObjectId,
    name      : String,
    persons   : [{ type: Schema.Types.ObjectId, ref: 'Person' }] // A family have 0,n persons
});

var User   = mongoose.model('User', userSchema);
var Person = mongoose.model('Person', personSchema);
var Family = mongoose.model('Family', familySchema);

I don't know if my schema is good, does the parameter person is require in my userSchema ? Because the informations will be duplicated, in userSchema I will have the personID and in the personSchema this wil be the userID.

If I understand it's usefull to have this duplicated values for my requests ? But if the informations is duplicated I need to execute two queries to update the two tables ?

For exemple, if I have a person with a family (families parameter in personSchema), and in the family I have this person (persons parameter in familySchema). What will be the requests to remove / update the lines in the tables ?

Thanks


Solution

  • IMHO, your schema seems fine if it meets your needs !! (Although, if you think your current schema fulfills your purpose without being bloated, then yeah its fine)..

    1. "Person" seems to be the only type of a user and the entity to be connected to rest of the other entities . As long as this is the case, you can feel free to remove the person parameter from the userschema as you can access the user information from the person. But lets assume if there exists another entity "Aliens" who also has their own unique family, then it would be better to add the alien and person parameter in the "User" Schema to see the types of users.(As long as there's only one type i.e. Person, then you may not need to add it in userschema). In case, if you still like to keep it, then please make the following change too in your schema as you are passing the array although it seems to be one to one relation !

    var userSchema = Schema({
      _id       : Schema.Types.ObjectId,
      email     : String,
      person    : { type: Schema.Types.ObjectId, ref: 'Person' } // A user is linked to 1 
      //person // Here I have removed the []
    });
    
    var personSchema = Schema({
    _id       : Schema.Types.ObjectId,
    name      : String,
    user      : { type: Schema.Types.ObjectId, ref: 'User' }   // removed [] here too
    families  : [{ type: Schema.Types.ObjectId, ref: 'Family' }] 
    });

    1. Yes, you will need to update it for both entities Person and Family if you want to maintain the uniformity. But, it could be done in one request/ mutation.

    2. Well, you could perform the request depending upon the flow order of your business logic. Lets say if "Homer" is a Person who is a new member of the Simpson Family. So, in that case you would add "Homer" to the Family collection(table) and then push the ObjectId of this Simpson (Family collection) to the Person entity.

    I have added the sample example of adding Homer to the Simpson family below. Hope this helps :)

    addNewFamilyMember: async (_, {personID, familyID}) => {
      try{
        let family = await Family.findOne({_id: familyID});
        let person = await Person.findOne({_id: personID}); // created to push the objectId of the family in this
        if (!family){
          throw new Error ('Family not found !')
        } else {
          let updatedFamily = await Family.findByIdAndUpdate(
            { _id: familyID },
            {
              "$addToSet": { // "addToSet" inserts into array only if it does not exist already
                persons: personID
              }
            },
            { new: true }
          );
    
          person.families.push(updatedFamily); // pushing objectId of the Simpson family in Person "Homer"
          person = await person.save();
          updatedFamily.persons.push(person); // pushing "Homer" in the Simpson family
          updatedFamily = updatedFamily.save();
          
          return updatedFamily;
        }
      } catch(e){
      throw e;
      }
    }

    If you want to perform update, then it depends upon the intent of your purpose (as for example, if you just want to update the name "Homer", you would only have to update it in the Person collection, as the Family collection already has reference to the objectId of Homer, so every time you make an update to the Homer, the updated document would be referenced by Family collection ! ), and

    if you want to perform deletion, then in that case too, the approach would be different based upon the scenario, as if you wish to remove a person document, or just remove the person reference from the family, or remove the family reference from the person !!

    Lets say you want to delete a person then in that case, you would have to take the personId and search for that person and since you have access to the families via this person, you can access the families via person.families and remove the personId from those respective families as well ! And then you could remove the associated user too as you have the reference to the user too from the same person object.

    To sum up, it depends upon your choice of action, and how much sanitization you want in your schema.. The above mentioned process would be just different in case if we take a different approach.