Skip to content

Commit

Permalink
Added caching support
Browse files Browse the repository at this point in the history
In an effort to make Iridium the go-to ORM for Node.js applications 😉
we've implemented inline caching support - allowing you to easily cache
requests for single objects using your favourite caching engine (Redis/Memcache etc.).

As it stands, the cache is only hit for Model.get(_id)/Model.find(_id) and is automatically
updated when inserts and instance specific saves/refreshes/removals are executed.

It doesn't aim to be a be-all and end-all replacement for having a fast MongoDB server,
but it should allow you to offload some of the overhead on common operations to some
kind of KVS which is better suited to the task.
  • Loading branch information
notheotherben committed Jan 10, 2014
1 parent 9d99b21 commit dcba1ac
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 50 deletions.
4 changes: 2 additions & 2 deletions benchmarks/mongodb.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ MongoClient.connect('mongodb://localhost/iridium_bench', function(err, mDB) {
function(done) {
console.log('Iridium 10000 Inserts { w: 1, wrap: false }');
var start = new Date();
model.insert(objects, false, function(err, inserted) {
model.insert(objects, { wrap: false }, function(err, inserted) {
if(err) return done(err);
printTime(' => %s', start);
return done();
Expand All @@ -79,7 +79,7 @@ MongoClient.connect('mongodb://localhost/iridium_bench', function(err, mDB) {
function(done) {
console.log('Iridium find() { wrap: false }');
var start = new Date();
model.find({}, false, function(err, results) {
model.find({}, { wrap: false }, function(err, results) {
if(err) return done(err);
printTime(' => %s', start);
return done();
Expand Down
6 changes: 4 additions & 2 deletions lib/Instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Instance.prototype.refresh = Instance.prototype.update = function(callback) {
var conditions = this.__state.model.uniqueConditions(this.__state.original);
this.__state.model.collection.findOne(conditions, (function(err, latest) {
if(err) return onError(err);

this.__state.model.onRetrieved(latest, callback || function() { }, (function(value) {
this.__state.model.fromSource(value);
this.__state.original = _.cloneDeep(value);
Expand All @@ -160,7 +160,9 @@ Instance.prototype.remove = Instance.prototype.delete = function(callback) {
if(this.__state.isNew) return (callback || function() { })(null, 0);

var conditions = this.__state.model.uniqueConditions(this.__state.modified);
this.__state.model.collection.remove(conditions, { w: callback ? 1 : 0 }, callback);
this.__state.model.cache.drop(conditions._id, function() {
this.__state.model.collection.remove(conditions, { w: callback ? 1 : 0 }, callback);
});
};

Instance.prototype.__extendSchema = function() {
Expand Down
139 changes: 96 additions & 43 deletions lib/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var Instance = require('./Instance');
var validate = require('./utils/validation');
var Concoction = require('concoction');
var ObjectID = require('mongodb').ObjectID;
var NoOpCache = require('./caches/NoOpCache');


(require.modules || {}).Model = module.exports = Model;
Expand Down Expand Up @@ -76,6 +77,11 @@ function Model(database, collection, schema, options) {
enumerable: false
});

Object.defineProperty(this, 'cache', {
value: options.cache || new NoOpCache(),
enumerable: false
})


var extraValidators = [];
for(var i = 0; i < database.plugins.length; i++) {
Expand Down Expand Up @@ -167,7 +173,7 @@ Model.prototype.wrap = function (document, isNew) {
return new this.Instance(document, isNew);
};

Model.prototype.onRetrieved = function(results, callback, wrapper) {
Model.prototype.onRetrieved = function(results, callback, wrapper, options) {
///<signature>
///<summary>Handles any post-receive hooks and the wrapping of objects from the database</summary>
///<param name="results" type="Object">The object retrieved from the database</param>
Expand All @@ -190,9 +196,23 @@ Model.prototype.onRetrieved = function(results, callback, wrapper) {
///<param name="callback" type="Function">The function to be called once the objects have been wrapped</param>
///<param name="wrapper" type="Function">A function which converts the retrieved objects prior to submission</param>
///</signature>
///<signature>
///<summary>Handles any post-receive hooks and the wrapping of objects from the database</summary>
///<param name="results" type="Array" elementType="Object">The objects retrieved from the database</param>
///<param name="callback" type="Function">The function to be called once the objects have been wrapped</param>
///<param name="wrapper" type="Function">A function which converts the retrieved objects prior to submission</param>
///<param name="options" type="Object">A set of options determining how to handle the retrieved object</param>
///</signature>

var $ = this;
wrapper = (wrapper !== true && wrapper) || this.wrap.bind(this);

wrapper = wrapper || this.wrap.bind(this);
options = options || {};

_.defaults(options, {
wrap: true,
cache: true
});

var returnArray = Array.isArray(results);
if(!returnArray) results = [results];
Expand All @@ -216,12 +236,19 @@ Model.prototype.onRetrieved = function(results, callback, wrapper) {
doHook(this.options.hooks.retrieved, target, (function(err) {
if(err) return done(err);

var wrapped = wrapper(target);
var cacheDoc = _.cloneDeep(target);

var wrapped = options.wrap ? wrapper(target) : target;

doHook(this.options.hooks.ready, wrapped, function(err) {
doHook(this.options.hooks.ready, wrapped, (function(err) {
if(err) return done(err);
return done(null, wrapped);
});
if(options.cache)
return this.cache.store(cacheDoc, function() {
return done(null, wrapped);
});
else
return done(null, wrapped);
}).bind(this));
}).bind(this));
}).bind(this);
}, this), function(err, output) {
Expand Down Expand Up @@ -269,12 +296,10 @@ Model.prototype.onSaving = function(instance, changes, callback) {
}
}

var $ = this;

doHook($.options.hooks.saving, instance, [changes], callback);
doHook(this.options.hooks.saving, instance, [changes], callback);
};

Model.prototype.find = function (conditions, wrap, callback) {
Model.prototype.find = function (conditions, options, callback) {
/// <signature>
/// <summary>Gets all objects in the collection.</summary>
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
Expand All @@ -291,48 +316,54 @@ Model.prototype.find = function (conditions, wrap, callback) {
/// </signature>
/// <signature>
/// <summary>Gets all objects in the collection.</summary>
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
/// </signature>
/// <signature>
/// <summary>Finds all occurences in the collection with an _id field matching the given conditions.</summary>
/// <param name="conditions" type="Mixed">The _id field of the object to locate</param>
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
/// </signature>
/// <signature>
/// <summary>Finds all occurences in the collection which match the given conditions.</summary>
/// <param name="conditions" type="Object">The conditions which will be used to select matches</param>
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
/// </signature>

var args = Array.prototype.splice.call(arguments, 0);

conditions = {};
wrap = true;
conditions = null;
options = null;

for(var i = 0; i < args.length; i++) {
if('function' == typeof args[i])
callback = args[i];
else if('boolean' == typeof args[i])
wrap = args[i];
else
else if(!conditions)
conditions = args[i];
else options = args[i];
}

conditions = conditions || {};
options = options || {};
_.defaults(options, {
wrap: true,
cache: true
});

var $ = this;
if (!_.isPlainObject(conditions)) conditions = this.downstreamID(conditions);
this.toSource(conditions);

this.collection.find(conditions).toArray(function (err, results) {
if (err) return callback(err);
if (!results) return callback(null, null);
return $.onRetrieved(results, callback, wrap || function(value) { return value; });
return $.onRetrieved(results, callback, options.wrap || function(value) { return value; });
});
};

Model.prototype.findOne = Model.prototype.get = function (conditions, wrap, callback) {
Model.prototype.findOne = Model.prototype.get = function (conditions, options, callback) {
/// <signature>
/// <summary>Gets a single object from the collection.</summary>
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
Expand All @@ -349,51 +380,69 @@ Model.prototype.findOne = Model.prototype.get = function (conditions, wrap, call
/// </signature>
/// <signature>
/// <summary>Gets a single object from the collection.</summary>
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
/// </signature>
/// <signature>
/// <summary>Finds the first occurence in the collection with an _id field matching the given conditions.</summary>
/// <param name="conditions" type="Mixed">The _id field of the object to locate</param>
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
/// </signature>
/// <signature>
/// <summary>Finds the first occurence in the collection which matches the given conditions.</summary>
/// <param name="conditions" type="Object">The conditions which will be used to select matches</param>
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
/// </signature>

var args = Array.prototype.splice.call(arguments, 0);

conditions = {};
wrap = true;
conditions = null;
options = null;

for(var i = 0; i < args.length; i++) {
if('function' == typeof args[i])
callback = args[i];
else if('boolean' == typeof args[i])
wrap = args[i];
else
else if(!conditions)
conditions = args[i];
else options = args[i];
}

conditions = conditions || {};
options = options || {};
_.defaults(options, {
wrap: true,
cache: true
});

var $ = this;
if (!_.isPlainObject(conditions)) conditions = this.downstreamID(conditions);

var isID = !_.isPlainObject(conditions);

if (isID) conditions = this.downstreamID(conditions);
this.toSource(conditions);

this.collection.findOne(conditions, function (err, results) {
if (err) return callback(err);
if (!results) return callback(null, null);

return $.onRetrieved(results, callback, wrap || function(value) { return value; });
});

var fromDB = (function() {
this.collection.findOne(conditions, (function (err, results) {
if (err) return callback(err);
if (!results) return callback(null, null);

return this.onRetrieved(results, callback, null, { wrap: options.wrap, cache: options.cache });
}).bind(this));
}).bind(this);

if(isID && this.cache && options.cache)
this.cache.fetch(conditions._id, (function(err, doc) {
if(!err && doc)
return this.onRetrieved(doc, callback, null, { wrap: options.wrap, cache: false });
else
return fromDB();
}).bind(this));
else
return fromDB();
};

Model.prototype.insert = Model.prototype.create = function (object, wrap, callback) {
Model.prototype.insert = Model.prototype.create = function (object, options, callback) {
/// <signature>
/// <summary>Inserts the given object into the database</summary>
/// <param name="object" type="Object">The properties to set on the newly created object</param>
Expand All @@ -414,13 +463,13 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba
/// </signature>
/// <summary>Inserts the given object into the database</summary>
/// <param name="object" type="Object">The properties to set on the newly created object</param>
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
/// <param name="callback" type="Function">A function to be called once the object has been created</param>
/// </signature>
/// <signature>
/// <summary>Inserts the given object into the database</summary>
/// <param name="object" type="Array" elementType="Object">An array of objects representing the properties to set on the newly created objects</param>
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
/// <param name="callback" type="Function">A function to be called once the objects have been created</param>
/// </signature>

Expand All @@ -429,10 +478,14 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba
var returnArray = true;

if(!callback) {
callback = wrap;
wrap = true;
callback = options;
options = options || {};
}

_.defaults(options, {
wrap: true
});

if(!Array.isArray(object)) {
object = [object];
returnArray = false;
Expand All @@ -454,7 +507,7 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba
$.collection.insert(prepped, { w: callback ? 1 : 0 }, function(err, inserted) {
if(err) return end(err);
if(callback)
return $.onRetrieved(inserted, end, wrap || function(value) { return value; });
return $.onRetrieved(inserted, end, null, options);
return end();
});
};
Expand Down
30 changes: 30 additions & 0 deletions lib/caches/NoOpCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module.exports = NoOpCache;

function NoOpCache(options) {
/// <summary>Creates a new cache which performs no caching of instances</summary>
/// <param name="options" type="Object">Options dictating the configuration of this cache</param>
}

NoOpCache.prototype.store = function(document, callback) {
/// <summary>Stores a document in the cache for future access</summary>
/// <param name="document" type="Object">The database object to store in the cache</param>
/// <param name="callback" type="Function">A function which is called once the document has been stored</param>

return callback();
};

NoOpCache.prototype.fetch = function(id, callback) {
/// <summary>Fetches the document with the matching id from the cache</summary>
/// <param name="id" type="Mixed">The _id field of the document to retrieve from the cache</param>
/// <param name="callback" type="Function">A function to call with the retrieved value</param>

return callback(null);
};

NoOpCache.prototype.drop = function(id, callback) {
/// <summary>Removes the document with the matching id from the cache</summary>
/// <param name="id" type="Mixed">The _id field of the document to remove from the cache</param>
/// <param name="callback" type="Function">A function to call once the document has been removed from the cache</param>

return callback(null);
};
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@
"concoction": "*"
},
"devDependencies": {
"async": "*",
"mocha": "*",
"should": "*",
"gitlablist-mocha": "*"
"should": "*"
}
}

0 comments on commit dcba1ac

Please sign in to comment.