Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can I create unique index for subdocument? #2226

Closed
raitucarp opened this issue Aug 4, 2014 · 16 comments
Closed

Can I create unique index for subdocument? #2226

raitucarp opened this issue Aug 4, 2014 · 16 comments

Comments

@raitucarp
Copy link

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

@vkarpov15
Copy link
Collaborator

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.

@raitucarp
Copy link
Author

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

@vkarpov15
Copy link
Collaborator

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?

@stgogm
Copy link

stgogm commented May 24, 2016

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.

@vkarpov15
Copy link
Collaborator

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.

@stgogm
Copy link

stgogm commented May 30, 2016

@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 :)

@vkarpov15
Copy link
Collaborator

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...

@stgogm
Copy link

stgogm commented Jun 2, 2016

@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 :)

@vkarpov15
Copy link
Collaborator

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

@stgogm
Copy link

stgogm commented Jun 16, 2016

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);

@vkarpov15
Copy link
Collaborator

Great idea 👍 that should work well

@stgogm
Copy link

stgogm commented Jun 20, 2016

🙌

@vkarpov15 vkarpov15 reopened this Jun 22, 2016
@vkarpov15 vkarpov15 modified the milestone: 4.7 Jun 22, 2016
@wallawe
Copy link

wallawe commented Oct 4, 2017

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

@ksuhiyp
Copy link

ksuhiyp commented Nov 3, 2018

@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

@vkarpov15
Copy link
Collaborator

@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:

@theutkarshsoni
Copy link

Thanks to @vkarpov15 You saved me.

@Automattic Automattic locked and limited conversation to collaborators Sep 7, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants