Search code examples
node.jsmongodbmongoosemongodb-querymongoose-schema

Find ObjectId _id but Schema has defined _id as String


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: {} })

Solution

  • 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
        },
    });