Mongoose: Can I create unique index for subdocument?

Created on 4 Aug 2014  ·  16Comments  ·  Source: Automattic/mongoose

Is it possible to create unique index for subdocument? I mean, it may duplicate in other subdocument, but not same parent,

Most helpful comment

I figured out something else that seems to work, even in simultaneous updates.

Let's assume subdocs.name is the value we want unique:

Model.update({
  _id: '576328595b2880f831413b92',
  'subdocs.name': {
    $ne: 'Unique Name'
  }
}, {
  $push: {
    subdocs: {
      name: 'Unique Name'
    }
  }
}).then((raw) => {
  // check raw.nModified value
}).catch(next);

All 16 comments

A combination of an index on the subdocument and the $addToSet operator should give you what you're looking for. Just use $addToSet whenever you're adding to the array and you should prevent duplicates within the parent document.

$addToSet is works, but it detect other subdocument in different parent too, correct me if i'm wrong, because i just tested it

I guess I'm not understanding what you mean by subdocument in a different parent. Can you provide a code example that shows what you're trying to do?

Sorry for the bump but when looking for a solution, I figured this could help in the future:

const schema = new Schema({
  value: String,
  another: Number,
  subdocs: [{
    prop: String, /* Should be unique */
    value: Number
  }]
});

/**
 * Check for duplicated sub document properties.
 */
schema.pre('validate', function validate(next) {
  var unique = [];

  for (var i = 0, l = this.subdocs.length; i < l; i++) {
    let prop = this.subdocs[i].prop;

    if (unique.indexOf(prop) > -1) {
      return next(new Error('Duplicated sub document!'));
    }

    unique.push(prop);
  }

  next();
});

Explanation:

Let's asume that we have a schema with a subdocs property which contains a set of subdoc with a property called prop that must be unique. The idea is to keep track of the duplicated properties in an array called unique and check if the next one is already in the unique array.

I know it's not super-efficient but it works.

Sure that works most of the time, but keep in mind that when you do a .push() to an array and then .save(), that becomes a $push to mongodb, so you can have 2 documents think they're saving a unique subdoc because they don't know they're .push()-ing the same one.

@vkarpov15 The push is made to the unique array that only holds the values you want to be unique so you can check if any new values are also unique. It's intended to be used after you've made a push or addToSet but before saving the document. The unique array is temporary and It's not part of the mongo document :)

Nah that's beside the point. Suppose you're trying to save 2 documents at roughly the same time from different JS programs - both do a .find() for the document, push on a sub-document with prop = 'test', and try to save. Both will think that it's ok to insert prop = 'test' because their local copies of the document don't have that subdoc yet, so both will successfully $push it into mongodb. The issue is that doing .push() on a mongoose array translates to a $push to mongodb, so there's no good way for us to enforce uniqueness within an array doc for all array operations...

@vkarpov15 good point, didn't thought it for simultaneous inserts. Well, this seems a lot more complex but I'll try to figure it out :)

Well your approach works as long as you call markModified on the props array _before_ calling push, then mongoose will always overwrite the whole array when saving to mongodb, so you don't have the same race conditions

I figured out something else that seems to work, even in simultaneous updates.

Let's assume subdocs.name is the value we want unique:

Model.update({
  _id: '576328595b2880f831413b92',
  'subdocs.name': {
    $ne: 'Unique Name'
  }
}, {
  $push: {
    subdocs: {
      name: 'Unique Name'
    }
  }
}).then((raw) => {
  // check raw.nModified value
}).catch(next);

Great idea 👍 that should work well

🙌

@stgogm you just ended a 5 hour search, and for that, I'm grateful.

@vkarpov15 can you give me an example of how $addToSet can assure sub-document field uniqueness

Ive tried this

await Course.updateOne({ _id: id }, { $addToSet: { 'units.level': { name: body.name, level: body.level} } }) unitSchema.index({ level: 1 })
But unit.level still accepts duplicates

@ksuhiyp unitSchema.index({ level: 1 }, { unique: true }) and make sure you also set _id to false in unitSchema, like:

const unitSchema = new Schema({ name: String, level: Number }, { _id: false })

$addToSet works with subdocuments, but it looks for exact deep equality between documents, so if a subdoc has an _id then $addToSet will never catch a duplicate. See:

Thanks to @vkarpov15 You saved me.

Was this page helpful?
0 / 5 - 0 ratings