Find ObjectId _id but Schema has defined _id as String

Viewed 1231

Previously I didn't declare the _id in my Schema, so each new submission naturally will have MongoDB generated ObjectId as it's _id. However, the requirements have changed and now _id is declared as String as below.

var mongoose = require("mongoose");
var Schema = mongoose.Schema;

var MySchema = new Schema({
    _id: {
        type: String,
    },
    schoolID: {
        type: mongoose.Schema.Types.ObjectId, ref: 'School'
    },
    points: {
        type: Number
    },
});
MySchema.index({ schoolID : 1})

module.exports = mongoose.model('Submission', MySchema);

However, now I cannot find my previously inserted documents using _id at all. I tried

var submissionId = "60654319a8062f684ac8fde4"
Submission.findOne({ _id: mongoose.mongo.ObjectId(submissionId ) })
Submission.findOne({ _id: mongoose.Types.ObjectId(submissionId ) })
Submission.findOne({ _id: mongoose.ObjectId(submissionId ) })

but it will always return null. So when I use var mongoose = require('mongoose').set('debug', true); to check, it will display below; that all my queries above will still find using String, and not ObjectId

Mongoose: submission.findOne({ _id: '60654319a8062f684ac8fde4' }, { projection: {} })
2 Answers

Problem - _id: { type: String,}, mongoose will cast the value before making a query to the database, so in your case, it will always be a string.

Option -1

Convert your old objectId's to String as you're planning to use String _id so it's better to keep it consistent.

Run these commands from the shell, Robomongo

This will add records with strings _id for existing ObjectId records.

db.collection.find({}).toArray() // loop all records
    .forEach(function (c) {
        if (typeof c._id !== 'string') { // check _id is not string
            c._id = c._id.str; db.collection.save(c); // create new record with _id as string value
        }
    });

Delete records with ObjectId

db.collection.remove({ _id: { $type: 'objectId' } })

Option -2

Add Mongoose custom types.

https://mongoosejs.com/docs/customschematypes.html

class StringOrObjectId extends mongoose.SchemaType {
  constructor(key, options) {
    super(key, options, 'StringOrObjectId');
  }

  convertToObjectId(v) {
    const checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
    let _val;
    try {
      if (checkForHexRegExp.test(v)) {
        _val = mongoose.Types.ObjectId(v);
        return _val;
      }
    } catch (e) {
      console.log(e);
    }
  }

  convertToString(v) {
    let _val = v;
    try {
      if(_.isString(_val)) return _val;
      if(_.isNumber(_val)) return _.toString(_val);
    } catch (e) {
      console.log(e);
    }
  }

  cast(val) {
    const objectIdVal = this.convertToObjectId(val);
    if (objectIdVal) return objectIdVal;

    const stringVal = this.convertToString(val)
    if (stringVal) return stringVal;

    throw new Error('StringOrObjectId: ' + val +
        ' Nor string nor ObjectId');
  }
}

mongoose.Schema.Types.StringOrObjectId = StringOrObjectId;

var MySchema = new Schema({
    _id: {
        type: StringOrObjectId, // custom type here
    },
    schoolID: {
        type: mongoose.Schema.Types.ObjectId, ref: 'School'
    },
    points: {
        type: Number
    },
});

Query

Submission.findOne({ _id: submissionId }); // it will cast ObjectId or String or throw error

Drawbacks

  • If your _id type string is 60516ae1ef682d2804a2fa72 is like valid ObjectId it will convert to ObjectId which will not match the record.

Note - It's a rough class StringOrObjectId add proper checks and test properly.


Option -3

Easy way - Use mongoose.Mixed https://mongoosejs.com/docs/schematypes.html#mixed

https://mongoosejs.com/docs/api.html#mongoose_Mongoose-Mixed

cont MySchema = new Schema({
    _id: {
        type: mongoose.Mixed,
    },
    schoolID: {
        type: mongoose.Schema.Types.ObjectId, ref: 'School'
    },
    points: {
        type: Number
    },
});

There is no straight way to handle this situation in mongoose,

Mongoose casts the filter to match the model's schema before the command is sent. See query casting tutorial for more information on how Mongoose casts filter.

You can try custom schema types,

class StrOrObjId extends mongoose.SchemaType {
    constructor(key, options) {
        super(key, options, 'StrOrObjId');
    }
    cast(val) {
        if (typeof val !== 'string' && !mongoose.Types.ObjectId.isValid(val)) {
            throw new Error('StrOrObjId: ' + val + ' must be a String or ObjectId');
        }
        return val;
    }
}
// Don't forget to add `StrOrObjId` to the type registry
mongoose.Schema.Types.StrOrObjId = StrOrObjId;

Than use that type in _id field,

var MySchema = new Schema({
    _id: {
        type: StrOrObjId,
    },
    schoolID: {
        type: mongoose.Schema.Types.ObjectId, ref: 'School'
    },
    points: {
        type: Number
    }
});
Related