1

I'm coding an app using Node.js and MongooseJS as my middleware for handling database calls.

My problem is that I have some nested schemas and one of them is populated in a wrong way. When I track every step of the population - all of the data is fine, except the devices array, which is empty. I double checked the database and there is data inside that array, so it should be fine.

I've got Room schema. Each object of Room has a field called DeviceGroups. This field contains some information and one of them is an array called Devices which stored devices that are assigned to the parent room.

As you can see in the code, I am finding a room based on it's ID that comes in request to the server. Everything is populated fine and data is consistent with the data in the database. Problem is that the devices array is empty.

Is that some kind of a quirk of MongooseJS, or am I doing something wrong here, that devices array is returned empty? I checked in the database itself and there is some data inside it, so the data is fine, the bug is somewhere in the pasted code.

The code:

Schemas:

const roomSchema = Schema({
    name: {
        type: String,
        required: [true, 'Room name not provided']
    },
    deviceGroups: [{
        type: Schema.Types.ObjectId,
        ref: 'DeviceGroup'
    }]
}, { collection: 'rooms' });

const deviceGroupSchema = Schema({
    parentRoomId: {
        type: Schema.Types.ObjectId,
        ref: 'Room'
    },
    groupType: {
        type: String,
        enum: ['LIGHTS', 'BLINDS', 'ALARM_SENSORS', 'WEATHER_SENSORS']
    },
    devices: [
        {
            type: Schema.Types.ObjectId,
            ref: 'LightBulb'
        },
        {
            type: Schema.Types.ObjectId,
            ref: 'Blind'
        }
    ]
}, { collection: 'deviceGroups' });

const lightBulbSchema = Schema({
    name: String,
    isPoweredOn: Boolean,
    currentColor: Number
}, { collection: 'lightBulbs' });

const blindSchema = Schema({
    name: String,
    goingUp: Boolean,
    goingDown: Boolean
}, { collection: 'blinds' });

Database call:

Room
    .findOne({ _id: req.params.roomId })
    .populate({
        path: 'deviceGroups',
        populate: {
            path: 'devices'
        }
    })
    .lean()
    .exec(function(err, room) {
        if (err) {
            res.send(err);
        } else {
            room.deviceGroups.map(function(currentDeviceGroup, index) {
                if (currentDeviceGroup.groupType === "BLINDS") {
                    var blinds = room.deviceGroups[index].devices.map(function(currentBlind) {
                    return {
                        _id: currentBlind._id,
                        name: currentBlind.name,
                        goingUp: currentBlind.goingUp,
                        goingDown: currentBlind.goingDown
                    }
                });
                res.send(blinds);
            }
        });
    }
})
Aleksander Sadaj
  • 184
  • 3
  • 15
  • I don't think that the way you have defined `devices` is correct. One way to have multiple schemas refs in an array is to use [discriminator](http://mongoosejs.com/docs/api.html#model_Model.discriminator) see this [answer](https://stackoverflow.com/a/35470271/3284355) – Molda Dec 21 '17 at 20:31
  • @Molda could you provide a simple example how to do that in my case? Of course, `discriminator` is exactly what I was looking for, but the other post you linked is a little bit complicated because of all of the middleware it uses and it is not super clear for me. could you come up with an example of how to use that in my case? thank you – Aleksander Sadaj Dec 21 '17 at 21:02

2 Answers2

1

This is an example of using discriminator method to be able to use multiple schemas in a single array.

const roomSchema = Schema({
    name: {
        type: String,
        required: [true, 'Room name not provided']
    },
    deviceGroups: [{ type: Schema.Types.ObjectId, ref: 'DeviceGroup' }]
});

const deviceGroupSchema = Schema({
    parentRoom: { type: Schema.Types.ObjectId, ref: 'Room' },
    groupType: {
        type: String,
        enum: ['LIGHTS', 'BLINDS', 'ALARM_SENSORS', 'WEATHER_SENSORS']
    },
    devices: [{ type: Schema.Types.ObjectId, ref: 'Device' }]
});

// base schema for all devices
function DeviceSchema() {
  Schema.apply(this, arguments);

  // add common props for all devices 
  this.add({
    name: String
  });
}

util.inherits(DeviceSchema, Schema);

var deviceSchema = new DeviceSchema();

var lightBulbSchema = new DeviceSchema({
    // add props specific to lightBulbs
    isPoweredOn: Boolean,
    currentColor: Number   
});

var blindSchema = new DeviceSchema({
    // add props specific to blinds
    goingUp: Boolean,
    goingDown: Boolean
});

var Room = mongoose.model("Room", roomSchema );
var DeviceGroup = mongoose.model("DeviceGroup", deviceGroupSchema );

var Device = mongoose.model("Device", deviceSchema );

var LightBulb = Device.discriminator("LightBulb", lightBulbSchema );
var Blind = Device.discriminator("Blind", blindSchema );

// this should return all devices
Device.find()
// this should return all devices that are LightBulbs
LightBulb.find()
// this should return all devices that are Blinds
Blind.find()

In the collection you will see __t property on each device with values according to the schema used (LightBulb or Blind)

I haven't tried the code and i haven't used mongoose in a while but i hope it will work :)

Update - tested working example

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

const roomSchema = Schema({
    name: {
        type: String,
        required: [true, 'Room name not provided']
    },
    deviceGroups: [{ type: Schema.Types.ObjectId, ref: 'DeviceGroup' }]
});

const deviceGroupSchema = Schema({
    parentRoomId: { type: Schema.Types.ObjectId, ref: 'Room' },
    groupType: {
        type: String,
        enum: ['LIGHTS', 'BLINDS', 'ALARM_SENSORS', 'WEATHER_SENSORS']
    },
    devices: [{ type: Schema.Types.ObjectId, ref: 'Device' }]
});

// base schema for all devices
function DeviceSchema() {
  Schema.apply(this, arguments);

  // add common props for all devices 
  this.add({
    name: String
  });
}

util.inherits(DeviceSchema, Schema);

var deviceSchema = new DeviceSchema();

var lightBulbSchema = new DeviceSchema({
    // add props specific to lightBulbs
    isPoweredOn: Boolean,
    currentColor: Number   
});

var blindSchema = new DeviceSchema();
blindSchema.add({
    // add props specific to blinds
    goingUp: Boolean,
    goingDown: Boolean
});


var Room = mongoose.model("Room", roomSchema );
var DeviceGroup = mongoose.model("DeviceGroup", deviceGroupSchema );

var Device = mongoose.model("Device", deviceSchema );

var LightBulb = Device.discriminator("LightBulb", lightBulbSchema );
var Blind = Device.discriminator("Blind", blindSchema );


var conn = mongoose.connect('mongodb://127.0.0.1/test', { useMongoClient: true });

conn.then(function(db){

    var room = new Room({
        name: 'Kitchen'
    });

    var devgroup = new DeviceGroup({
        parentRoom: room._id,
        groupType: 'LIGHTS'
    });

    var blind = new Blind({
        name: 'blind1',
        goingUp: false,
        goingDown: true
    });
    blind.save();

    var light = new LightBulb({
        name: 'light1',
        isPoweredOn: false,
        currentColor: true
    });
    light.save();

    devgroup.devices.push(blind._id);
    devgroup.devices.push(light._id);
    devgroup.save();

    room.deviceGroups.push(devgroup._id);
    room.save(function(err){
        console.log(err);
    });

    // Room
    // .find()
    // .populate({
    //     path: 'deviceGroups',
    //     populate: {
    //         path: 'devices'
    //     }
    // })
    // .then(function(result){
    //     console.log(JSON.stringify(result, null, 4));
    // });

}).catch(function(err){

});
Molda
  • 5,619
  • 2
  • 23
  • 39
  • you propose invoking function `util.inherits(DeviceSchema, Schema);`. my question is: what is `util` ? I dont see it declared anywhere. is it like another package from NPM or maybe something from mongoose itself? – Aleksander Sadaj Dec 22 '17 at 19:13
  • I used your solution and Im still getting an empty array. I checked the database and data is saved there. This is the output: [ { _id: undefined, name: undefined, goingUp: undefined, goingDown: undefined } ] – Aleksander Sadaj Dec 22 '17 at 19:52
  • `util` is built-in module, just require it `var util = require('util')` – Molda Dec 22 '17 at 23:25
  • Thank you! The example you provided works! Also, it helped me understand the `discriminator` and `mongoose` itself a little bit better. I still need some work to do on the `discriminator`, but, thanks to you, Im much closer! – Aleksander Sadaj Dec 24 '17 at 09:34
  • I'm glad i could help. There's just one more think i'd like to point out. Room.find call with populating devicegroup and devices will make 3 individual call to db since it needs to query 3 collections. I'd definitely try to simplify it by avoiding devicegroups completely if possible. That's just my opinion but it's something to keep in mind. – Molda Dec 24 '17 at 15:32
  • I didnt know it works this way so thank you, even more value gained from this question! But Im afraid it has to stay the way it is - the mobile app using this server needs that deviceGroup collection to operate correctly. But still - I will have that in mind and if possible, will remove it :) – Aleksander Sadaj Dec 25 '17 at 15:51
0

Can you delete everything in you else statement and

console.log(room)

Check your mongo store to make sure you have data in your collection.

arrra
  • 31
  • 6
  • I've done that before posting the question. As I wrote in the post itself - I checked the database and everything has right id references saved, I logged `room` and everything is populated correctly except the `devices` array which comes empty – Aleksander Sadaj Dec 22 '17 at 08:34