diff --git a/benchmarks/mongodb.js b/benchmarks/mongodb.js index b18b64a..bafbd34 100644 --- a/benchmarks/mongodb.js +++ b/benchmarks/mongodb.js @@ -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(); @@ -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(); diff --git a/lib/Instance.js b/lib/Instance.js index e8bcedb..a3293ec 100644 --- a/lib/Instance.js +++ b/lib/Instance.js @@ -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); @@ -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() { diff --git a/lib/Model.js b/lib/Model.js index 3c11ffb..7dd5d02 100644 --- a/lib/Model.js +++ b/lib/Model.js @@ -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; @@ -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++) { @@ -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) { /// ///Handles any post-receive hooks and the wrapping of objects from the database ///The object retrieved from the database @@ -190,9 +196,23 @@ Model.prototype.onRetrieved = function(results, callback, wrapper) { ///The function to be called once the objects have been wrapped ///A function which converts the retrieved objects prior to submission /// + /// + ///Handles any post-receive hooks and the wrapping of objects from the database + ///The objects retrieved from the database + ///The function to be called once the objects have been wrapped + ///A function which converts the retrieved objects prior to submission + ///A set of options determining how to handle the retrieved object + /// 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]; @@ -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) { @@ -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) { /// /// Gets all objects in the collection. /// A function to be called with the results once they have been retrieved. @@ -291,36 +316,42 @@ Model.prototype.find = function (conditions, wrap, callback) { /// /// /// Gets all objects in the collection. - /// Whether or not to wrap results in an Instance object + /// Options dictating how Iridium handles this request /// A function to be called with the results once they have been retrieved. /// /// /// Finds all occurences in the collection with an _id field matching the given conditions. /// The _id field of the object to locate - /// Whether or not to wrap results in an Instance object + /// Options dictating how Iridium handles this request /// A function to be called with the results once they have been retrieved. /// /// /// Finds all occurences in the collection which match the given conditions. /// The conditions which will be used to select matches - /// Whether or not to wrap results in an Instance object + /// Options dictating how Iridium handles this request /// A function to be called with the results once they have been retrieved. /// 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); @@ -328,11 +359,11 @@ Model.prototype.find = function (conditions, wrap, callback) { 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) { /// /// Gets a single object from the collection. /// A function to be called with the results once they have been retrieved. @@ -349,51 +380,69 @@ Model.prototype.findOne = Model.prototype.get = function (conditions, wrap, call /// /// /// Gets a single object from the collection. - /// Whether or not to wrap results in an Instance object + /// Options dictating how Iridium handles this request /// A function to be called with the results once they have been retrieved. /// /// /// Finds the first occurence in the collection with an _id field matching the given conditions. /// The _id field of the object to locate - /// Whether or not to wrap results in an Instance object + /// Options dictating how Iridium handles this request /// A function to be called with the results once they have been retrieved. /// /// /// Finds the first occurence in the collection which matches the given conditions. /// The conditions which will be used to select matches - /// Whether or not to wrap results in an Instance object + /// Options dictating how Iridium handles this request /// A function to be called with the results once they have been retrieved. /// 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) { /// /// Inserts the given object into the database /// The properties to set on the newly created object @@ -414,13 +463,13 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba /// /// Inserts the given object into the database /// The properties to set on the newly created object - /// Whether or not to wrap results in an Instance object + /// Options dictating how Iridium handles this request /// A function to be called once the object has been created /// /// /// Inserts the given object into the database /// An array of objects representing the properties to set on the newly created objects - /// Whether or not to wrap results in an Instance object + /// Options dictating how Iridium handles this request /// A function to be called once the objects have been created /// @@ -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; @@ -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(); }); }; diff --git a/lib/caches/NoOpCache.js b/lib/caches/NoOpCache.js new file mode 100644 index 0000000..aafe7f3 --- /dev/null +++ b/lib/caches/NoOpCache.js @@ -0,0 +1,30 @@ +module.exports = NoOpCache; + +function NoOpCache(options) { + /// Creates a new cache which performs no caching of instances + /// Options dictating the configuration of this cache +} + +NoOpCache.prototype.store = function(document, callback) { + /// Stores a document in the cache for future access + /// The database object to store in the cache + /// A function which is called once the document has been stored + + return callback(); +}; + +NoOpCache.prototype.fetch = function(id, callback) { + /// Fetches the document with the matching id from the cache + /// The _id field of the document to retrieve from the cache + /// A function to call with the retrieved value + + return callback(null); +}; + +NoOpCache.prototype.drop = function(id, callback) { + /// Removes the document with the matching id from the cache + /// The _id field of the document to remove from the cache + /// A function to call once the document has been removed from the cache + + return callback(null); +}; \ No newline at end of file diff --git a/package.json b/package.json index b549583..3ea6bf1 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,7 @@ "concoction": "*" }, "devDependencies": { - "async": "*", "mocha": "*", - "should": "*", - "gitlablist-mocha": "*" + "should": "*" } } \ No newline at end of file diff --git a/test/cache.js b/test/cache.js new file mode 100644 index 0000000..48dfd1e --- /dev/null +++ b/test/cache.js @@ -0,0 +1,106 @@ +/// +/// +/// +/// + +var config = require('./config'); +var Database = require('../index'); +var Model = Database.Model; +var Instance = Database.Instance; +var should = require('should'); +var Concoction = require('concoction'); +var EventEmitter = require('events').EventEmitter; + +function EventEmitterCache() { + this.cache = {}; +} + +EventEmitterCache.prototype.__proto__ = EventEmitter.prototype; +EventEmitterCache.prototype.store = function(document, callback) { + this.emit('store'); + var id = JSON.stringify(document._id); + this.cache[id] = document; + callback(); +}; +EventEmitterCache.prototype.fetch = function(id, callback) { + id = JSON.stringify(id); + if(this.cache[id]) this.emit('fetched'); + callback(this.cache[id]); +}; +EventEmitterCache.prototype.drop = function(id, callback) { + id = JSON.stringify(id); + if(this.cache[id]) { + delete this.cache[id]; + this.emit('dropped'); + } + callback(); +}; + +describe('orm', function () { + "use strict"; + + describe('Model', function () { + var db = null; + + before(function (done) { + db = new Database(config); + db.connect(done); + }); + + describe('cache', function() { + var model = null; + + before(function(done) { + model = new Model(db, 'model', { + name: /.+/ + }, { + preprocessors: [new Concoction.Rename({ _id: 'name' })], + cache: new EventEmitterCache() + }); + + model.remove(function(err) { + if(err) return done(err); + + model.create({ + name: 'Demo1' + }, function(err, instance) { + if(err) return done(err); + return done(); + }); + }); + }); + + describe('findOne', function() { + it('should store newly retrieved documents in the cache', function(done) { + var pending = 2; + function almostDone() { + if(!(--pending)) return done(); + } + + model.cache.once('store', almostDone); + + model.findOne('Demo1', function(err, instance) { + should.not.exist(err); + should.exist(instance); + almostDone(); + }); + }); + + it('should fetch retrieved documents from the cache', function(done) { + var pending = 2; + function almostDone() { + if(!(--pending)) return done(); + } + + model.cache.once('fetched', almostDone); + + model.findOne('Demo1', function(err, instance) { + should.not.exist(err); + should.exist(instance); + almostDone(); + }); + }); + }); + }); + }); +});