7 Patterns to Refactor JavaScript Applications: Query Objects
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.
Share your thoughts with @engineyard on Twitter
OR
Talk about it on reddit