Created
May 2, 2012 07:53
-
-
Save jsocol/2574884 to your computer and use it in GitHub Desktop.
Mongolian + Deferred = Tolerable Mongo in Node
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Imagine, if you will, a simple task. You have a blog written in Node, with | |
| * its content stored in Mongo, and you want to display a single post (for | |
| * simplicity, let's pretend the URL contains the ObjectID) and its comments. | |
| * | |
| * Normally, you'd have to fall into callback/scope hell, and do something | |
| * along the lines of this, already in an onRequest handler: | |
| */ | |
| function displayPost(request, response) { | |
| // We'll look up the post and its comments. | |
| var id = request.get('id'); // Or something similarly easy. | |
| posts.findOne({"_id": id}, function(err, post) { | |
| if (err) throw Http404(); | |
| // Mongolian seems to lack DBRefs right now. | |
| comments.find({"post_id": id}).toArray(function(err, comms) { | |
| if (err) throw Http500(); | |
| response.write(someTemplate.render(post, comms)); | |
| response.end(); | |
| }); | |
| }); | |
| } | |
| /** | |
| * I have utterly glossed over error handling. It's wrong, I don't care, it gets | |
| * the point across. I have seen good developers write exactly this kind of mess. | |
| * | |
| * Because you need both the post and the comments (comms) to render the page, | |
| * the naive solution is to do two asynchronous look-ups in *serial* rather than | |
| * in parallel, and then deeply couple/nest the actual rendering and sending the | |
| * response within the look-up code. That's insane. | |
| * | |
| * First, both lookups are asynchronous. Even though they are related, they don't | |
| * (have to) depend on each other, so we should really run them in parallel. | |
| * Sometimes they'll need to be serial--and in those cases we can still use a | |
| * better method, because... | |
| * | |
| * Second, we need to separate them. Getting the comments right now needs the whole | |
| * post in scope--even if it *does* depend on the post object and not just the id, | |
| * that should be passed in as an argument, not in the scope. That makes it possible | |
| * to test "getting the comments", a much smaller unit. It also lets us test "getting | |
| * the post" without having to worry about the comments. Smaller units: more testable, | |
| * more comprehensible, more good. | |
| * | |
| * Third, we're mixing concerns like a novice PHP developer. Having a template render | |
| * in the middle of a database query is just as bad as | |
| * | |
| * <h1><?= mysql_query($my, "SELECT title FROM posts WHERE..."); ?></h1> | |
| * | |
| * The template render, again, ends up depending on scope rather than passing values, | |
| * which makes it nearly impossible to test on its own, and extremely deeply nested. | |
| * Just imagine dealing with errors at any level in this. | |
| * | |
| * But there's a better way! | |
| * | |
| */ | |
| var Mongolian = require('mongolian'), | |
| Deferred = require('Deferred'); | |
| var server = new Mongolain; | |
| var db = server.db('blog'); | |
| var posts = db.collection('posts'); | |
| var comments = db.collection('comments'); | |
| /* For example... | |
| posts.insert({ | |
| "title": "f1rst", | |
| "content": "the first post!" | |
| }); | |
| posts.insert({ | |
| "title": "second", | |
| "content": "the second post!" | |
| }); | |
| */ | |
| // These two functions are totally independent, testable, and do not in any | |
| // way depend on scope. Everything necessary is passed in via an argument. | |
| function getPost(o, postsColl) { | |
| var query = new Deferred(); | |
| postsColl.findOne(o || {}, function(err, post) { | |
| if (err) | |
| return query.reject(err); | |
| return query.resolve(post); | |
| }); | |
| return query.promise(); | |
| } | |
| function getComments(o, commentsColl) { | |
| var query = new Deferred(); | |
| commentsColl.find(o || {}).toArray(function(err, comms) { | |
| if (err) | |
| return query.reject(err); | |
| return query.resolve(comms); | |
| }); | |
| return query.promise(); | |
| } | |
| // This is a kind of nonsensical, pseudo-way to do this: | |
| function displayPost(id, template, stream) { | |
| var lookup = Deferred.when( | |
| getPost({"_id":id}, posts), | |
| getComments({"post":id}, comments) | |
| ); | |
| lookup.then( | |
| function(post, comments) { | |
| stream.write(template.render(post, comments)); | |
| }, | |
| function(postErr, commentsErr) { | |
| stream.write(errorTemplate.render([postErr, commentsErr]); | |
| } | |
| ); | |
| } | |
| /** | |
| * If template compiling/rendering is slow, we can make that a deferred, too! | |
| * This function is, again, totally testable and isolated. Everything necessary | |
| * is passed into it via arguments. Any test suite capable of handling async | |
| * code can test this with: | |
| * | |
| * renderTemplate('foo', {}).then(test.pass, test.fail); | |
| * | |
| * Or something more complicated, that makes real assertions. | |
| */ | |
| function renderTemplate(template, data) { | |
| var render = new Deferred(); | |
| // Maybe 'template' is a filename and we have to read it off the | |
| // filesystem, then do the parsing and rendering. Here let's just | |
| // pretend it's generically async. | |
| template.render(data, function(err, html) { | |
| if (err) | |
| return render.reject(err); | |
| return render.resolve(html); | |
| }); | |
| return render.promise(); | |
| } | |
| /** | |
| * This is more of a legitimate view from something like Express. | |
| * I've still glossed over some things. | |
| * Look! Look how little nesting! How things are atomic and testable! | |
| * I still glossed over some things like status codes and not-founds | |
| * vs legit query errors. | |
| * It depends a _little_ on scope because I get the mongo collections | |
| * from the global scope, but that's easily fixable, if it needs to | |
| * be fixed at all. | |
| */ | |
| function displayPost(request, response) { | |
| var id = request.get('id'); | |
| var lookup = Deferred.when( | |
| getPost({"_id":id}, posts), | |
| getComments({"post":id}, comments) | |
| ); | |
| lookup.done(function(post, comments) { | |
| renderTemplate( | |
| 'post.ejs', | |
| {'post': post, 'comments': comments} | |
| ).done(response.end); | |
| }).fail(function(postErr, commentsErr) { | |
| renderTemplate( | |
| 'error.ejs', | |
| {'errors': [postErr, commentsErr]} | |
| ).done(response.end); | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is awesome man, good work!