Skip to content

Commit dcba1ac

Browse files
committed
Added caching support
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.
1 parent 9d99b21 commit dcba1ac

File tree

6 files changed

+239
-50
lines changed

6 files changed

+239
-50
lines changed

benchmarks/mongodb.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ MongoClient.connect('mongodb://localhost/iridium_bench', function(err, mDB) {
6161
function(done) {
6262
console.log('Iridium 10000 Inserts { w: 1, wrap: false }');
6363
var start = new Date();
64-
model.insert(objects, false, function(err, inserted) {
64+
model.insert(objects, { wrap: false }, function(err, inserted) {
6565
if(err) return done(err);
6666
printTime(' => %s', start);
6767
return done();
@@ -79,7 +79,7 @@ MongoClient.connect('mongodb://localhost/iridium_bench', function(err, mDB) {
7979
function(done) {
8080
console.log('Iridium find() { wrap: false }');
8181
var start = new Date();
82-
model.find({}, false, function(err, results) {
82+
model.find({}, { wrap: false }, function(err, results) {
8383
if(err) return done(err);
8484
printTime(' => %s', start);
8585
return done();

lib/Instance.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Instance.prototype.refresh = Instance.prototype.update = function(callback) {
142142
var conditions = this.__state.model.uniqueConditions(this.__state.original);
143143
this.__state.model.collection.findOne(conditions, (function(err, latest) {
144144
if(err) return onError(err);
145-
145+
146146
this.__state.model.onRetrieved(latest, callback || function() { }, (function(value) {
147147
this.__state.model.fromSource(value);
148148
this.__state.original = _.cloneDeep(value);
@@ -160,7 +160,9 @@ Instance.prototype.remove = Instance.prototype.delete = function(callback) {
160160
if(this.__state.isNew) return (callback || function() { })(null, 0);
161161

162162
var conditions = this.__state.model.uniqueConditions(this.__state.modified);
163-
this.__state.model.collection.remove(conditions, { w: callback ? 1 : 0 }, callback);
163+
this.__state.model.cache.drop(conditions._id, function() {
164+
this.__state.model.collection.remove(conditions, { w: callback ? 1 : 0 }, callback);
165+
});
164166
};
165167

166168
Instance.prototype.__extendSchema = function() {

lib/Model.js

Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ var Instance = require('./Instance');
1010
var validate = require('./utils/validation');
1111
var Concoction = require('concoction');
1212
var ObjectID = require('mongodb').ObjectID;
13+
var NoOpCache = require('./caches/NoOpCache');
1314

1415

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

80+
Object.defineProperty(this, 'cache', {
81+
value: options.cache || new NoOpCache(),
82+
enumerable: false
83+
})
84+
7985

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

170-
Model.prototype.onRetrieved = function(results, callback, wrapper) {
176+
Model.prototype.onRetrieved = function(results, callback, wrapper, options) {
171177
///<signature>
172178
///<summary>Handles any post-receive hooks and the wrapping of objects from the database</summary>
173179
///<param name="results" type="Object">The object retrieved from the database</param>
@@ -190,9 +196,23 @@ Model.prototype.onRetrieved = function(results, callback, wrapper) {
190196
///<param name="callback" type="Function">The function to be called once the objects have been wrapped</param>
191197
///<param name="wrapper" type="Function">A function which converts the retrieved objects prior to submission</param>
192198
///</signature>
199+
///<signature>
200+
///<summary>Handles any post-receive hooks and the wrapping of objects from the database</summary>
201+
///<param name="results" type="Array" elementType="Object">The objects retrieved from the database</param>
202+
///<param name="callback" type="Function">The function to be called once the objects have been wrapped</param>
203+
///<param name="wrapper" type="Function">A function which converts the retrieved objects prior to submission</param>
204+
///<param name="options" type="Object">A set of options determining how to handle the retrieved object</param>
205+
///</signature>
193206

194207
var $ = this;
195-
wrapper = (wrapper !== true && wrapper) || this.wrap.bind(this);
208+
209+
wrapper = wrapper || this.wrap.bind(this);
210+
options = options || {};
211+
212+
_.defaults(options, {
213+
wrap: true,
214+
cache: true
215+
});
196216

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

219-
var wrapped = wrapper(target);
239+
var cacheDoc = _.cloneDeep(target);
240+
241+
var wrapped = options.wrap ? wrapper(target) : target;
220242

221-
doHook(this.options.hooks.ready, wrapped, function(err) {
243+
doHook(this.options.hooks.ready, wrapped, (function(err) {
222244
if(err) return done(err);
223-
return done(null, wrapped);
224-
});
245+
if(options.cache)
246+
return this.cache.store(cacheDoc, function() {
247+
return done(null, wrapped);
248+
});
249+
else
250+
return done(null, wrapped);
251+
}).bind(this));
225252
}).bind(this));
226253
}).bind(this);
227254
}, this), function(err, output) {
@@ -269,12 +296,10 @@ Model.prototype.onSaving = function(instance, changes, callback) {
269296
}
270297
}
271298

272-
var $ = this;
273-
274-
doHook($.options.hooks.saving, instance, [changes], callback);
299+
doHook(this.options.hooks.saving, instance, [changes], callback);
275300
};
276301

277-
Model.prototype.find = function (conditions, wrap, callback) {
302+
Model.prototype.find = function (conditions, options, callback) {
278303
/// <signature>
279304
/// <summary>Gets all objects in the collection.</summary>
280305
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
@@ -291,48 +316,54 @@ Model.prototype.find = function (conditions, wrap, callback) {
291316
/// </signature>
292317
/// <signature>
293318
/// <summary>Gets all objects in the collection.</summary>
294-
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
319+
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
295320
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
296321
/// </signature>
297322
/// <signature>
298323
/// <summary>Finds all occurences in the collection with an _id field matching the given conditions.</summary>
299324
/// <param name="conditions" type="Mixed">The _id field of the object to locate</param>
300-
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
325+
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
301326
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
302327
/// </signature>
303328
/// <signature>
304329
/// <summary>Finds all occurences in the collection which match the given conditions.</summary>
305330
/// <param name="conditions" type="Object">The conditions which will be used to select matches</param>
306-
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
331+
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
307332
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
308333
/// </signature>
309334

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

312-
conditions = {};
313-
wrap = true;
337+
conditions = null;
338+
options = null;
314339

315340
for(var i = 0; i < args.length; i++) {
316341
if('function' == typeof args[i])
317342
callback = args[i];
318-
else if('boolean' == typeof args[i])
319-
wrap = args[i];
320-
else
343+
else if(!conditions)
321344
conditions = args[i];
345+
else options = args[i];
322346
}
323347

348+
conditions = conditions || {};
349+
options = options || {};
350+
_.defaults(options, {
351+
wrap: true,
352+
cache: true
353+
});
354+
324355
var $ = this;
325356
if (!_.isPlainObject(conditions)) conditions = this.downstreamID(conditions);
326357
this.toSource(conditions);
327358

328359
this.collection.find(conditions).toArray(function (err, results) {
329360
if (err) return callback(err);
330361
if (!results) return callback(null, null);
331-
return $.onRetrieved(results, callback, wrap || function(value) { return value; });
362+
return $.onRetrieved(results, callback, options.wrap || function(value) { return value; });
332363
});
333364
};
334365

335-
Model.prototype.findOne = Model.prototype.get = function (conditions, wrap, callback) {
366+
Model.prototype.findOne = Model.prototype.get = function (conditions, options, callback) {
336367
/// <signature>
337368
/// <summary>Gets a single object from the collection.</summary>
338369
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
@@ -349,51 +380,69 @@ Model.prototype.findOne = Model.prototype.get = function (conditions, wrap, call
349380
/// </signature>
350381
/// <signature>
351382
/// <summary>Gets a single object from the collection.</summary>
352-
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
383+
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
353384
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
354385
/// </signature>
355386
/// <signature>
356387
/// <summary>Finds the first occurence in the collection with an _id field matching the given conditions.</summary>
357388
/// <param name="conditions" type="Mixed">The _id field of the object to locate</param>
358-
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
389+
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
359390
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
360391
/// </signature>
361392
/// <signature>
362393
/// <summary>Finds the first occurence in the collection which matches the given conditions.</summary>
363394
/// <param name="conditions" type="Object">The conditions which will be used to select matches</param>
364-
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
395+
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
365396
/// <param name="callback" type="Function">A function to be called with the results once they have been retrieved.</param>
366397
/// </signature>
367398

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

370-
conditions = {};
371-
wrap = true;
401+
conditions = null;
402+
options = null;
372403

373404
for(var i = 0; i < args.length; i++) {
374405
if('function' == typeof args[i])
375406
callback = args[i];
376-
else if('boolean' == typeof args[i])
377-
wrap = args[i];
378-
else
407+
else if(!conditions)
379408
conditions = args[i];
409+
else options = args[i];
380410
}
381411

412+
conditions = conditions || {};
413+
options = options || {};
414+
_.defaults(options, {
415+
wrap: true,
416+
cache: true
417+
});
382418

383-
var $ = this;
384-
if (!_.isPlainObject(conditions)) conditions = this.downstreamID(conditions);
385419

420+
var isID = !_.isPlainObject(conditions);
421+
422+
if (isID) conditions = this.downstreamID(conditions);
386423
this.toSource(conditions);
387-
388-
this.collection.findOne(conditions, function (err, results) {
389-
if (err) return callback(err);
390-
if (!results) return callback(null, null);
391-
392-
return $.onRetrieved(results, callback, wrap || function(value) { return value; });
393-
});
424+
425+
var fromDB = (function() {
426+
this.collection.findOne(conditions, (function (err, results) {
427+
if (err) return callback(err);
428+
if (!results) return callback(null, null);
429+
430+
return this.onRetrieved(results, callback, null, { wrap: options.wrap, cache: options.cache });
431+
}).bind(this));
432+
}).bind(this);
433+
434+
if(isID && this.cache && options.cache)
435+
this.cache.fetch(conditions._id, (function(err, doc) {
436+
if(!err && doc)
437+
return this.onRetrieved(doc, callback, null, { wrap: options.wrap, cache: false });
438+
else
439+
return fromDB();
440+
}).bind(this));
441+
else
442+
return fromDB();
394443
};
395444

396-
Model.prototype.insert = Model.prototype.create = function (object, wrap, callback) {
445+
Model.prototype.insert = Model.prototype.create = function (object, options, callback) {
397446
/// <signature>
398447
/// <summary>Inserts the given object into the database</summary>
399448
/// <param name="object" type="Object">The properties to set on the newly created object</param>
@@ -414,13 +463,13 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba
414463
/// </signature>
415464
/// <summary>Inserts the given object into the database</summary>
416465
/// <param name="object" type="Object">The properties to set on the newly created object</param>
417-
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
466+
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
418467
/// <param name="callback" type="Function">A function to be called once the object has been created</param>
419468
/// </signature>
420469
/// <signature>
421470
/// <summary>Inserts the given object into the database</summary>
422471
/// <param name="object" type="Array" elementType="Object">An array of objects representing the properties to set on the newly created objects</param>
423-
/// <param name="wrap" type="Boolean">Whether or not to wrap results in an Instance object</param>
472+
/// <param name="options" type="Object">Options dictating how Iridium handles this request</param>
424473
/// <param name="callback" type="Function">A function to be called once the objects have been created</param>
425474
/// </signature>
426475

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

431480
if(!callback) {
432-
callback = wrap;
433-
wrap = true;
481+
callback = options;
482+
options = options || {};
434483
}
435484

485+
_.defaults(options, {
486+
wrap: true
487+
});
488+
436489
if(!Array.isArray(object)) {
437490
object = [object];
438491
returnArray = false;
@@ -454,7 +507,7 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba
454507
$.collection.insert(prepped, { w: callback ? 1 : 0 }, function(err, inserted) {
455508
if(err) return end(err);
456509
if(callback)
457-
return $.onRetrieved(inserted, end, wrap || function(value) { return value; });
510+
return $.onRetrieved(inserted, end, null, options);
458511
return end();
459512
});
460513
};

lib/caches/NoOpCache.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module.exports = NoOpCache;
2+
3+
function NoOpCache(options) {
4+
/// <summary>Creates a new cache which performs no caching of instances</summary>
5+
/// <param name="options" type="Object">Options dictating the configuration of this cache</param>
6+
}
7+
8+
NoOpCache.prototype.store = function(document, callback) {
9+
/// <summary>Stores a document in the cache for future access</summary>
10+
/// <param name="document" type="Object">The database object to store in the cache</param>
11+
/// <param name="callback" type="Function">A function which is called once the document has been stored</param>
12+
13+
return callback();
14+
};
15+
16+
NoOpCache.prototype.fetch = function(id, callback) {
17+
/// <summary>Fetches the document with the matching id from the cache</summary>
18+
/// <param name="id" type="Mixed">The _id field of the document to retrieve from the cache</param>
19+
/// <param name="callback" type="Function">A function to call with the retrieved value</param>
20+
21+
return callback(null);
22+
};
23+
24+
NoOpCache.prototype.drop = function(id, callback) {
25+
/// <summary>Removes the document with the matching id from the cache</summary>
26+
/// <param name="id" type="Mixed">The _id field of the document to remove from the cache</param>
27+
/// <param name="callback" type="Function">A function to call once the document has been removed from the cache</param>
28+
29+
return callback(null);
30+
};

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@
2525
"concoction": "*"
2626
},
2727
"devDependencies": {
28-
"async": "*",
2928
"mocha": "*",
30-
"should": "*",
31-
"gitlablist-mocha": "*"
29+
"should": "*"
3230
}
3331
}

0 commit comments

Comments
 (0)