Mongoose: Discriminator update problem (with save() operation)

Created on 3 Feb 2016  ·  11Comments  ·  Source: Automattic/mongoose

Hey guys, I use discriminator to do inherit mechanism. like

let UserSchema = new Schema({name: String, ...})
let User = mongoose.model('User', UserSchema);

let AdminSchema = new Schema({country:[String], ...}, {discriminatorKey: 'kind'}));
User.discriminator('Admin', AdminSchema);

but I use
User.find()....save() => will not update the admin related attributes
new Admin()....save() => will get duplicate key error

is it possible that update an existed user to admin?

help

Most helpful comment

Ok. So the right order is:

  1. convert mongoose document to object with toObject()
  2. change discriminator, and change/delete other properties
  3. convert back to mongoose document with hydrate()
  4. save

All 11 comments

I think right now the only way to do this is to specifically set the user kind to 'Admin' and then manually re-hydrate the doc.

User.findOne({}, function(error, user) {
  const userObj = user.toObject();
  userObj.kind = 'Admin';
  const admin = Admin.hydrate(userObj);
  // Should now be able to modify admin
});

Does that work?


@jackypan1989 can you let us know if that works for you, or are you looking for something more?

thanks @vkarpov15

@vkarpov15
Hi, your suggestion does not work. It did not change the discriminator.
I have created a repository to reproduce the issue. it is here:
https://github.com/shaozi/mongoose-hydrate-bug

@shaozi thanks for the repro instructions, my original example was incorrect. You need to convert to a POJO, then change the type, then hydrate, because mongoose documents explicitly disallow changing the discriminator key.

Ok. So the right order is:

  1. convert mongoose document to object with toObject()
  2. change discriminator, and change/delete other properties
  3. convert back to mongoose document with hydrate()
  4. save

@shaozi , can you give the example, i am trying to update the subschema and i have used the discriminator but i am not able to update the subschema data.

I think right now the only way to do this is to specifically set the user kind to 'Admin' and then manually re-hydrate the doc.

User.findOne({}, function(error, user) {
  const userObj = user.toObject();
  userObj.kind = 'Admin';
  const admin = Admin.hydrate(userObj);
  // Should now be able to modify admin
});

Does that work?

@vkarpov15 This still doesn't work for me with the latest version of mongoose (5.4.0)

Here is how to reproduce the issue:

const mongoose = require('mongoose');
const assert = require('assert');

mongoose.connect("mongodb://localhost:27017/discriminator", {useNewUrlParser: true});
mongoose.set('debug', true);

var options = {discriminatorKey: 'kind'};

var eventSchema = new mongoose.Schema({time: Date}, options);
var Event = mongoose.model('Event', eventSchema);

var ClickedLinkEvent = Event.discriminator('ClickedLink',
  new mongoose.Schema({url: String}, options));
var OtherLinkEvent = Event.discriminator('OtherLink',
  new mongoose.Schema({url: String}, options));

var id;

var doc = new Event({kind: 'ClickedLink'});
doc.save()
.then(function(doc){
    id = doc._id;

    const newDoc = OtherLinkEvent.hydrate(
        Object.assign(doc.toObject(), {kind: 'OtherLink'})
    );

    return newDoc.save();
})
.then(() => {
    return Event.findById(id)
})
.then(doc => {
    assert.equal(doc.kind, 'OtherLink');
    mongoose.connection.close();
})
.catch(err => {
    console.log(err);
    mongoose.connection.close();
});

@arkihillel looks like you're trying to just change the discriminator key. Remember that Model.hydrate() doesn't mark any paths as modified, so just add newDoc.markModified('kind') and your script works fine.

const mongoose = require('mongoose');
const assert = require('assert');

mongoose.connect("mongodb://localhost:27017/discriminator", {useNewUrlParser: true});
mongoose.set('debug', true);

var options = {discriminatorKey: 'kind'};

var eventSchema = new mongoose.Schema({time: Date}, options);
var Event = mongoose.model('Event', eventSchema);

var ClickedLinkEvent = Event.discriminator('ClickedLink',
  new mongoose.Schema({url: String}, options));
var OtherLinkEvent = Event.discriminator('OtherLink',
  new mongoose.Schema({url: String}, options));

var id;

var doc = new Event({kind: 'ClickedLink'});
doc.save()
.then(function(doc){
        id = doc._id;

        const newDoc = OtherLinkEvent.hydrate(
                Object.assign(doc.toObject(), {kind: 'OtherLink'})
        );
        newDoc.markModified('kind'); // <--- added this line

        return newDoc.save();
})
.then(() => {
        return Event.findById(id)
})
.then(doc => {
        assert.equal(doc.kind, 'OtherLink');
        mongoose.connection.close();
})
.catch(err => {
        console.log(err);
        mongoose.connection.close();
});

Changing the discriminator types works for me this way, thanks! But …:

I have e.g. three schemas, where two inherit from a parent. E.g.

  • shape as base schema with common properties
  • circle with a property diameter
  • quadrat with a property width

I’m changing a document from circle to quadrat (imagine a user how can choose between different shapes in a UI). I follow the above code, I set the width property, and save the document.

Now the document has the type quadrat (fine!), a width (yay!), but still contains the diameter property.

Is this “by design”, or is there any way to get rid of the now undesired property easily?

@qqilihq try using Model.replaceOne():

const mongoose = require('mongoose');
const assert = require('assert');

mongoose.connect("mongodb://localhost:27017/discriminator", {useNewUrlParser: true});
mongoose.set('debug', true);

var options = {discriminatorKey: 'kind'};

var shapeSchema = new mongoose.Schema({}, options);
var Shape = mongoose.model('Shape', shapeSchema);

var Circle = Shape.discriminator('Circle',
  new mongoose.Schema({radius: Number}, options));
var Square = Shape.discriminator('Square',
  new mongoose.Schema({width: Number}, options));

var _id;
var doc = new Shape({kind: 'Circle', radius: 5});
doc.save().
  then(function(doc) {
    _id = doc._id;
    return Shape.replaceOne({ _id }, { kind: 'Square', width: 10 });
  }).
  then(() => Square.findById(_id)).
  then(doc => console.log('Done', doc));
Was this page helpful?
0 / 5 - 0 ratings