I’ve been playing around with node, JavaScript & MongoDB a bit with this project and one of the things I ran into was MongoDB’s lack of a sequential ID. MongoDB defaults to ObjectIDs for primary keys in collections, and they have good reasons for doing so, but in writing this app, I’d rather have URLs look like “/question/423” than “/question/3001024e521e9c6500000000”.
Fortunately, there’s a relatively convenient workaround for this problem, detailed here. Essentially, create a collection in which each document is a counter. The ID for the document is its “name” and the property “next” contains the next value. This is similar to creating and using a sequence in PostgreSQL, though hopefully in the future they’ll provide an easier way to do this – perhaps a built-in sequence object.
First, in the mongo shell, I created the counter(name)
function as specified in the Mongo wiki:
> counter function counter(name) { var ret = db.counters.findAndModify({query:{_id:name}, update:{$inc:{next:1}}, 'new':true, upsert:true}); return ret.next; }
Then I created the counter document for the “questions” collection (so questions can have numeric IDs):
db.counters.insert({_id:"questions", next: 1});
This is optional – the counter() function will create the document in the counters collection if needed, with an initial value of 1.
That’s it. Each call to counter() will now return an incrementing number:
> db.counters.find(); { "_id" : "questions", "next" : 3 } > counter("joe"); 1 > db.counters.find(); { "_id" : "questions", "next" : 3 } { "_id" : "joe", "next" : 1 } > counter("joe"); 2 > counter("joe"); 3
Cool. So how do we save the counter() function in the DB so we can use it in queries without having to define it every time? Fortunately Mongo makes saving functions on the server easy:
> db.system.js.save( { _id : "counter" , value : counter }); > db.system.js.find(); { "_id" : "counter", "value" : function cf__2__f_counter(name) { var ret = db.counters.findAndModify({query:{_id:name}, update:{$inc:{next:1}}, 'new':true, upsert:true}); return ret.next; } }
Calling the stored procedure directly in the shell is sort of strange, but it works:
> db.eval("return counter('question')"); 3 > db.eval("return counter('question')"); 4 > bye [Wed Jun 22 13:14:10 evan@EvanMBP 3 ~]$ ~/Downloads/mongodb-osx-x86_64-1.8.1/bin/mongo questionsMongoDB shell version: 1.8.1 connecting to: questions > db.eval("return counter('question')"); 5
Unfortunately, I was unable to figure out how to call the stored function from within the mongoskin driver, and ended up writing analogous code, calling findAndModify()
within the app code. I finally got it:
db.collection('counters').findAndModify( {_id:'questions'}, [], {$inc : {next: 1}}, true, true, function(err, counter) { if (err) { throw new Error(err); } var ins = { date: new Date(), author: req.body.author, body: req.body.body, tags: tags, tag_count: tagCount, answers: [], votes: 0, _id: counter.next }; db.collection('questions').insert(ins, {}); res.end('Added new question: '+req.body.body); } );
I tried throwing some code in there to query by either ObjectId() or integer but it didn’t really work (Edit: I tried this again on Linux and it worked fine), so I just deleted all the documents with ObjectIds and everything seems to work fine now. I think the next step is going to have to be some sort of authentication/session stuff, because typing your name in every time kind of sucks (and makes it hard to do things like list questions by user).
Full code for this revision is here.