7 Patterns to Refactor JavaScript Applications: Query Objects

Note: This is part four (prev/next) of a seven part series that starts here.

Database queries, even simple ones, can be both repetitive and hard to comprehend. With more complex queries, especially ones that embed data from multiple collections or tables, this process can become downright unintelligible.

Query objects provide a nice tool for extracting database query logic and associated operations into a contained module, pulling the logic out into a more maintainable and readable structure, while also providing a very readable API where the query object is used.

Examples

Let’s imagine the operations around creating a student record from a form. The developer could use a Form Object for validation and feedback, and a Service Object for persistence. The Service Object, which could be called CreateStudent, would persist the data to the database. Without using a Query Object, the method for saving the user to the database could look like this:

var MongoClient = require('mongodb').MongoClient

var createStudentQuery = function(student, callback) {
  MongoClient.connect('mongodb://127.0.0.1:27017/test', function(err, db) {
    if (err) throw err;

    var collection = db.collection('students');
    collection.insert(student, function(err, docs) {

      collection.find().toArray(function(err, result) {
        db.close();
        callback(result);
      });
    });
  });
}

Not only are we entering into some deep layers of callback hell, we’ve also written some code that is very difficult to read.

We can create a much more expressive module if we use a Query Object:

var _ = require('underscore')
var async = require('async')
var MongoClient = require('mongodb').MongoClient

var CreateStudentQuery = function(student) {
  this.student = student
}

CreateStudentQuery.prototype = _.extend(CreateStudentQuery, {

  run: function(callback) {
    async.waterfall([
      _.bind(this.connectToDatabase, this),
      _.bind(this.getStudentsCollection, this),
      _.bind(this.insertStudent, this),
      _.bind(this.closeConnection, this)
    ], callback)
  },

  connectToDatabase: function(callback) {
    MongoClient.connect(CreateStudentQuery.DATABASE_URL, callback)
  },

  getStudentsCollection: function(db, callback) {
    this.db = db
    callback(null, db.collection(CreateStudentQuery.COLLECTION_NAME))
  },

  insertStudent: function(collection, callback) {
    collection.insert(this.student, callback)
  },

  closeConnection: function(result, callback) {
    this.db.close()
    callback(null, result)
  }

})

CreateStudentQuery.DATABASE_URL = 'mongodb://127.0.0.1:27017/test'
CreateStudentQuery.COLLECTION_NAME = 'students'

Encapsulating all associated operations for this query feels more organized and gives you an expressive API to integrate into your application.

This could be the Service Object for creating a user:

var CreateStudent = function(student) {
  this.student = student
}

CreateStudent.prototype = _.extend(CreateStudent.prototype, {
  
  run: function() {
    ...
  },

  persist: function() {
    return new CreateStudentQuery(this.student).run()
  },

  ...

})

Testing

Constructing Query Objects outside of the context in which they are used makes testing supremely simple. If you’re using a testing database, just run the Query Object and make sure the results are accurate!

var expect = require('chai').expect;
var CreateStudentQuery = require('./createStudentQuery');

describe('CreateStudentQuery', function(){

  before(function(done){
    this.student = {
      first_name: 'John',
      last_name: 'Smith',
      gender: 'M'
    }

    var callback = _.bind(function(err, result) {
      this.err = err
      this.result = result
      done()
    }, this)

    // set the collection being affected as a test table
    // or set the DATABASE_URL to a test database instead.
    CreateStudentQuery.COLLECTION_NAME = 'students_test'

    new CreateStudentQuery(this.student).run(callback)
  });

  // perform checks to make sure that the returned object is 
  // has the same values as the original object but also
  // has new database values (such as _id in Mongo)
  it('returns a new student database object', function(){
    expect(this.result.first_name).to.equal(this.student.first_name)
    expect(this.results._id).to.not.be.undefined
  });

});

Conclusion

A Query Object can be a great way to make your code base more readable and testable, even if the query is only one or two lines long. Remember that code is usually written once but read many times, so any effort towards making your code more readable is a worthwhile pursuit.

In the next post, we’ll take a look at View Objects, which are great tools for isolating view-specific model transformations.

The post was originally written for the Crush & Lovely Blog in 2012 and has been lovingly brought up-to-date for Engine Yard by Michael Phillips, the original author. Special thanks to Justin Reidy and Anisha Vasandani for help with the original.

About Michael Phillips

Michael was born in Portland, Oregon but spent a decade living and working in New York City. With experience working on product teams as well as with consulting companies, he has worked on web and mobile applications ranging from greenfield startups to large corporations. He is most interested in client-side JavaScript application development, but also loves learning about engineering leadership, programming culture and best practices, and client relationship management. Michael lives in Denver, Colorado with his wife Alison and currently works for Quick Left.