81

Using node.js, mongodb on mongoHQ and mongoose. I'm setting a schema for Categories. I would like to use the document ObjectId as my categoryId.

var mongoose = require('mongoose');

var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;
var Schema_Category = new Schema({
    categoryId  : ObjectId,
    title       : String,
    sortIndex   : String
});

I then run

var Category = mongoose.model('Schema_Category');
var category = new Category();
category.title = "Bicycles";
category.sortIndex = "3";

category.save(function(err) {
  if (err) { throw err; }
  console.log('saved');
  mongoose.disconnect();     
});

Notice that I don't provide a value for categoryId. I assumed mongoose will use the schema to generate it but the document has the usual "_id" and not "categoryId". What am I doing wrong?

idophir
  • 14,451
  • 5
  • 24
  • 21

6 Answers6

136

Unlike traditional RBDMs, mongoDB doesn't allow you to define any random field as the primary key, the _id field MUST exist for all standard documents.

For this reason, it doesn't make sense to create a separate uuid field.

In mongoose, the ObjectId type is used not to create a new uuid, rather it is mostly used to reference other documents.

Here is an example:

var mongoose = require('mongoose');

var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;
var Schema_Product = new Schema({
    categoryId  : ObjectId, // a product references a category _id with type ObjectId
    title       : String,
    price       : Number
});

As you can see, it wouldn't make much sense to populate categoryId with a ObjectId.

However, if you do want a nicely named uuid field, mongoose provides virtual properties that allow you to proxy (reference) a field.

Check it out:

var mongoose = require('mongoose');

var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;
var Schema_Category = new Schema({
    title       : String,
    sortIndex   : String
});

Schema_Category.virtual('categoryId').get(function() {
    return this._id;
});

So now, whenever you call category.categoryId, mongoose just returns the _id instead.

You can also create a "set" method so that you can set virtual properties, check out this link for more info

addisonj
  • 2,136
  • 1
  • 15
  • 16
  • After setting a virtual property, I tried: db.cats.find({categoryId: ObjectId("the id")}) but got null results. when I use db.cats.find({_id: ObjectId("the id")}) I did get a doc back. So it looks like the virtual property cannot be used for searching. I think it could be easier to manage the code if it was possible to reference each ID using a clear name, rather than using _id for everything. Just a thought... – idophir Nov 14 '11 at 07:06
  • `ObjectId` should be referenced with `Schema.Types.ObjectId`. – sean Oct 25 '22 at 22:25
57

I was looking for a different answer for the question title, so maybe other people will be too.

To set type as an ObjectId (so you may reference author as the author of book, for example), you may do like:

const Book = mongoose.model('Book', {
  author: {
    type: mongoose.Schema.Types.ObjectId, // here you set the author ID
                                          // from the Author colection, 
                                          // so you can reference it
    required: true
  },
  title: {
    type: String,
    required: true
  }
});
jpenna
  • 8,426
  • 5
  • 28
  • 36
4

My solution on using ObjectId

// usermodel.js

const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ObjectId = Schema.Types.ObjectId


let UserSchema = new Schema({
   username: {
     type: String
   },
   events: [{
     type: ObjectId,
     ref: 'Event' // Reference to some EventSchema
   }]
})

UserSchema.set('autoIndex', true)

module.exports = mongoose.model('User', UserSchema)

Using mongoose's populate method

// controller.js

const mongoose = require('mongoose')
const User = require('./usermodel.js')

let query = User.findOne({ name: "Person" })

query.exec((err, user) => {
  if (err) {
     console.log(err)
  }

  user.events = events
  // user.events is now an array of events
})
devcodex
  • 101
  • 1
  • 5
2

The solution provided by @dex worked for me. But I want to add something else that also worked for me: Use

let UserSchema = new Schema({
   username: {
     type: String
   },
   events: [{
     type: ObjectId,
     ref: 'Event' // Reference to some EventSchema
   }]
})

if what you want to create is an Array reference. But if what you want is an Object reference, which is what I think you might be looking for anyway, remove the brackets from the value prop, like this:

let UserSchema = new Schema({
   username: {
     type: String
   },
   events: {
     type: ObjectId,
     ref: 'Event' // Reference to some EventSchema
   }
})

Look at the 2 snippets well. In the second case, the value prop of key events does not have brackets over the object def.

Chukwuma Nwaugha
  • 575
  • 8
  • 17
1

You can directly define the ObjectId

var Schema = new mongoose.Schema({ categoryId : mongoose.Schema.Types.ObjectId, title : String, sortIndex : String })

Note: You need to import the mongoose module

0

Another possible way is to transform your _id to something you like.

Here's an example with a Page-Document that I implemented for a project:

interface PageAttrs {
    label: string
    // ...
}

const pageSchema = new mongoose.Schema<PageDoc>(
    {
        label: {
            type: String,
            required: true
        }
        // ...
    },
    {
        toJSON: {
            transform(doc, ret) {
                // modify ret directly
                ret.id = ret._id
                delete ret._id
            }
        }
    }
)

pageSchema.statics.build = (attrs: PageAttrs) => {
    return new Page({
        label: attrs.label,
        // ...
    })
}

const Page = mongoose.model<PageDoc, PageModel>('Page', pageSchema)

Now you can directly access the property 'id', e.g. in a unit test like so:

it('implements optimistic concurrency', async () => {
    const page = Page.build({
        label: 'Root Page'
        // ...
    })

    await page.save()

    const firstInstance = await Page.findById(page.id)
    const secondInstance = await Page.findById(page.id)

    firstInstance!.set({ label: 'Main Page' })
    secondInstance!.set({ label: 'Home Page' })

    await firstInstance!.save()

    try {
        await secondInstance!.save()
    } catch (err) {
        console.error('Error:', err)
        return
    }

    throw new Error('Should not reach this point')
})

ShadowGames
  • 1,110
  • 1
  • 10
  • 15